diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/Navigation.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/Navigation.java index 467f09a833..9fa4ce2958 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/Navigation.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/editor/Navigation.java @@ -45,6 +45,7 @@ import name.abuchen.portfolio.ui.views.BrowserTestView; import name.abuchen.portfolio.ui.views.GroupedAccountsListView; import name.abuchen.portfolio.ui.views.InvestmentPlanListView; +import name.abuchen.portfolio.ui.views.IRRChartView; import name.abuchen.portfolio.ui.views.PerformanceChartView; import name.abuchen.portfolio.ui.views.PerformanceView; import name.abuchen.portfolio.ui.views.PortfolioListView; @@ -499,6 +500,7 @@ private void createPerformanceSection() performance.add(new Item(Messages.ClientEditorPerformanceCalculation, PerformanceView.class)); performance.add(new Item(Messages.ClientEditorLabelChart, PerformanceChartView.class, true)); + performance.add(new Item("IRR", IRRChartView.class, true)); performance.add(new Item(Messages.ClientEditorLabelReturnsVolatility, ReturnsVolatilityChartView.class, true)); performance.add(new Item(Messages.LabelSecurities, SecuritiesPerformanceView.class)); performance.add(new Item(Messages.LabelPayments, PaymentsView.class, true)); diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/util/chart/TimelineChart.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/util/chart/TimelineChart.java index 7cc746c056..6c888da942 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/util/chart/TimelineChart.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/util/chart/TimelineChart.java @@ -55,6 +55,20 @@ public long getEpochDay() } } + private static class HorizontalMarkerLine + { + private Color color; + private String label; + private double value; + + private HorizontalMarkerLine(Color color, String label, double value) + { + this.color = color; + this.label = label; + this.value = value; + } + } + private static class NonTradingDayMarker { private LocalDate date; @@ -73,6 +87,7 @@ public long getEpochDay() } private List markerLines = new ArrayList<>(); + private List horizMarkerLines = new ArrayList<>(); private List nonTradingDayMarkers = new ArrayList<>(); private Map addedAxis = new HashMap<>(); @@ -152,6 +167,16 @@ public void clearMarkerLines() this.markerLines.clear(); } + public void addHorizontalMarkerLine(double value, Color color, String label) + { + this.horizMarkerLines.add(new HorizontalMarkerLine(color, label, value)); + } + + public void clearHorizontalMarkerLines() + { + this.horizMarkerLines.clear(); + } + public void addNonTradingDayMarker(LocalDate date, Color color) { this.nonTradingDayMarkers.add(new NonTradingDayMarker(date, color)); @@ -251,7 +276,7 @@ private void paintTimeGrid(PaintEvent e) private void paintMarkerLines(PaintEvent e) // NOSONAR { - if (markerLines.isEmpty()) + if (markerLines.isEmpty() && horizMarkerLines.isEmpty()) return; IAxis xAxis = getAxisSet().getXAxis(0); @@ -286,6 +311,22 @@ private void paintMarkerLines(PaintEvent e) // NOSONAR e.gc.drawLine(x - 5, y, x + 5, y); } } + + for (HorizontalMarkerLine marker : horizMarkerLines) + { + int y = yAxis.getPixelCoordinate(marker.value); + + e.gc.setLineStyle(SWT.LINE_DASHDOT); + e.gc.setForeground(marker.color); + e.gc.setLineWidth(1); + e.gc.drawLine(0, y, e.width, y); + + if (marker.label != null) + { + Point textExtent = e.gc.textExtent(marker.label); + e.gc.drawText(marker.label, 10, y - textExtent.y - 2, true); + } + } } private void paintNonTradingDayMarker(PaintEvent eventNonTradingDay) // NOSONAR diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/IRRChartView.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/IRRChartView.java new file mode 100644 index 0000000000..17c9feaa2f --- /dev/null +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/IRRChartView.java @@ -0,0 +1,209 @@ +package name.abuchen.portfolio.ui.views; + +import java.io.File; +import java.io.IOException; +import java.text.DecimalFormat; +import java.time.LocalDate; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import jakarta.annotation.PostConstruct; + +import org.eclipse.jface.action.Action; +import org.eclipse.jface.action.IMenuListener; +import org.eclipse.jface.action.IMenuManager; +import org.eclipse.jface.action.Separator; +import org.eclipse.jface.action.ToolBarManager; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Shell; + +import name.abuchen.portfolio.ui.Images; +import name.abuchen.portfolio.ui.Messages; +import name.abuchen.portfolio.ui.util.AbstractCSVExporter; +import name.abuchen.portfolio.ui.util.Colors; +import name.abuchen.portfolio.ui.util.DropDown; +import name.abuchen.portfolio.ui.util.chart.TimelineChart; +import name.abuchen.portfolio.ui.views.AbstractHistoricView; +import name.abuchen.portfolio.ui.util.chart.TimelineChartCSVExporter; +import name.abuchen.portfolio.ui.util.format.AxisTickPercentNumberFormat; +import name.abuchen.portfolio.snapshot.IRRSeries; +import name.abuchen.portfolio.snapshot.PerformanceIndex; +import name.abuchen.portfolio.money.CurrencyConverter; +import name.abuchen.portfolio.money.CurrencyConverterImpl; +import name.abuchen.portfolio.money.ExchangeRateProviderFactory; +import name.abuchen.portfolio.util.ColorConversion; + +public class IRRChartView extends AbstractHistoricView +{ + private TimelineChart chart; + + @Override + protected String getDefaultTitle() + { + return "IRR"; + } + + @Override + protected Composite createBody(Composite parent) + { + Composite composite = new Composite(parent, SWT.NONE); + composite.setBackground(Colors.theme().defaultBackground()); + + chart = new TimelineChart(composite); + chart.getTitle().setText("IRR"); + chart.getTitle().setVisible(false); + chart.getAxisSet().getYAxis(0).getTick().setFormat(new AxisTickPercentNumberFormat("0.#%")); //$NON-NLS-1$ + chart.getToolTip().setDefaultValueFormat(new DecimalFormat("0.##%")); + chart.getToolTip().reverseLabels(true); + + GridLayoutFactory.fillDefaults().numColumns(1).margins(0, 0).spacing(0, 0).applyTo(composite); + GridDataFactory.fillDefaults().grab(true, true).applyTo(chart); + + updateChart(); + return composite; + } + + @Override + protected void addButtons(ToolBarManager toolBar) + { + super.addButtons(toolBar); + toolBar.add(new ExportDropDown()); + } + + @Override + public void setFocus() + { + chart.adjustRange(); + chart.setFocus(); + } + + @Override + public void reportingPeriodUpdated() + { + notifyModelUpdated(); + } + + @Override + public void notifyModelUpdated() + { + updateChart(); + } + + private void updateChart() + { + chart.suspendUpdate(true); + try + { + chart.getTitle().setText("IRR"); + for (var s : chart.getSeriesSet().getSeries()) + chart.getSeriesSet().deleteSeries(s.getId()); + + chart.clearHorizontalMarkerLines(); + + ExchangeRateProviderFactory factory = make(ExchangeRateProviderFactory.class); + CurrencyConverter converter = new CurrencyConverterImpl(factory, getClient().getBaseCurrency()); + java.util.List warnings = new java.util.ArrayList<>(); + PerformanceIndex clientIndex = PerformanceIndex.forClient(getClient(), converter, getReportingPeriod().toInterval(LocalDate.now()), warnings); + + var irrValues = IRRSeries.calculate(clientIndex); + var allDates = clientIndex.getDates(); + + double maxTotal = 0.1; + double minTotal = -0.1; + double[] totals = clientIndex.getAccumulatedPercentage(); + if (totals != null) { + for (double v : totals) { + if (Double.isFinite(v)) { + if (v > maxTotal) maxTotal = v; + if (v < minTotal) minTotal = v; + } + } + } + + double capMax = Math.max(0.2, maxTotal * 1.5); + double capMin = Math.min(-0.2, minTotal * 1.5); + + int validCount = 0; + for (double v : irrValues) { + if (Double.isFinite(v)) validCount++; + } + + LocalDate[] validDates = new LocalDate[validCount]; + double[] validValues = new double[validCount]; + int idx = 0; + for (int i = 0; i < irrValues.length; i++) { + if (Double.isFinite(irrValues[i])) { + validDates[idx] = allDates[i]; + validValues[idx] = Math.max(capMin, Math.min(capMax, irrValues[i])); + idx++; + } + } + + if (validCount > 0) { + var irrLineSeries = chart.addDateSeries("irr-main-series", + validDates, validValues, Messages.LabelIRR); + irrLineSeries.setLineColor(Colors.getColor(ColorConversion.hex2RGB("#034EFF"))); + irrLineSeries.setLineWidth(2); + + double[] sortedValues = Arrays.copyOf(validValues, validCount); + Arrays.sort(sortedValues); + + double minV = sortedValues[0]; + double p5 = sortedValues[Math.max(0, (int)(validCount * 0.05))]; + double median = sortedValues[Math.max(0, (int)(validCount * 0.50))]; + double p95 = sortedValues[Math.min(validCount - 1, (int)(validCount * 0.95))]; + double maxV = sortedValues[validCount - 1]; + + var colorMin = Colors.getColor(ColorConversion.hex2RGB("#D90429")); + var colorP5 = Colors.getColor(ColorConversion.hex2RGB("#F27438")); + var colorMed = Colors.getColor(ColorConversion.hex2RGB("#E6AF2E")); + var colorP95 = Colors.getColor(ColorConversion.hex2RGB("#8CD373")); + var colorMax = Colors.getColor(ColorConversion.hex2RGB("#2B9348")); + + chart.addHorizontalMarkerLine(minV, colorMin, String.format("Mínimo (%.1f%%)", minV * 100)); + chart.addHorizontalMarkerLine(p5, colorP5, String.format("Percentil 5%% (%.1f%%)", p5 * 100)); + chart.addHorizontalMarkerLine(median, colorMed, String.format("Mediana (%.1f%%)", median * 100)); + chart.addHorizontalMarkerLine(p95, colorP95, String.format("Percentil 95%% (%.1f%%)", p95 * 100)); + chart.addHorizontalMarkerLine(maxV, colorMax, String.format("Máximo (%.1f%%)", maxV * 100)); + } + + chart.adjustRange(); + } + finally + { + chart.suspendUpdate(false); + } + chart.redraw(); + } + + private final class ExportDropDown extends DropDown implements IMenuListener + { + private ExportDropDown() + { + super(Messages.MenuExportData, Images.EXPORT, SWT.NONE); + setMenuListener(this); + } + + @Override + public void menuAboutToShow(IMenuManager manager) + { + manager.add(new Action(Messages.MenuExportChartData) + { + @Override + public void run() + { + TimelineChartCSVExporter exporter = new TimelineChartCSVExporter(chart); + exporter.setValueFormat(new DecimalFormat("0.##########")); //$NON-NLS-1$ + exporter.export("IRR.csv"); //$NON-NLS-1$ + } + }); + + manager.add(new Separator()); + chart.exportMenuAboutToShow(manager, "IRR"); + } + } +} diff --git a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dataseries/PerformanceChartSeriesBuilder.java b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dataseries/PerformanceChartSeriesBuilder.java index d9055b44b8..975f65b19e 100644 --- a/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dataseries/PerformanceChartSeriesBuilder.java +++ b/name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/dataseries/PerformanceChartSeriesBuilder.java @@ -1,10 +1,13 @@ package name.abuchen.portfolio.ui.views.dataseries; import java.time.LocalDate; +import java.util.Arrays; import org.eclipse.swt.graphics.Color; +import org.eclipse.swtchart.LineStyle; import name.abuchen.portfolio.snapshot.Aggregation; +import name.abuchen.portfolio.snapshot.IRRSeries; import name.abuchen.portfolio.snapshot.PerformanceIndex; import name.abuchen.portfolio.ui.Messages; import name.abuchen.portfolio.ui.util.Colors;