From 19ea81f4949dd027c2c39aeda8d30d758b54a189 Mon Sep 17 00:00:00 2001 From: Andreas Buchen Date: Sun, 29 Sep 2019 10:21:01 +0200 Subject: [PATCH] Extended heat map to calculate excess return Optionally, the user can choose a baseline to calculate the excess return against. Two methods are available: alpha excess return and relative excess return. --- .../name/abuchen/portfolio/ui/Messages.java | 4 ++ .../abuchen/portfolio/ui/messages.properties | 8 ++++ .../portfolio/ui/messages_de.properties | 8 ++++ .../portfolio/ui/messages_es.properties | 8 ++++ .../portfolio/ui/messages_nl.properties | 8 ++++ .../ui/views/dashboard/DataSeriesConfig.java | 32 +++++++++----- .../ui/views/dashboard/EnumBasedConfig.java | 28 +++++++++++- .../heatmap/ExcessReturnDataSeriesConfig.java | 15 +++++++ .../heatmap/ExcessReturnOperator.java | 31 +++++++++++++ .../heatmap/ExcessReturnOperatorConfig.java | 16 +++++++ .../heatmap/PerformanceHeatmapWidget.java | 43 ++++++++++++++----- .../abuchen/portfolio/model/Dashboard.java | 4 +- 12 files changed, 180 insertions(+), 25 deletions(-) create mode 100644 name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/ExcessReturnDataSeriesConfig.java create mode 100644 name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/ExcessReturnOperator.java create mode 100644 name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/ExcessReturnOperatorConfig.java diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Messages.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Messages.java index ed45482425..a9f5d1bd91 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Messages.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/Messages.java @@ -500,6 +500,10 @@ public class Messages extends NLS public static String LabelEarningsSelectStartYear; public static String LabelError; public static String LabelEurostatRegion; + public static String LabelExcessReturnBaselineDataSeries; + public static String LabelExcessReturnOperator; + public static String LabelExcessReturnOperatorAlpha; + public static String LabelExcessReturnOperatorRelative; public static String LabelExchange; public static String LabelExchangeRate; public static String LabelExchangeRates; diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties index aeb1d19e78..1c6e7eeef9 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages.properties @@ -1007,6 +1007,14 @@ LabelError = Error LabelEurostatRegion = Region +LabelExcessReturnBaselineDataSeries = Excess return (Baseline) + +LabelExcessReturnOperator = Calculation + +LabelExcessReturnOperatorAlpha = Alpha + +LabelExcessReturnOperatorRelative = Relativ + LabelExchange = Exchange LabelExchangeRate = Exchange Rate diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties index 7345a4ae1f..cf7f5a7a76 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_de.properties @@ -1003,6 +1003,14 @@ LabelEarningsSelectStartYear = Ertr\u00E4ge ab Jahr: LabelError = Fehler +LabelExcessReturnBaselineDataSeries = \u00DCberrendite (Basiswert) + +LabelExcessReturnOperator = Berechnung + +LabelExcessReturnOperatorAlpha = Alpha + +LabelExcessReturnOperatorRelative = Relativ + LabelExchange = B\u00F6rsenplatz LabelExchangeRate = Wechselkurs diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_es.properties b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_es.properties index 10da8e4376..a12477d815 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_es.properties +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_es.properties @@ -891,6 +891,14 @@ LabelEarningsSelectStartYear = Ganancias a partir del a\u00F1o: LabelError = Error +LabelExcessReturnBaselineDataSeries = Exceso de retorno (L\u00EDnea de base) + +LabelExcessReturnOperator = Calculo + +LabelExcessReturnOperatorAlpha = Alpha + +LabelExcessReturnOperatorRelative = Pariente + LabelExchange = Exchange LabelExchangeRate = Tipo de cambio diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_nl.properties b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_nl.properties index b44edbf149..056f248ec5 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_nl.properties +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/messages_nl.properties @@ -999,6 +999,14 @@ LabelError = Fout LabelEurostatRegion = Regio +LabelExcessReturnBaselineDataSeries = Overtollig rendement (Baseline) + +LabelExcessReturnOperator = Berekening + +LabelExcessReturnOperatorAlpha = Alpha + +LabelExcessReturnOperatorRelative = Relatief + LabelExchange = Effectenbeurs LabelExchangeRate = Wisselkoers diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/DataSeriesConfig.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/DataSeriesConfig.java index c34b89238b..37b61504a4 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/DataSeriesConfig.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/DataSeriesConfig.java @@ -19,18 +19,28 @@ public class DataSeriesConfig implements WidgetConfig { private final WidgetDelegate delegate; private final boolean supportsBenchmarks; + private final String label; + private final Dashboard.Config configurationKey; private DataSeries dataSeries; public DataSeriesConfig(WidgetDelegate delegate, boolean supportsBenchmarks) + { + this(delegate, supportsBenchmarks, false, Messages.LabelDataSeries, Dashboard.Config.DATA_SERIES); + } + + protected DataSeriesConfig(WidgetDelegate delegate, boolean supportsBenchmarks, boolean supportsEmptyDataSeries, + String label, Dashboard.Config configurationKey) { this.delegate = delegate; this.supportsBenchmarks = supportsBenchmarks; + this.label = label; + this.configurationKey = configurationKey; - String uuid = delegate.getWidget().getConfiguration().get(Dashboard.Config.DATA_SERIES.name()); + String uuid = delegate.getWidget().getConfiguration().get(configurationKey.name()); if (uuid != null && !uuid.isEmpty()) dataSeries = delegate.getDashboardData().getDataSeriesSet().lookup(uuid); - if (dataSeries == null) + if (dataSeries == null && !supportsEmptyDataSeries) dataSeries = delegate.getDashboardData().getDataSeriesSet().getAvailableSeries().stream() .filter(ds -> ds.getType().equals(DataSeries.Type.CLIENT)).findAny() .orElseThrow(IllegalArgumentException::new); @@ -44,10 +54,12 @@ public DataSeries getDataSeries() @Override public void menuAboutToShow(IMenuManager manager) { - manager.appendToGroup(DashboardView.INFO_MENU_GROUP_NAME, new LabelOnly(dataSeries.getLabel())); + manager.appendToGroup(DashboardView.INFO_MENU_GROUP_NAME, new LabelOnly(getLabel())); - MenuManager subMenu = new MenuManager(Messages.LabelDataSeries); - subMenu.add(new LabelOnly(dataSeries.getLabel())); + // use configurationKey as contribution id to allow other context menus + // to attach to this menu manager later + MenuManager subMenu = new MenuManager(label, configurationKey.name()); + subMenu.add(new LabelOnly(dataSeries != null ? dataSeries.getLabel() : "-")); //$NON-NLS-1$ subMenu.add(new Separator()); subMenu.add(new SimpleAction(Messages.MenuSelectDataSeries, a -> doAddSeries(false))); @@ -66,7 +78,7 @@ private void doAddSeries(boolean showOnlyBenchmark) dialog.setElements(list); dialog.setMultiSelection(false); - if (dialog.open() != DataSeriesSelectionDialog.OK) + if (dialog.open() != DataSeriesSelectionDialog.OK) // NOSONAR return; List result = dialog.getResult(); @@ -74,12 +86,12 @@ private void doAddSeries(boolean showOnlyBenchmark) return; dataSeries = result.get(0); - delegate.getWidget().getConfiguration().put(Dashboard.Config.DATA_SERIES.name(), dataSeries.getUUID()); + delegate.getWidget().getConfiguration().put(configurationKey.name(), dataSeries.getUUID()); // construct label to indicate the data series (user can manually change // the label later) - String label = WidgetFactory.valueOf(delegate.getWidget().getType()).getLabel() + ", " + dataSeries.getLabel(); //$NON-NLS-1$ - delegate.getWidget().setLabel(label); + delegate.getWidget().setLabel(WidgetFactory.valueOf(delegate.getWidget().getType()).getLabel() + ", " //$NON-NLS-1$ + + dataSeries.getLabel()); delegate.update(); delegate.getClient().touch(); @@ -88,6 +100,6 @@ private void doAddSeries(boolean showOnlyBenchmark) @Override public String getLabel() { - return Messages.LabelDataSeries + ": " + dataSeries.getLabel(); //$NON-NLS-1$ + return label + ": " + (dataSeries != null ? dataSeries.getLabel() : "-"); //$NON-NLS-1$ //$NON-NLS-2$ } } diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/EnumBasedConfig.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/EnumBasedConfig.java index 7b6f936d3d..7e174274ac 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/EnumBasedConfig.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/EnumBasedConfig.java @@ -7,6 +7,7 @@ import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; +import org.eclipse.jface.action.Separator; import name.abuchen.portfolio.model.Dashboard; import name.abuchen.portfolio.ui.PortfolioPlugin; @@ -25,10 +26,22 @@ public enum Policy private final String label; private final Class type; + /** + * If not null, display the context menu at this path (and not at the top + * level of the widget configuration menu). + */ + private final String pathToMenu; + private EnumSet values; public EnumBasedConfig(WidgetDelegate delegate, String label, Class type, Dashboard.Config configurationKey, Policy policy) + { + this(delegate, label, type, configurationKey, policy, null); + } + + public EnumBasedConfig(WidgetDelegate delegate, String label, Class type, Dashboard.Config configurationKey, + Policy policy, String pathToMenu) { this.delegate = delegate; this.configurationKey = configurationKey; @@ -36,6 +49,8 @@ public EnumBasedConfig(WidgetDelegate delegate, String label, Class type, this.type = type; this.policy = policy; + this.pathToMenu = pathToMenu; + this.values = EnumSet.noneOf(type); String code = delegate.getWidget().getConfiguration().get(configurationKey.name()); @@ -62,10 +77,19 @@ public EnumBasedConfig(WidgetDelegate delegate, String label, Class type, public void menuAboutToShow(IMenuManager manager) { MenuManager subMenu = new MenuManager(label); - manager.add(subMenu); - for (E v : type.getEnumConstants()) subMenu.add(buildAction(v)); + + if (pathToMenu != null) + { + IMenuManager alternative = manager.findMenuUsingPath(pathToMenu); + alternative.add(new Separator()); + alternative.add(subMenu); + } + else + { + manager.add(subMenu); + } } private Action buildAction(E value) diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/ExcessReturnDataSeriesConfig.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/ExcessReturnDataSeriesConfig.java new file mode 100644 index 0000000000..61ea7f577c --- /dev/null +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/ExcessReturnDataSeriesConfig.java @@ -0,0 +1,15 @@ +package name.abuchen.portfolio.ui.views.dashboard.heatmap; + +import name.abuchen.portfolio.model.Dashboard; +import name.abuchen.portfolio.ui.Messages; +import name.abuchen.portfolio.ui.views.dashboard.DataSeriesConfig; +import name.abuchen.portfolio.ui.views.dashboard.WidgetDelegate; + +public class ExcessReturnDataSeriesConfig extends DataSeriesConfig +{ + public ExcessReturnDataSeriesConfig(WidgetDelegate delegate) + { + super(delegate, true, true, Messages.LabelExcessReturnBaselineDataSeries, Dashboard.Config.SECONDARY_DATA_SERIES); + } + +} diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/ExcessReturnOperator.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/ExcessReturnOperator.java new file mode 100644 index 0000000000..e0cd7eb456 --- /dev/null +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/ExcessReturnOperator.java @@ -0,0 +1,31 @@ +package name.abuchen.portfolio.ui.views.dashboard.heatmap; + +import java.util.function.DoubleBinaryOperator; + +import name.abuchen.portfolio.ui.Messages; + +public enum ExcessReturnOperator +{ + ALPHA(Messages.LabelExcessReturnOperatorAlpha, (x, y) -> x - y), // + RELATIVE(Messages.LabelExcessReturnOperatorRelative, (x, y) -> ((x + 1) / (y + 1)) - 1); + + private String label; + private DoubleBinaryOperator operator; + + private ExcessReturnOperator(String label, DoubleBinaryOperator operator) + { + this.label = label; + this.operator = operator; + } + + public DoubleBinaryOperator getOperator() + { + return operator; + } + + @Override + public String toString() + { + return label; + } +} diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/ExcessReturnOperatorConfig.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/ExcessReturnOperatorConfig.java new file mode 100644 index 0000000000..d6e760cb0f --- /dev/null +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/ExcessReturnOperatorConfig.java @@ -0,0 +1,16 @@ +package name.abuchen.portfolio.ui.views.dashboard.heatmap; + +import name.abuchen.portfolio.model.Dashboard; +import name.abuchen.portfolio.ui.Messages; +import name.abuchen.portfolio.ui.views.dashboard.EnumBasedConfig; +import name.abuchen.portfolio.ui.views.dashboard.WidgetDelegate; + +class ExcessReturnOperatorConfig extends EnumBasedConfig +{ + public ExcessReturnOperatorConfig(WidgetDelegate delegate) + { + super(delegate, Messages.LabelExcessReturnOperator, ExcessReturnOperator.class, + Dashboard.Config.CALCULATION_METHOD, Policy.EXACTLY_ONE, + Dashboard.Config.SECONDARY_DATA_SERIES.name()); + } +} diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/PerformanceHeatmapWidget.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/PerformanceHeatmapWidget.java index 6b1d4d5224..3644e6d844 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/PerformanceHeatmapWidget.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dashboard/heatmap/PerformanceHeatmapWidget.java @@ -3,6 +3,8 @@ import java.time.LocalDate; import java.time.Year; import java.util.Arrays; +import java.util.function.DoubleBinaryOperator; +import java.util.function.ToDoubleFunction; import name.abuchen.portfolio.model.Dashboard.Widget; import name.abuchen.portfolio.money.Values; @@ -23,6 +25,8 @@ public PerformanceHeatmapWidget(Widget widget, DashboardData data) addConfig(new ColorSchemaConfig(this)); addConfig(new HeatmapOrnamentConfig(this)); addConfig(new DataSeriesConfig(this, true)); + addConfig(new ExcessReturnDataSeriesConfig(this)); + addConfig(new ExcessReturnOperatorConfig(this)); } @Override @@ -30,14 +34,8 @@ protected HeatmapModel build() { int numDashboardColumns = getDashboardData().getDashboard().getColumns().size(); - // fill the table lines according to the supplied period - // calculate the performance with a temporary reporting period - // calculate the color interpolated between red and green with yellow as - // the median Interval interval = get(ReportingPeriodConfig.class).getReportingPeriod().toInterval(LocalDate.now()); - DataSeries dataSeries = get(DataSeriesConfig.class).getDataSeries(); - // adapt interval to include the first and last month fully Interval calcInterval = Interval.of( @@ -45,8 +43,28 @@ protected HeatmapModel build() : interval.getStart().withDayOfMonth(1).minusDays(1), interval.getEnd().withDayOfMonth(interval.getEnd().lengthOfMonth())); + DataSeries dataSeries = get(DataSeriesConfig.class).getDataSeries(); PerformanceIndex performanceIndex = getDashboardData().calculate(dataSeries, calcInterval); + // build functions to calculate performance and sum values + + ToDoubleFunction calculatePerformance = month -> getPerformanceFor(performanceIndex, month); + ToDoubleFunction calculateSum = year -> getSumPerformance(performanceIndex, year); + + DataSeries benchmark = get(ExcessReturnDataSeriesConfig.class).getDataSeries(); + if (benchmark != null) + { + PerformanceIndex benchmarkIndex = getDashboardData().calculate(benchmark, calcInterval); + + DoubleBinaryOperator operator = get(ExcessReturnOperatorConfig.class).getValue().getOperator(); + calculatePerformance = month -> operator.applyAsDouble( // + getPerformanceFor(performanceIndex, month), getPerformanceFor(benchmarkIndex, month)); + calculateSum = year -> operator.applyAsDouble( // + getSumPerformance(performanceIndex, year), getSumPerformance(benchmarkIndex, year)); + } + + // build heat map model for actual interval + Interval actualInterval = performanceIndex.getActualInterval(); boolean showSum = get(HeatmapOrnamentConfig.class).getValues().contains(HeatmapOrnament.SUM); @@ -58,8 +76,11 @@ protected HeatmapModel build() model.setCellToolTip(v -> Messages.PerformanceHeatmapToolTip); // add header + addMonthlyHeader(model, numDashboardColumns, showSum, showStandardDeviation); + // build row for each year + for (Year year : actualInterval.getYears()) { String label = numDashboardColumns > 2 ? String.valueOf(year.getValue() % 100) : String.valueOf(year); @@ -70,14 +91,14 @@ protected HeatmapModel build() .getValue(); month = month.plusMonths(1)) { if (actualInterval.contains(month)) - row.addData(getPerformanceFor(performanceIndex, month)); + row.addData(calculatePerformance.applyAsDouble(month)); else row.addData(null); } // sum if (showSum) - row.addData(getSumPerformance(performanceIndex, LocalDate.of(year.getValue(), 1, 1))); + row.addData(calculateSum.applyAsDouble(LocalDate.of(year.getValue(), 1, 1))); if (showStandardDeviation) row.addData(standardDeviation(row.getDataSubList(0, 12))); @@ -98,12 +119,12 @@ protected HeatmapModel build() return model; } - private Double getPerformanceFor(PerformanceIndex index, LocalDate month) + private double getPerformanceFor(PerformanceIndex index, LocalDate month) { int start = Arrays.binarySearch(index.getDates(), month.minusDays(1)); // should not happen, but let's be defensive this time if (start < 0) - return null; + start = 0; int end = Arrays.binarySearch(index.getDates(), month.withDayOfMonth(month.lengthOfMonth())); // make sure there is an end index if the binary search returns a @@ -117,7 +138,7 @@ private Double getPerformanceFor(PerformanceIndex index, LocalDate month) return ((index.getAccumulatedPercentage()[end] + 1) / (index.getAccumulatedPercentage()[start] + 1)) - 1; } - private Double getSumPerformance(PerformanceIndex index, LocalDate year) + private double getSumPerformance(PerformanceIndex index, LocalDate year) { int start = Arrays.binarySearch(index.getDates(), year.minusDays(1)); if (start < 0) diff --git a/name.abuchen.portfolio/src/name/abuchen/portfolio/model/Dashboard.java b/name.abuchen.portfolio/src/name/abuchen/portfolio/model/Dashboard.java index 9d2b861d11..8ff9d2c501 100644 --- a/name.abuchen.portfolio/src/name/abuchen/portfolio/model/Dashboard.java +++ b/name.abuchen.portfolio/src/name/abuchen/portfolio/model/Dashboard.java @@ -9,9 +9,9 @@ public final class Dashboard { public enum Config { - REPORTING_PERIOD, DATA_SERIES, CONFIG_UUID, AGGREGATION, EXCHANGE_RATE_SERIES, COLOR_SCHEMA, LAYOUT, HEIGHT, EARNING_TYPE, NET_GROSS, CLIENT_FILTER; + REPORTING_PERIOD, DATA_SERIES, SECONDARY_DATA_SERIES, CONFIG_UUID, AGGREGATION, EXCHANGE_RATE_SERIES, COLOR_SCHEMA, LAYOUT, HEIGHT, EARNING_TYPE, NET_GROSS, CLIENT_FILTER, CALCULATION_METHOD; } - + public static final class Column { private List widgets = new ArrayList<>();