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()));
}
}