package name.abuchen.portfolio.ui.views; import java.text.DecimalFormat; import java.time.LocalDate; import java.time.Period; import java.time.temporal.TemporalAmount; import java.util.Collections; import java.util.EnumSet; import java.util.List; import java.util.Optional; import java.util.stream.Collectors; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.layout.GridDataFactory; import org.eclipse.jface.layout.RowLayoutFactory; import org.eclipse.swt.SWT; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Menu; import org.swtchart.ILineSeries; import org.swtchart.ILineSeries.PlotSymbolType; import org.swtchart.ISeries; import org.swtchart.ISeries.SeriesType; import name.abuchen.portfolio.model.AccountTransaction; import name.abuchen.portfolio.model.Client; import name.abuchen.portfolio.model.Portfolio; import name.abuchen.portfolio.model.PortfolioTransaction; import name.abuchen.portfolio.model.Security; import name.abuchen.portfolio.model.SecurityPrice; import name.abuchen.portfolio.model.Transaction.Unit; import name.abuchen.portfolio.money.CurrencyConverter; import name.abuchen.portfolio.money.Values; import name.abuchen.portfolio.ui.Images; import name.abuchen.portfolio.ui.Messages; import name.abuchen.portfolio.ui.PortfolioPlugin; import name.abuchen.portfolio.ui.util.SimpleAction; import name.abuchen.portfolio.ui.util.chart.TimelineChart; /** * Chart of historical quotes for a given security */ public class SecuritiesChart { private enum ChartDetails { INVESTMENT(Messages.LabelChartDetailInvestments), // EVENTS(Messages.LabelChartDetailEvents), // DIVIDENDS(Messages.LabelChartDetailDividends); private final String label; private ChartDetails(String label) { this.label = label; } @Override public String toString() { return label; } } private static final String PREF_KEY = "security-chart-details"; //$NON-NLS-1$ private Menu contextMenu; private Client client; private CurrencyConverter converter; private Security security; private TimelineChart chart; private LocalDate chartPeriod = LocalDate.now().minusYears(2); private EnumSet<ChartDetails> chartConfig = EnumSet.of(ChartDetails.INVESTMENT, ChartDetails.EVENTS); public SecuritiesChart(Composite parent, Client client, CurrencyConverter converter) { this.client = client; this.converter = converter; readChartConfig(client); chart = new TimelineChart(parent); chart.getTitle().setText("..."); //$NON-NLS-1$ chart.getToolTip().setValueFormat(new DecimalFormat(Values.Quote.pattern())); GridDataFactory.fillDefaults().grab(true, true).applyTo(chart); Composite buttons = new Composite(parent, SWT.NONE); buttons.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE)); GridDataFactory.fillDefaults().grab(false, true).applyTo(buttons); RowLayoutFactory.fillDefaults().type(SWT.VERTICAL).spacing(2).fill(true).applyTo(buttons); addConfigButton(buttons); addButton(buttons, Messages.SecurityTabChart1M, Period.ofMonths(1)); addButton(buttons, Messages.SecurityTabChart2M, Period.ofMonths(2)); addButton(buttons, Messages.SecurityTabChart6M, Period.ofMonths(6)); addButton(buttons, Messages.SecurityTabChart1Y, Period.ofYears(1)); addButton(buttons, Messages.SecurityTabChart2Y, Period.ofYears(2)); addButton(buttons, Messages.SecurityTabChart3Y, Period.ofYears(3)); addButton(buttons, Messages.SecurityTabChart5Y, Period.ofYears(5)); addButton(buttons, Messages.SecurityTabChart10Y, Period.ofYears(10)); Button button = new Button(buttons, SWT.FLAT); button.setText(Messages.SecurityTabChartAll); button.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { chartPeriod = null; updateChart(); } }); } private final void readChartConfig(Client client) { String pref = client.getProperty(PREF_KEY); if (pref == null) return; chartConfig.clear(); for (String key : pref.split(",")) //$NON-NLS-1$ { try { chartConfig.add(ChartDetails.valueOf(key)); } catch (IllegalArgumentException e) { PortfolioPlugin.log(e); } } } private void addConfigButton(Composite buttons) { Button b = new Button(buttons, SWT.FLAT); b.setImage(Images.CONFIG.image()); b.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (contextMenu == null) { MenuManager menuMgr = new MenuManager("#PopupMenu"); //$NON-NLS-1$ menuMgr.setRemoveAllWhenShown(true); menuMgr.addMenuListener(SecuritiesChart.this::chartConfigAboutToShow); contextMenu = menuMgr.createContextMenu(buttons.getShell()); buttons.addDisposeListener(event -> contextMenu.dispose()); } contextMenu.setVisible(true); } }); } private void chartConfigAboutToShow(IMenuManager manager) { for (ChartDetails detail : ChartDetails.values()) { Action action = new SimpleAction(detail.toString(), a -> { boolean isActive = chartConfig.contains(detail); if (isActive) chartConfig.remove(detail); else chartConfig.add(detail); client.setProperty(PREF_KEY, String.join(",", //$NON-NLS-1$ chartConfig.stream().map(ChartDetails::name).collect(Collectors.toList()))); updateChart(); }); action.setChecked(chartConfig.contains(detail)); manager.add(action); } } private void addButton(Composite buttons, String label, TemporalAmount amountToAdd) { Button b = new Button(buttons, SWT.FLAT); b.setText(label); b.addSelectionListener(new ChartPeriodSelectionListener() { @Override protected LocalDate startAt() { return LocalDate.now().minus(amountToAdd); } }); } /** * ChartPeriodSelectionListener handles the selection of the time which * should be displayed in the chart */ private abstract class ChartPeriodSelectionListener implements SelectionListener { @Override public void widgetSelected(SelectionEvent e) { chartPeriod = startAt(); updateChart(); } protected abstract LocalDate startAt(); @Override public void widgetDefaultSelected(SelectionEvent e) { // not used } } public void updateChart(Security security) { this.security = security; updateChart(); } private void updateChart() { chart.setRedraw(false); try { ISeries series = chart.getSeriesSet().getSeries(Messages.ColumnQuote); if (series != null) { chart.getSeriesSet().deleteSeries(Messages.ColumnQuote); } chart.clearMarkerLines(); if (security == null || security.getPrices().isEmpty()) { chart.getTitle().setText(security == null ? "..." : security.getName()); //$NON-NLS-1$ chart.redraw(); return; } chart.getTitle().setText(security.getName()); List<SecurityPrice> prices = security.getPricesIncludingLatest(); int index; LocalDate[] dates; double[] values; if (chartPeriod == null) { index = 0; dates = new LocalDate[prices.size()]; values = new double[prices.size()]; } else { index = Math.abs(Collections.binarySearch(prices, new SecurityPrice(chartPeriod, 0), new SecurityPrice.ByDate())); if (index >= prices.size()) { // no data available chart.redraw(); return; } dates = new LocalDate[prices.size() - index]; values = new double[prices.size() - index]; } for (int ii = 0; index < prices.size(); index++, ii++) { SecurityPrice p = prices.get(index); dates[ii] = p.getTime(); values[ii] = p.getValue() / Values.Quote.divider(); } ILineSeries lineSeries = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, Messages.ColumnQuote); lineSeries.setXDateSeries(TimelineChart.toJavaUtilDate(dates)); lineSeries.setLineWidth(2); lineSeries.enableArea(true); lineSeries.setSymbolType(PlotSymbolType.NONE); lineSeries.setYSeries(values); lineSeries.setAntialias(SWT.ON); chart.adjustRange(); addChartMarker(); } finally { chart.setRedraw(true); chart.redraw(); } } private void addChartMarker() { if (chartConfig.contains(ChartDetails.INVESTMENT)) addInvestmentMarkerLines(); if (chartConfig.contains(ChartDetails.DIVIDENDS)) addDividendMarkerLines(); if (chartConfig.contains(ChartDetails.EVENTS)) addEventMarkerLines(); } private void addInvestmentMarkerLines() { for (Portfolio portfolio : client.getPortfolios()) { for (PortfolioTransaction t : portfolio.getTransactions()) { if (t.getSecurity() == security && (chartPeriod == null || chartPeriod.isBefore(t.getDate()))) { String label = Values.Share.format(t.getType().isPurchase() ? t.getShares() : -t.getShares()); Color color = Display.getDefault().getSystemColor( t.getType().isPurchase() ? SWT.COLOR_DARK_GREEN : SWT.COLOR_DARK_RED); double value = t.getGrossPricePerShare(converter.with(t.getSecurity().getCurrencyCode())) .getAmount() / Values.Quote.divider(); chart.addMarkerLine(t.getDate(), color, label, value); } } } } private void addDividendMarkerLines() { client.getAccounts().stream().flatMap(a -> a.getTransactions().stream()) // .filter(t -> t.getType() == AccountTransaction.Type.DIVIDENDS) .filter(t -> t.getSecurity() == security) .filter(t -> chartPeriod == null || chartPeriod.isBefore(t.getDate())) // .forEach(t -> { Color color = Display.getDefault().getSystemColor(SWT.COLOR_DARK_MAGENTA); if (t.getShares() == 0L) { chart.addMarkerLine(t.getDate(), color, "\u2211 " + t.getGrossValue().toString()); //$NON-NLS-1$ } else { Optional<Unit> grossValue = t.getUnit(Unit.Type.GROSS_VALUE); long gross = grossValue.isPresent() ? grossValue.get().getForex().getAmount() : t.getGrossValueAmount(); long perShare = Math.round(gross * Values.Share.divider() * Values.Quote.factorToMoney() / t.getShares()); chart.addMarkerLine(t.getDate(), color, Values.Quote.format(perShare)); } }); } private void addEventMarkerLines() { security.getEvents().stream() // .filter(e -> chartPeriod == null || chartPeriod.isBefore(e.getDate())) // .forEach(e -> chart.addMarkerLine(e.getDate(), Display.getDefault().getSystemColor(SWT.COLOR_DARK_GRAY), e.getDetails())); } }