Skip to content

Commit

Permalink
Extended heat map to calculate excess return
Browse files Browse the repository at this point in the history
Optionally, the user can choose a baseline to calculate the excess
return against. Two methods are available: alpha excess return and
relative excess return.
  • Loading branch information
buchen committed Sep 29, 2019
1 parent d05192d commit 19ea81f
Show file tree
Hide file tree
Showing 12 changed files with 180 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,14 @@ LabelError = Error
LabelEurostatRegion = Region
LabelExcessReturnBaselineDataSeries = Excess return (Baseline)
LabelExcessReturnOperator = Calculation
LabelExcessReturnOperatorAlpha = Alpha
LabelExcessReturnOperatorRelative = Relativ
LabelExchange = Exchange
LabelExchangeRate = Exchange Rate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -999,6 +999,14 @@ LabelError = Fout
LabelEurostatRegion = Regio
LabelExcessReturnBaselineDataSeries = Overtollig rendement (Baseline)
LabelExcessReturnOperator = Berekening
LabelExcessReturnOperatorAlpha = Alpha
LabelExcessReturnOperatorRelative = Relatief
LabelExchange = Effectenbeurs
LabelExchangeRate = Wisselkoers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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)));

Expand All @@ -66,20 +78,20 @@ private void doAddSeries(boolean showOnlyBenchmark)
dialog.setElements(list);
dialog.setMultiSelection(false);

if (dialog.open() != DataSeriesSelectionDialog.OK)
if (dialog.open() != DataSeriesSelectionDialog.OK) // NOSONAR
return;

List<DataSeries> result = dialog.getResult();
if (result.isEmpty())
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();
Expand All @@ -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$
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,17 +26,31 @@ public enum Policy
private final String label;
private final Class<E> 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<E> values;

public EnumBasedConfig(WidgetDelegate<?> delegate, String label, Class<E> type, Dashboard.Config configurationKey,
Policy policy)
{
this(delegate, label, type, configurationKey, policy, null);
}

public EnumBasedConfig(WidgetDelegate<?> delegate, String label, Class<E> type, Dashboard.Config configurationKey,
Policy policy, String pathToMenu)
{
this.delegate = delegate;
this.configurationKey = configurationKey;
this.label = label;
this.type = type;
this.policy = policy;

this.pathToMenu = pathToMenu;

this.values = EnumSet.noneOf(type);

String code = delegate.getWidget().getConfiguration().get(configurationKey.name());
Expand All @@ -62,10 +77,19 @@ public EnumBasedConfig(WidgetDelegate<?> delegate, String label, Class<E> 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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}

}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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<ExcessReturnOperator>
{
public ExcessReturnOperatorConfig(WidgetDelegate<?> delegate)
{
super(delegate, Messages.LabelExcessReturnOperator, ExcessReturnOperator.class,
Dashboard.Config.CALCULATION_METHOD, Policy.EXACTLY_ONE,
Dashboard.Config.SECONDARY_DATA_SERIES.name());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -23,30 +25,46 @@ 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
protected HeatmapModel<Double> 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(
interval.getStart().getDayOfMonth() == interval.getStart().lengthOfMonth() ? interval.getStart()
: 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<LocalDate> calculatePerformance = month -> getPerformanceFor(performanceIndex, month);
ToDoubleFunction<LocalDate> 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);
Expand All @@ -58,8 +76,11 @@ protected HeatmapModel<Double> 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);
Expand All @@ -70,14 +91,14 @@ protected HeatmapModel<Double> 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)));
Expand All @@ -98,12 +119,12 @@ protected HeatmapModel<Double> 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
Expand All @@ -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)
Expand Down
Loading

0 comments on commit 19ea81f

Please sign in to comment.