Optimization of markers and font in security chart
The dividend texts can be stacked up to three times.
As soon as an overlapping of the texts is possible, no text is written or is suspended until there is free space for the text again.

The respective offset value of the markers has been removed and adjusted by the respective font size and/or by the marker size.

The position of the text of the highest and lowest price has been swapped.

![grafik](https://user-images.githubusercontent.com/45203494/134807868-d0fdf7b7-9d11-4b58-9ede-395a0f0b071f.png)
![grafik](https://user-images.githubusercontent.com/45203494/134807752-333bc187-4a5c-400b-b42f-8909e7c4e22a.png)
![grafik](https://user-images.githubusercontent.com/45203494/134807797-88104fe7-5b8d-49a0-a498-f0445008d5ab.png)
![grafik](https://user-images.githubusercontent.com/45203494/134807815-adee7f32-dd2c-4763-8334-d62e819dab52.png)

name.abuchen.portfolio.ui/src/name/abuchen/portfolio/ui/views/SecuritiesChart.java
package name.abuchen.portfolio.ui.views;

import java.text.DecimalFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
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.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.swtchart.IAxis;
import org.swtchart.ILegend;
import org.swtchart.ILineSeries;
import org.swtchart.ILineSeries.PlotSymbolType;
import org.swtchart.ISeries;
import org.swtchart.ISeries.SeriesType;
import org.swtchart.LineStyle;
import org.swtchart.Range;

import com.google.common.primitives.Doubles;
import com.ibm.icu.text.MessageFormat;

import name.abuchen.portfolio.model.AccountTransaction;
import name.abuchen.portfolio.model.AttributeType;
import name.abuchen.portfolio.model.Client;
import name.abuchen.portfolio.model.LimitPrice;
import name.abuchen.portfolio.model.PortfolioTransaction;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.model.SecurityEvent;
import name.abuchen.portfolio.model.SecurityPrice;
import name.abuchen.portfolio.model.Transaction;
import name.abuchen.portfolio.model.Transaction.Unit;
import name.abuchen.portfolio.model.TransactionPair;
import name.abuchen.portfolio.money.CurrencyConverter;
import name.abuchen.portfolio.money.Money;
import name.abuchen.portfolio.money.Values;
import name.abuchen.portfolio.snapshot.ClientSnapshot;
import name.abuchen.portfolio.snapshot.filter.ClientSecurityFilter;
import name.abuchen.portfolio.snapshot.filter.ReadOnlyClient;
import name.abuchen.portfolio.snapshot.security.SecurityPerformanceIndicator;
import name.abuchen.portfolio.snapshot.security.SecurityPerformanceSnapshot;
import name.abuchen.portfolio.ui.Images;
import name.abuchen.portfolio.ui.Messages;
import name.abuchen.portfolio.ui.util.Colors;
import name.abuchen.portfolio.ui.util.DropDown;
import name.abuchen.portfolio.ui.util.SimpleAction;
import name.abuchen.portfolio.ui.util.chart.TimelineChart;
import name.abuchen.portfolio.ui.util.chart.TimelineChartToolTip;
import name.abuchen.portfolio.util.FormatHelper;
import name.abuchen.portfolio.util.Interval;
import name.abuchen.portfolio.util.TradeCalendar;
import name.abuchen.portfolio.util.TradeCalendarManager;

/**
 * Chart of historical quotes for a given security
 */
public class SecuritiesChart
{
    /**
     * A <em>closed</em> interval that includes start and end date.
     * <p/>
     * We create a separate {@code ChartInterval} class because - for historical
     * reasons - the {@link Interval} is half-open, i.e. it does not include the
     * start date. When working with charts, however, it is easier to work with
     * a closed interval that includes both start and end date.
     */
    public static class ChartInterval
    {
        private final LocalDate start;
        private final LocalDate end;

        public ChartInterval(LocalDate start, LocalDate end)
        {
            this.start = start;
            this.end = end;
        }

        public LocalDate getStart()
        {
            return start;
        }

        public LocalDate getEnd()
        {
            return end;
        }

        public boolean contains(LocalDate other)
        {
            return !other.isBefore(start) && !other.isAfter(end);
        }

        public boolean contains(LocalDateTime other)
        {
            return contains(other.toLocalDate());
        }
    }

    public enum IntervalOption
    {
        M1(Messages.SecurityTabChart1M, Messages.SecurityTabChart1MToolTip), //
        M2(Messages.SecurityTabChart2M, Messages.SecurityTabChart2MToolTip), //
        M6(Messages.SecurityTabChart6M, Messages.SecurityTabChart6MToolTip), //
        Y1(Messages.SecurityTabChart1Y, Messages.SecurityTabChart1YToolTip), //
        Y2(Messages.SecurityTabChart2Y, Messages.SecurityTabChart2YToolTip), //
        Y3(Messages.SecurityTabChart3Y, Messages.SecurityTabChart3YToolTip), //
        Y5(Messages.SecurityTabChart5Y, Messages.SecurityTabChart5YToolTip), //
        Y10(Messages.SecurityTabChart10Y, Messages.SecurityTabChart10YToolTip), //
        YTD(Messages.SecurityTabChartYTD, Messages.SecurityTabChartYTDToolTip), //
        H(Messages.SecurityTabChartHoldingPeriod, Messages.SecurityTabChartHoldingPeriodToolTip), //
        ALL(Messages.SecurityTabChartAll, Messages.SecurityTabChartAllToolTip);

        private final String label;
        private final String tooltip;

        private IntervalOption(String label, String tooltip)
        {
            this.label = label;
            this.tooltip = tooltip;
        }

        public String getLabel()
        {
            return label;
        }

        public String getTooltip()
        {
            return tooltip;
        }

        public ChartInterval getInverval(Client client, CurrencyConverter converter, Security security)
        {
            LocalDate now = LocalDate.now();

            switch (this)
            {
                case M1:
                    return new ChartInterval(now.minus(Period.ofMonths(1)), now);
                case M2:
                    return new ChartInterval(now.minus(Period.ofMonths(2)), now);
                case M6:
                    return new ChartInterval(now.minus(Period.ofMonths(6)), now);
                case Y1:
                    return new ChartInterval(now.minus(Period.ofYears(1)), now);
                case Y2:
                    return new ChartInterval(now.minus(Period.ofYears(2)), now);
                case Y3:
                    return new ChartInterval(now.minus(Period.ofYears(3)), now);
                case Y5:
                    return new ChartInterval(now.minus(Period.ofYears(5)), now);
                case Y10:
                    return new ChartInterval(now.minus(Period.ofYears(10)), now);
                case YTD:
                    return new ChartInterval(now.minus(Period.ofDays(now.getDayOfYear() - 1)), now);
                case H:
                    List<TransactionPair<?>> tx = security.getTransactions(client);
                    if (tx.isEmpty())
                        return new ChartInterval(now, now);

                    Collections.sort(tx, new TransactionPair.ByDate());
                    boolean hasHoldings = ClientSnapshot.create(client, converter, LocalDate.now())
                                    .getPositionsByVehicle().containsKey(security);

                    return new ChartInterval(tx.get(0).getTransaction().getDateTime().toLocalDate(), hasHoldings
                                    ? LocalDate.now()
                                    : tx.get(tx.size() - 1).getTransaction().getDateTime().toLocalDate());
                case ALL:
                    List<SecurityPrice> prices = security.getPricesIncludingLatest();

                    if (prices.isEmpty())
                        return new ChartInterval(now, now);
                    else
                        return new ChartInterval(prices.get(0).getDate(), prices.get(prices.size() - 1).getDate());

                default:
                    throw new IllegalArgumentException();
            }
        }
    }

    private enum ChartDetails
    {
        SCALING_LINEAR(Messages.LabelChartDetailChartScalingLinear), //
        SCALING_LOG(Messages.LabelChartDetailChartScalingLog), //
        CLOSING(Messages.LabelChartDetailChartDevelopmentClosing), //
        PURCHASEPRICE(Messages.LabelChartDetailChartDevelopmentClosingFIFO), //
        INVESTMENT(Messages.LabelChartDetailMarkerInvestments), //
        DIVIDENDS(Messages.LabelChartDetailMarkerDividends), //
        EVENTS(Messages.LabelChartDetailMarkerSplits), //
        EXTREMES(Messages.LabelChartDetailMarkerHighLow), //
        FIFOPURCHASE(Messages.LabelChartDetailMarkerPurchaseFIFO), //
        FLOATINGAVGPURCHASE(Messages.LabelChartDetailMarkerPurchaseMovingAverage), //
        BOLLINGERBANDS(Messages.LabelChartDetailIndicatorBollingerBands), //
        SMA_5DAYS(Messages.LabelChartDetailMovingAverage_5days), //
        SMA_20DAYS(Messages.LabelChartDetailMovingAverage_20days), //
        SMA_30DAYS(Messages.LabelChartDetailMovingAverage_30days), //
        SMA_38DAYS(Messages.LabelChartDetailMovingAverage_38days), //
        SMA_50DAYS(Messages.LabelChartDetailMovingAverage_50days), //
        SMA_90DAYS(Messages.LabelChartDetailMovingAverage_90days), //
        SMA_100DAYS(Messages.LabelChartDetailMovingAverage_100days), //
        SMA_200DAYS(Messages.LabelChartDetailMovingAverage_200days), //
        EMA_5DAYS(Messages.LabelChartDetailMovingAverage_5days), //
        EMA_20DAYS(Messages.LabelChartDetailMovingAverage_20days), //
        EMA_30DAYS(Messages.LabelChartDetailMovingAverage_30days), //
        EMA_38DAYS(Messages.LabelChartDetailMovingAverage_38days), //
        EMA_50DAYS(Messages.LabelChartDetailMovingAverage_50days), //
        EMA_90DAYS(Messages.LabelChartDetailMovingAverage_90days), //
        EMA_100DAYS(Messages.LabelChartDetailMovingAverage_100days), //
        EMA_200DAYS(Messages.LabelChartDetailMovingAverage_200days), //
        SHOW_MARKER_LINES(Messages.LabelChartDetailSettingsShowMarkerLines), //
        SHOW_DATA_LABELS(Messages.LabelChartDetailSettingsShowDataLabel), //
        SHOW_MISSING_TRADING_DAYS(Messages.LabelChartDetailSettingsShowMissingTradingDays), //
        SHOW_LIMITS(Messages.LabelChartDetailSettingsShowLimits);

        private final String label;

        private ChartDetails(String label)
        {
            this.label = label;
        }

        @Override
        public String toString()
        {
            return label;
        }
    }

    /* testing */ static class ChartRange
    {
        public final int start;
        public final int size;

        public final LocalDate startDate;
        public final LocalDate endDate;

        public ChartRange(int start, int end, LocalDate startDate, LocalDate endDate)
        {
            this.start = start;
            this.size = end - start;
            this.startDate = startDate;
            this.endDate = endDate;
        }

        /**
         * Maps the given {@link ChartInterval} to a range in the list of
         * security prices. Returns null if the interval does not intersect with
         * the list of prices.
         */
        public static ChartRange createFor(List<SecurityPrice> prices, ChartInterval chartInterval)
        {
            int start = Collections.binarySearch(prices, new SecurityPrice(chartInterval.getStart(), 0),
                            new SecurityPrice.ByDate());

            if (start < 0)
                start = -start - 1;

            if (start >= prices.size())
                return null;

            int end = Collections.binarySearch(prices, new SecurityPrice(chartInterval.getEnd(), 0),
                            new SecurityPrice.ByDate());

            if (end < 0)
                end = -end - 1;
            else
                end += 1; // include the entry that has been found

            if (end <= start)
                return null;

            return new ChartRange(start, end, prices.get(start).getDate(),
                            prices.get(Math.min(end, prices.size() - 1)).getDate());
        }
    }

    private Color colorQuote;

    private static final Color colorEventPurchase = Colors.getColor(26, 173, 33);
    private static final Color colorEventSale = Colors.getColor(232, 51, 69);
    private static final Color colorEventDividend = Colors.getColor(128, 0, 128);

    private static final Color colorHigh = Colors.getColor(0, 102, 0);
    private static final Color colorLow = Colors.getColor(128, 0, 0);

    private static final Color colorFifoPurchasePrice = Colors.getColor(226, 122, 121);
    private static final Color colorMovingAveragePurchasePrice = Colors.getColor(150, 82, 81);
    private static final Color colorBollingerBands = Colors.getColor(201, 141, 68);
    private static final Color colorSMA1 = Colors.getColor(179, 107, 107); // #B36B6B
    private static final Color colorSMA2 = Colors.getColor(179, 167, 107); // #B3A76B
    private static final Color colorSMA3 = Colors.getColor(131, 179, 107); // #83B36B
    private static final Color colorSMA4 = Colors.getColor(107, 179, 143); // #6BB38F
    private static final Color colorSMA5 = Colors.getColor(107, 155, 179); // #6B9BB3
    private static final Color colorSMA6 = Colors.getColor(119, 107, 179); // #776BB3
    private static final Color colorSMA7 = Colors.getColor(179, 107, 179); // #B36BB3

    private static final Color colorEMA1 = Colors.getColor(200, 107, 107); // #C86B6B
    private static final Color colorEMA2 = Colors.getColor(200, 167, 107); // #C8A76B
    private static final Color colorEMA3 = Colors.getColor(131, 200, 107); // #83C86B
    private static final Color colorEMA4 = Colors.getColor(107, 200, 143); // #6BC88F
    private static final Color colorEMA5 = Colors.getColor(107, 155, 200); // #6B9BC8
    private static final Color colorEMA6 = Colors.getColor(119, 107, 200); // #776BC8
    private static final Color colorEMA7 = Colors.getColor(200, 107, 200); // #C86BB3

    private static final Color colorAreaPositive = Colors.getColor(90, 114, 226);
    private static final Color colorAreaNegative = Colors.getColor(226, 91, 90);

    private static final Color colorNonTradingDay = Colors.getColor(255, 137, 89);

    private static final String PREF_KEY = "security-chart-details"; //$NON-NLS-1$

    private DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("d LLL"); //$NON-NLS-1$

    private Composite container;

    private Client client;
    private CurrencyConverter converter;
    private Security security;

    private TimelineChart chart;

    /**
     * Calculates dynamically for each security the interval of security prices
     * to be shown.
     */
    private IntervalOption intervalOption = IntervalOption.Y2;

    private EnumSet<ChartDetails> chartConfig = EnumSet.of(ChartDetails.INVESTMENT, ChartDetails.EVENTS,
                    ChartDetails.SCALING_LINEAR);

    private List<PaintListener> customPaintListeners = new ArrayList<>();
    private List<PaintListener> customBehindPaintListener = new ArrayList<>();
    private List<Transaction> customTooltipEvents = new ArrayList<>();

    private int swtAntialias = SWT.ON;

    public SecuritiesChart(Composite parent, Client client, CurrencyConverter converter)
    {
        this.client = client;
        this.converter = converter;

        readChartConfig(client);

        container = new Composite(parent, SWT.NONE);
        container.setLayout(new FillLayout());

        chart = new TimelineChart(container);
        chart.getTitle().setVisible(false);

        chart.getPlotArea().addPaintListener(event -> customPaintListeners.forEach(l -> l.paintControl(event)));
        chart.getPlotArea().addPaintListener(event -> customBehindPaintListener.forEach(l -> l.paintControl(event)));

        setupTooltip();

        ILegend legend = chart.getLegend();
        legend.setPosition(SWT.BOTTOM);
        legend.setVisible(true);
    }

    public IntervalOption getIntervalOption()
    {
        return intervalOption;
    }

    public void setIntervalOption(IntervalOption intervalOption)
    {
        this.intervalOption = intervalOption;
    }

    public void setQuoteColor(Color color)
    {
        this.colorQuote = color;
    }

    private void setupTooltip()
    {
        TimelineChartToolTip toolTip = chart.getToolTip();

        toolTip.showToolTipOnlyForDatesInDataSeries(Messages.ColumnQuote);

        toolTip.setDefaultValueFormat(new DecimalFormat(Values.Quote.pattern()));
        toolTip.addSeriesExclude(Messages.LabelChartDetailChartDevelopment + "Positive"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.LabelChartDetailChartDevelopment + "Negative"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.LabelChartDetailChartDevelopment + "Zero"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.SecurityMenuBuy);
        toolTip.addSeriesExclude(Messages.SecurityMenuBuy + "1"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.SecurityMenuBuy + "2"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.SecurityMenuSell);
        toolTip.addSeriesExclude(Messages.SecurityMenuSell + "1"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.SecurityMenuSell + "2"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.LabelChartDetailMarkerDividends);
        toolTip.addSeriesExclude(Messages.LabelChartDetailMarkerDividends + "1"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.LabelChartDetailMarkerDividends + "2"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.LabelChartDetailIndicatorBollingerBands);

        int precision = FormatHelper.getCalculatedQuoteDisplayPrecision();
        DecimalFormat calculatedFormat = new DecimalFormat(Values.CalculatedQuote.pattern());
        calculatedFormat.setMinimumFractionDigits(precision);
        calculatedFormat.setMaximumFractionDigits(precision);
        for (String period : new String[] { Messages.LabelChartDetailMovingAverage_5days,
                        Messages.LabelChartDetailMovingAverage_20days, Messages.LabelChartDetailMovingAverage_30days,
                        Messages.LabelChartDetailMovingAverage_38days, Messages.LabelChartDetailMovingAverage_50days,
                        Messages.LabelChartDetailMovingAverage_100days,
                        Messages.LabelChartDetailMovingAverage_200days, })
        {
            toolTip.overrideValueFormat(String.format("%s (%s)", Messages.LabelChartDetailMovingAverageEMA, period), //$NON-NLS-1$
                            calculatedFormat);
            toolTip.overrideValueFormat(String.format("%s (%s)", Messages.LabelChartDetailMovingAverageSMA, period), //$NON-NLS-1$
                            calculatedFormat);
        }
        toolTip.overrideValueFormat(Messages.LabelChartDetailIndicatorBollingerBandsLower, calculatedFormat);
        toolTip.overrideValueFormat(Messages.LabelChartDetailIndicatorBollingerBandsUpper, calculatedFormat);
        toolTip.overrideValueFormat(Messages.LabelChartDetailMarkerPurchaseFIFO, calculatedFormat);
        toolTip.overrideValueFormat(Messages.LabelChartDetailMarkerPurchaseMovingAverage, calculatedFormat);

        toolTip.addExtraInfo((composite, focus) -> {
            if (focus instanceof Date)
            {
                Instant instant = ((Date) focus).toInstant();
                ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
                LocalDate date = zdt.toLocalDate();

                Interval displayInterval = Interval.of(date.minusDays(5), date.plusDays(5));

                customTooltipEvents.stream() //
                                .filter(t -> displayInterval.contains(t.getDateTime())) //
                                .forEach(t -> {
                                    if (t instanceof AccountTransaction)
                                        addDividendTooltip(composite, (AccountTransaction) t);
                                    else if (t instanceof PortfolioTransaction)
                                        addInvestmentTooltip(composite, (PortfolioTransaction) t);
                                });
            }
        });
    }

    private void addInvestmentTooltip(Composite composite, PortfolioTransaction t)
    {
        Label label = new Label(composite, SWT.NONE);
        label.setText(MessageFormat.format(Messages.LabelToolTipTransactionSummary, t.getType().toString(),
                        dateTimeFormatter.format(t.getDateTime().toLocalDate()), t.getMonetaryAmount().toString()));

        label = new Label(composite, SWT.NONE);
        label.setText(MessageFormat.format(Messages.LabelToolTipInvestmentDetails, Values.Share.format(t.getShares()),
                        Values.Quote.format(
                                        t.getGrossPricePerShare(converter.with(t.getSecurity().getCurrencyCode())))));
    }

    private void addDividendTooltip(Composite composite, AccountTransaction t)
    {
        Label label = new Label(composite, SWT.NONE);
        String amount = t.getMonetaryAmount().toString();
        label.setText(MessageFormat.format(Messages.LabelToolTipTransactionSummary, t.getType().toString(),
                        dateTimeFormatter.format(t.getDateTime().toLocalDate()), amount));

        if (t.getShares() == 0L)
        {
            label = new Label(composite, SWT.NONE);
            label.setText("\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();
            String currency = grossValue.isPresent() ? grossValue.get().getForex().getCurrencyCode()
                            : t.getCurrencyCode();

            // gross value in either forex currency or transaction currency
            String grossAmount = Money.of(currency, gross).toString();

            // gross value in transaction currency
            String grossValueAmount = Money.of(t.getCurrencyCode(), t.getGrossValueAmount()).toString();

            // display gross value in transaction currency, different gross
            // value in security currency exists
            if (!grossValueAmount.equals(grossAmount))
            {
                label = new Label(composite, SWT.NONE);
                label.setText(MessageFormat.format(Messages.LabelToolTipDividendDetailsGross, grossValueAmount));
            }

            // display gross value, if different to net amount
            if (!grossAmount.equals(amount))
            {
                label = new Label(composite, SWT.NONE);
                label.setText(MessageFormat.format(Messages.LabelToolTipDividendDetailsGross, grossAmount));
            }

            // display dividend per share in security currency
            label = new Label(composite, SWT.NONE);
            label.setText(MessageFormat.format(Messages.LabelToolTipDividendDetails, Values.Share.format(t.getShares()),
                            currency, Values.Quote.format(Math.round(gross * Values.Share.divider()
                                            * Values.Quote.factorToMoney() / t.getShares()))));

        }
    }

    private void configureSeriesPainter(ILineSeries series, Date[] dates, double[] values, Color color, int lineWidth,
                    LineStyle lineStyle, boolean enableArea, boolean visibleInLegend)
    {
        if (lineWidth != 0)
            series.setLineWidth(lineWidth);
        series.setLineStyle(lineStyle);
        series.setXDateSeries(dates);
        series.enableArea(enableArea);
        series.setYSeries(values);
        series.setAntialias(swtAntialias);

        if (color != null)
            series.setLineColor(color);
        series.setVisibleInLegend(visibleInLegend);
    }

    private final void readChartConfig(Client client)
    {
        String pref = ReadOnlyClient.unwrap(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 ignore)
            {
                // do not print exception to the log as it confuses users. The
                // old SMA200 label has been renamed, nothing we can change
                // anymore
            }
        }
    }

    public void addButtons(ToolBarManager toolBar)
    {
        List<Action> viewActions = new ArrayList<>();

        for (IntervalOption option : IntervalOption.values())
        {
            SimpleAction action = new SimpleAction(option.getLabel(), IAction.AS_CHECK_BOX, option.getTooltip(), a -> {
                this.intervalOption = option;
                updateChart();
                for (Action viewAction : viewActions)
                    viewAction.setChecked(a.equals(viewAction));
            });
            if (intervalOption == option)
                action.setChecked(true);
            viewActions.add(action);
            toolBar.add(action);
        }

        toolBar.add(new DropDown(Messages.MenuConfigureChart, Images.CONFIG, SWT.NONE, this::chartConfigAboutToShow));
    }

    private void chartConfigAboutToShow(IMenuManager manager)
    {
        MenuManager subMenuChartScaling = new MenuManager(Messages.LabelChartDetailChartScaling, null);
        MenuManager subMenuChartDevelopment = new MenuManager(Messages.LabelChartDetailChartDevelopment, null);
        MenuManager subMenuChartMarker = new MenuManager(Messages.LabelChartDetailMarker, null);
        MenuManager subMenuChartIndicator = new MenuManager(Messages.LabelChartDetailIndicator, null);
        MenuManager subMenuChartMovingAverage = new MenuManager(Messages.LabelChartDetailMovingAverage, null);
        MenuManager subMenuChartMovingAverageSMA = new MenuManager(Messages.LabelChartDetailMovingAverageSMA, null);
        MenuManager subMenuChartMovingAverageEMA = new MenuManager(Messages.LabelChartDetailMovingAverageEMA, null);
        MenuManager subMenuChartSettings = new MenuManager(Messages.LabelChartDetailSettings, null);

        subMenuChartScaling.add(addMenuAction(ChartDetails.SCALING_LINEAR));
        subMenuChartScaling.add(addMenuAction(ChartDetails.SCALING_LOG));
        subMenuChartDevelopment.add(addMenuAction(ChartDetails.CLOSING));
        subMenuChartDevelopment.add(addMenuAction(ChartDetails.PURCHASEPRICE));
        subMenuChartMarker.add(addMenuAction(ChartDetails.INVESTMENT));
        subMenuChartMarker.add(addMenuAction(ChartDetails.DIVIDENDS));
        subMenuChartMarker.add(addMenuAction(ChartDetails.EVENTS));
        subMenuChartMarker.add(addMenuAction(ChartDetails.EXTREMES));
        subMenuChartMarker.add(addMenuAction(ChartDetails.FIFOPURCHASE));
        subMenuChartMarker.add(addMenuAction(ChartDetails.FLOATINGAVGPURCHASE));
        subMenuChartMarker.add(addMenuAction(ChartDetails.SHOW_LIMITS));
        subMenuChartIndicator.add(addMenuAction(ChartDetails.BOLLINGERBANDS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_5DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_20DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_30DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_38DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_50DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_90DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_100DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_200DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_5DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_20DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_30DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_38DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_50DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_90DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_100DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_200DAYS));
        subMenuChartSettings.add(addMenuAction(ChartDetails.SHOW_MARKER_LINES));
        subMenuChartSettings.add(addMenuAction(ChartDetails.SHOW_DATA_LABELS));
        subMenuChartSettings.add(addMenuAction(ChartDetails.SHOW_MISSING_TRADING_DAYS));
        manager.add(subMenuChartScaling);
        manager.add(subMenuChartDevelopment);
        manager.add(subMenuChartMarker);
        manager.add(subMenuChartIndicator);
        manager.add(subMenuChartMovingAverage);
        subMenuChartMovingAverage.add(subMenuChartMovingAverageSMA);
        subMenuChartMovingAverage.add(subMenuChartMovingAverageEMA);
        manager.add(subMenuChartSettings);
    }

    private Action addMenuAction(ChartDetails detail)
    {
        Action action = new SimpleAction(detail.toString(), a -> {
            boolean isActive = chartConfig.contains(detail);

            if (isActive)
                chartConfig.remove(detail);
            else
                chartConfig.add(detail);

            if (!isActive)
            {
                switch (detail)
                {
                    case SCALING_LINEAR:
                        chartConfig.remove(ChartDetails.SCALING_LOG);
                        break;
                    case SCALING_LOG:
                        chartConfig.remove(ChartDetails.SCALING_LINEAR);
                        chartConfig.remove(ChartDetails.PURCHASEPRICE);
                        chartConfig.remove(ChartDetails.CLOSING);
                        break;
                    case CLOSING:
                        chartConfig.remove(ChartDetails.PURCHASEPRICE);
                        chartConfig.remove(ChartDetails.SCALING_LOG);
                        break;
                    case PURCHASEPRICE:
                        chartConfig.remove(ChartDetails.CLOSING);
                        chartConfig.remove(ChartDetails.SCALING_LOG);
                        break;
                    default:
                        break;
                }
            }

            if (!chartConfig.contains(ChartDetails.SCALING_LINEAR) && !chartConfig.contains(ChartDetails.SCALING_LOG))
                chartConfig.add(ChartDetails.SCALING_LINEAR);

            ReadOnlyClient.unwrap(client).setProperty(PREF_KEY, String.join(",", //$NON-NLS-1$
                            chartConfig.stream().map(ChartDetails::name).collect(Collectors.toList())));

            updateChart();

        });

        action.setChecked(chartConfig.contains(detail));
        return action;
    }

    public void updateChart(Client client, Security security)
    {
        this.client = client;
        this.security = security;
        updateChart();
    }

    public Control getControl()
    {
        return container;
    }

    private void updateChart()
    {
        chart.setRedraw(false);

        try
        {
            // delete all line series (quotes + possibly moving average)
            ISeries[] series = chart.getSeriesSet().getSeries();
            for (ISeries s : series)
                chart.getSeriesSet().deleteSeries(s.getId());

            chart.clearMarkerLines();
            chart.clearNonTradingDayMarker();
            customPaintListeners.clear();
            customBehindPaintListener.clear();
            customTooltipEvents.clear();

            if (security == null || security.getPrices().isEmpty())
            {
                chart.redraw();
                return;
            }

            boolean showAreaRelativeToFirstQuote = chartConfig.contains(ChartDetails.CLOSING)
                            || chartConfig.contains(ChartDetails.PURCHASEPRICE);

            // determine the interval to be shown in the chart

            ChartInterval chartInterval = intervalOption.getInverval(client, converter, security);

            // determine index range for given interval in prices list

            List<SecurityPrice> prices = security.getPricesIncludingLatest();

            ChartRange range = ChartRange.createFor(prices, chartInterval);
            if (range == null)
            {
                chart.redraw();
                return;
            }

            // prepare value arrays

            LocalDate[] dates = new LocalDate[range.size];

            double[] values = new double[range.size];
            double[] valuesRelative = new double[range.size];
            double[] valuesRelativePositive = new double[range.size];
            double[] valuesRelativeNegative = new double[range.size];
            double[] valuesZeroLine = new double[range.size];
            double firstQuote = 0;

            // Disable SWT antialias for more than 1000 records due to SWT
            // performance issue in Drawing
            swtAntialias = range.size > 1000 ? SWT.OFF : SWT.ON;

            if (!chartConfig.contains(ChartDetails.PURCHASEPRICE))
            {
                SecurityPrice p2 = prices.get(range.start);
                firstQuote = (p2.getValue() / Values.Quote.divider());
            }
            else
            {
                Optional<Double> purchasePrice = getLatestPurchasePrice();

                if (purchasePrice.isPresent())
                    firstQuote = purchasePrice.get();
                else
                    showAreaRelativeToFirstQuote = false;
            }

            addChartMarkerBackground(chartInterval, range);

            for (int ii = 0; ii < range.size; ii++)
            {
                SecurityPrice p = prices.get(ii + range.start);
                dates[ii] = p.getDate();
                values[ii] = p.getValue() / Values.Quote.divider();
                if (showAreaRelativeToFirstQuote)
                {
                    valuesRelative[ii] = (p.getValue() / Values.Quote.divider()) - firstQuote;
                    valuesZeroLine[ii] = 0;
                    if (valuesRelative[ii] >= 0)
                    {
                        valuesRelativePositive[ii] = valuesRelative[ii];
                        valuesRelativeNegative[ii] = 0;
                    }
                    else
                    {
                        valuesRelativePositive[ii] = 0;
                        valuesRelativeNegative[ii] = valuesRelative[ii];
                    }
                }
            }

            Date[] javaDates = TimelineChart.toJavaUtilDate(dates);

            if (showAreaRelativeToFirstQuote)
            {

                ILineSeries lineSeries2ndNegative = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                                Messages.LabelChartDetailChartDevelopmentClosing + "Negative"); //$NON-NLS-1$
                lineSeries2ndNegative.setSymbolType(PlotSymbolType.NONE);
                lineSeries2ndNegative.setYAxisId(1);
                configureSeriesPainter(lineSeries2ndNegative, javaDates, valuesRelativeNegative, colorAreaNegative, 1,
                                LineStyle.SOLID, true, false);

                ILineSeries lineSeries2ndPositive = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                                Messages.LabelChartDetailChartDevelopmentClosing + "Positive"); //$NON-NLS-1$
                lineSeries2ndPositive.setSymbolType(PlotSymbolType.NONE);
                lineSeries2ndPositive.setYAxisId(1);
                configureSeriesPainter(lineSeries2ndPositive, javaDates, valuesRelativePositive, colorAreaPositive, 1,
                                LineStyle.SOLID, true, false);
            }

            ILineSeries lineSeries = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                            Messages.ColumnQuote);
            lineSeries.setSymbolType(PlotSymbolType.NONE);
            configureSeriesPainter(lineSeries, javaDates, values, colorQuote, 2, LineStyle.SOLID,
                            !showAreaRelativeToFirstQuote, false);

            chart.adjustRange();

            addChartMarkerForeground(chartInterval);

            chart.adjustRange();

            IAxis yAxis1st = chart.getAxisSet().getYAxis(0);
            IAxis yAxis2nd = chart.getAxisSet().getYAxis(1);
            yAxis2nd.setRange(
                            new Range(yAxis1st.getRange().lower - firstQuote, yAxis1st.getRange().upper - firstQuote));

            yAxis1st.enableLogScale(chartConfig.contains(ChartDetails.SCALING_LOG));
            yAxis2nd.enableLogScale(chartConfig.contains(ChartDetails.SCALING_LOG));

            yAxis1st.getTick().setVisible(true);

            if (chartConfig.contains(ChartDetails.SHOW_MISSING_TRADING_DAYS))
            {
                TradeCalendar tradeCalendar = TradeCalendarManager.getInstance(security);
                List<LocalDate> calendarDates = new ArrayList<>();
                for (LocalDate calendarDate = dates[0]; calendarDate
                                .isBefore(dates[dates.length - 1]); calendarDate = calendarDate.plusDays(1))
                    calendarDates.add(calendarDate);
                for (LocalDate pricingDate : dates)
                    calendarDates.remove(pricingDate);
                for (LocalDate targetDate : calendarDates)
                {
                    if (!tradeCalendar.isHoliday(targetDate))
                        chart.addNonTradingDayMarker(targetDate, colorNonTradingDay);
                }
            }
        }
        finally
        {
            chart.setRedraw(true);
            chart.redraw();
        }
    }

    private void addChartMarkerBackground(ChartInterval chartInterval, ChartRange range)
    {
        if (chartConfig.contains(ChartDetails.BOLLINGERBANDS))
            addBollingerBandsMarkerLines(chartInterval, 20, 2);

        if (chartConfig.contains(ChartDetails.SMA_5DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_5days, 5, colorSMA1);

        if (chartConfig.contains(ChartDetails.SMA_20DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_20days, 20, colorSMA2);

        if (chartConfig.contains(ChartDetails.SMA_30DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_30days, 30, colorSMA3);

        if (chartConfig.contains(ChartDetails.SMA_38DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_38days, 38, colorSMA4);

        if (chartConfig.contains(ChartDetails.SMA_50DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_50days, 50, colorSMA4);

        if (chartConfig.contains(ChartDetails.SMA_90DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_90days, 90, colorSMA5);

        if (chartConfig.contains(ChartDetails.SMA_100DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_100days, 100, colorSMA6);

        if (chartConfig.contains(ChartDetails.SMA_200DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_200days, 200, colorSMA7);

        if (chartConfig.contains(ChartDetails.EMA_5DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_5days, 5, colorEMA1);

        if (chartConfig.contains(ChartDetails.EMA_20DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_20days, 20, colorEMA2);

        if (chartConfig.contains(ChartDetails.EMA_30DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_30days, 30, colorEMA3);

        if (chartConfig.contains(ChartDetails.EMA_38DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_38days, 38, colorEMA4);

        if (chartConfig.contains(ChartDetails.EMA_50DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_50days, 50, colorEMA4);

        if (chartConfig.contains(ChartDetails.EMA_90DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_90days, 90, colorEMA5);

        if (chartConfig.contains(ChartDetails.EMA_100DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_100days, 100, colorEMA6);

        if (chartConfig.contains(ChartDetails.EMA_200DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_200days, 200, colorEMA7);

        if (chartConfig.contains(ChartDetails.SHOW_LIMITS))
            addLimitLines(chartInterval, range);
    }

    private void addChartMarkerForeground(ChartInterval chartInterval)
    {
        if (chartConfig.contains(ChartDetails.FIFOPURCHASE))
            addFIFOPurchasePrice(chartInterval);

        if (chartConfig.contains(ChartDetails.FLOATINGAVGPURCHASE))
            addMovingAveragePurchasePrice(chartInterval);

        if (chartConfig.contains(ChartDetails.INVESTMENT))
            addInvestmentMarkerLines(chartInterval);

        if (chartConfig.contains(ChartDetails.DIVIDENDS))
            addDividendMarkerLines(chartInterval);

        if (chartConfig.contains(ChartDetails.EVENTS))
            addEventMarkerLines(chartInterval);

        if (chartConfig.contains(ChartDetails.EXTREMES))
            addExtremesMarkerLines(chartInterval);
    }

    private void addLimitLines(ChartInterval chartInterval, ChartRange range)
    {
        this.security.getAttributes().getMap().forEach((key, val) -> {
            // null OR not Limit Price --> ignore
            if (val == null || val.getClass() != LimitPrice.class)
                return;

            LimitPrice limitAttribute = (LimitPrice) val;

            Optional<AttributeType> attributeName = client.getSettings().getAttributeTypes()
                            .filter(attr -> attr.getId().equals(key)).findFirst();
            // could not find name of limit attribute --> don't draw
            if (attributeName.isEmpty())
                return;

            String lineID = attributeName.get().getName() + " (" + limitAttribute.toString() + ")"; //$NON-NLS-1$ //$NON-NLS-2$

            // horizontal line: only two points required
            LocalDate[] dates = new LocalDate[2];
            dates[0] = range.startDate;
            dates[1] = range.endDate;

            // both points with same y-value
            double[] values = new double[2];
            values[0] = values[1] = limitAttribute.getValue() / Values.Quote.divider();

            ILineSeries lineSeriesLimit = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, lineID);
            lineSeriesLimit.setXDateSeries(TimelineChart.toJavaUtilDate(dates));
            lineSeriesLimit.setLineWidth(2);
            lineSeriesLimit.setLineStyle(LineStyle.DASH);
            lineSeriesLimit.enableArea(false);
            lineSeriesLimit.setSymbolType(PlotSymbolType.NONE);
            lineSeriesLimit.setYSeries(values);
            lineSeriesLimit.setAntialias(swtAntialias);
            lineSeriesLimit.setLineColor(Colors.ICON_ORANGE);
            lineSeriesLimit.setYAxisId(0);
            lineSeriesLimit.setVisibleInLegend(true);
        });
    }

    private void addSMAMarkerLines(ChartInterval chartInterval, String smaSeries, String smaDaysWording, int smaDays,
                    Color smaColor)
    {
        ChartLineSeriesAxes smaLines = new SimpleMovingAverage(smaDays, this.security, chartInterval).getSMA();
        if (smaLines == null || smaLines.getValues() == null || smaLines.getDates() == null)
            return;

        @SuppressWarnings("nls")
        String lineID = smaSeries + " (" + smaDaysWording + ")";

        ILineSeries lineSeriesSMA = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, lineID);
        lineSeriesSMA.setXDateSeries(smaLines.getDates());
        lineSeriesSMA.setLineWidth(2);
        lineSeriesSMA.enableArea(false);
        lineSeriesSMA.setSymbolType(PlotSymbolType.NONE);
        lineSeriesSMA.setYSeries(smaLines.getValues());
        lineSeriesSMA.setAntialias(swtAntialias);
        lineSeriesSMA.setLineColor(smaColor);
        lineSeriesSMA.setYAxisId(0);
        lineSeriesSMA.setVisibleInLegend(true);
    }

    private void addEMAMarkerLines(ChartInterval chartInterval, String emaSeries, String emaDaysWording, int emaDays,
                    Color emaColor)
    {
        ChartLineSeriesAxes emaLines = new ExponentialMovingAverage(emaDays, this.security, chartInterval).getEMA();
        if (emaLines == null || emaLines.getValues() == null || emaLines.getDates() == null)
            return;

        @SuppressWarnings("nls")
        String lineID = emaSeries + " (" + emaDaysWording + ")";

        ILineSeries lineSeriesEMA = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, lineID);
        lineSeriesEMA.setXDateSeries(emaLines.getDates());
        lineSeriesEMA.setLineWidth(2);
        lineSeriesEMA.enableArea(false);
        lineSeriesEMA.setSymbolType(PlotSymbolType.NONE);
        lineSeriesEMA.setYSeries(emaLines.getValues());
        lineSeriesEMA.setAntialias(swtAntialias);
        lineSeriesEMA.setLineColor(emaColor);
        lineSeriesEMA.setYAxisId(0);
        lineSeriesEMA.setVisibleInLegend(true);
    }

    private void addInvestmentMarkerLines(ChartInterval chartInterval)
    {
        List<PortfolioTransaction> purchase = client.getPortfolios().stream().flatMap(p -> p.getTransactions().stream())
                        .filter(t -> t.getSecurity() == security)
                        .filter(t -> t.getType() == PortfolioTransaction.Type.BUY
                                        || t.getType() == PortfolioTransaction.Type.DELIVERY_INBOUND)
                        .filter(t -> chartInterval.contains(t.getDateTime())) //
                        .sorted(new Transaction.ByDate()).collect(Collectors.toList());

        addInvestmentMarkers(purchase, Messages.SecurityMenuBuy, colorEventPurchase);

        List<PortfolioTransaction> sales = client.getPortfolios().stream().flatMap(p -> p.getTransactions().stream())
                        .filter(t -> t.getSecurity() == security)
                        .filter(t -> t.getType() == PortfolioTransaction.Type.SELL
                                        || t.getType() == PortfolioTransaction.Type.DELIVERY_OUTBOUND)
                        .filter(t -> chartInterval.contains(t.getDateTime())) //
                        .sorted(new Transaction.ByDate()).collect(Collectors.toList());

        addInvestmentMarkers(sales, Messages.SecurityMenuSell, colorEventSale);
    }

    private void addInvestmentMarkers(List<PortfolioTransaction> transactions, String seriesLabel, Color color)
    {
        if (transactions.isEmpty())
            return;

        customTooltipEvents.addAll(transactions);

        if (chartConfig.contains(ChartDetails.SHOW_MARKER_LINES))
        {
            transactions.forEach(t -> {
                String label = Values.Share.format(t.getType().isPurchase() ? t.getShares() : -t.getShares());
                double value = t.getGrossPricePerShare(converter.with(t.getSecurity().getCurrencyCode())).getAmount()
                                / Values.Quote.divider();
                chart.addMarkerLine(t.getDateTime().toLocalDate(), color, label, value);
            });
        }
        else
        {
            Date[] dates = transactions.stream().map(PortfolioTransaction::getDateTime)
                            .map(d -> Date.from(d.atZone(ZoneId.systemDefault()).toInstant()))
                            .collect(Collectors.toList()).toArray(new Date[0]);

            double[] values = transactions.stream().mapToDouble(
                            t -> t.getGrossPricePerShare(converter.with(t.getSecurity().getCurrencyCode())).getAmount()
                                            / Values.Quote.divider())
                            .toArray();

            ILineSeries border = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, seriesLabel + "2"); //$NON-NLS-1$
            border.setYAxisId(0);
            border.setSymbolColor(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
            border.setSymbolType(PlotSymbolType.DIAMOND);
            border.setSymbolSize(7);

            configureSeriesPainter(border, dates, values, null, 0, LineStyle.NONE, false, false);

            ILineSeries background = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                            seriesLabel + "1"); //$NON-NLS-1$
            background.setYAxisId(0);
            background.setSymbolType(PlotSymbolType.DIAMOND);
            background.setSymbolSize(6);
            background.setSymbolColor(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
            configureSeriesPainter(background, dates, values, null, 0, LineStyle.NONE, false, false);

            ILineSeries inner = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, seriesLabel);
            inner.setYAxisId(0);
            inner.setSymbolType(PlotSymbolType.DIAMOND);
            inner.setSymbolSize(4);
            inner.setSymbolColor(color);
            configureSeriesPainter(inner, dates, values, color, 0, LineStyle.NONE, false, true);

            if (chartConfig.contains(ChartDetails.SHOW_DATA_LABELS))
            {
                customPaintListeners.add(event -> {
                    IAxis xAxis = chart.getAxisSet().getXAxis(0);
                    IAxis yAxis = chart.getAxisSet().getYAxis(0);

                    for (int index = 0; index < dates.length; index++)
                    {
                        int x = xAxis.getPixelCoordinate(dates[index].getTime());
                        int y = yAxis.getPixelCoordinate(values[index]);

                        PortfolioTransaction t = transactions.get(index);
                        String label = Values.Share.format(t.getType().isPurchase() ? t.getShares() : -t.getShares());
                        Point textExtent = event.gc.textExtent(label);

                        event.gc.setForeground(Colors.theme().defaultForeground());
                        event.gc.drawText(label, x - (textExtent.x / 2), y + 10, true);
                    }
                });
            }
        }
    }

    private void addDividendMarkerLines(ChartInterval chartInterval)
    {
        List<AccountTransaction> dividends = client.getAccounts().stream().flatMap(a -> a.getTransactions().stream())
                        .filter(t -> t.getSecurity() == security)
                        .filter(t -> t.getType() == AccountTransaction.Type.DIVIDENDS)
                        .filter(t -> chartInterval.contains(t.getDateTime())).sorted(new Transaction.ByDate())
                        .collect(Collectors.toList());

        if (dividends.isEmpty())
            return;

        customTooltipEvents.addAll(dividends);

        if (chartConfig.contains(ChartDetails.SHOW_MARKER_LINES))
        {
            dividends.forEach(t -> chart.addMarkerLine(t.getDateTime().toLocalDate(), colorEventDividend,
                            getDividendLabel(t)));
        }
        else
        {
            Date[] dates = dividends.stream().map(AccountTransaction::getDateTime)
                            .map(d -> Date.from(d.atZone(ZoneId.systemDefault()).toInstant()))
                            .collect(Collectors.toList()).toArray(new Date[0]);

            IAxis yAxis1st = chart.getAxisSet().getYAxis(0);
            double yAxis1stAxisPrice = yAxis1st.getRange().lower;

            double[] values = new double[dates.length];
            Arrays.fill(values, yAxis1stAxisPrice);

            ILineSeries border = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                            Messages.LabelChartDetailMarkerDividends + "2"); //$NON-NLS-1$
            border.setYAxisId(0);
            border.setSymbolType(PlotSymbolType.SQUARE);
            border.setSymbolSize(6);
            border.setSymbolColor(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
            configureSeriesPainter(border, dates, values, null, 0, LineStyle.NONE, false, false);

            ILineSeries background = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                            Messages.LabelChartDetailMarkerDividends + "1"); //$NON-NLS-1$
            background.setYAxisId(0);
            background.setSymbolType(PlotSymbolType.SQUARE);
            background.setSymbolSize(5);
            background.setSymbolColor(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
            configureSeriesPainter(background, dates, values, null, 0, LineStyle.NONE, false, false);

            ILineSeries inner = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                            Messages.LabelChartDetailMarkerDividends);
            inner.setYAxisId(0);
            inner.setSymbolType(PlotSymbolType.SQUARE);
            inner.setSymbolSize(3);
            inner.setSymbolColor(colorEventDividend);
            configureSeriesPainter(inner, dates, values, null, 0, LineStyle.NONE, false, true);

            if (chartConfig.contains(ChartDetails.SHOW_DATA_LABELS))
            {
                customPaintListeners.add(event -> {
                    IAxis xAxis = chart.getAxisSet().getXAxis(0);
                    IAxis yAxis = chart.getAxisSet().getYAxis(0);

                    for (int index = 0; index < dates.length; index++)
                    {
                        int x = xAxis.getPixelCoordinate(dates[index].getTime());
                        int y = yAxis.getPixelCoordinate(values[index]);

                        String label = getDividendLabel(dividends.get(index));
                        Point textExtent = event.gc.textExtent(label);

                        event.gc.setForeground(Colors.theme().defaultForeground());
                        event.gc.drawText(label, x - (textExtent.x / 2), y - 22, true);
                    }
                });
            }
        }
    }

    private String getDividendLabel(AccountTransaction t)
    {
        if (t.getShares() == 0L)
        {
            return "\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());

            return Values.Quote.format(perShare);
        }
    }

    private void addEventMarkerLines(ChartInterval chartInterval)
    {
        security.getEvents().stream() //
                        .filter(e -> chartInterval.contains(e.getDate())) //
                        .filter(e -> e.getType() != SecurityEvent.Type.DIVIDEND_PAYMENT) //
                        .forEach(e -> chart.addMarkerLine(e.getDate(),
                                        Display.getDefault().getSystemColor(SWT.COLOR_DARK_GRAY), e.getDetails()));
    }

    private void addExtremesMarkerLines(ChartInterval chartInterval)
    {
        Optional<SecurityPrice> max = security.getPricesIncludingLatest().stream() //
                        .filter(p -> chartInterval.contains(p.getDate())) //
                        .max(Comparator.comparing(SecurityPrice::getValue));

        Optional<SecurityPrice> min = security.getPricesIncludingLatest().stream() //
                        .filter(p -> chartInterval.contains(p.getDate())) //
                        .min(Comparator.comparing(SecurityPrice::getValue));

        max.ifPresent(high -> addExtremeMarker(high, PlotSymbolType.TRIANGLE, 10, //
                        Messages.LabelChartDetailMarkerHigh, colorHigh));
        min.ifPresent(low -> addExtremeMarker(low, PlotSymbolType.INVERTED_TRIANGLE, -25,
                        Messages.LabelChartDetailMarkerLow, colorLow));
    }

    private void addExtremeMarker(SecurityPrice price, PlotSymbolType plotSymbolType, int labelOffset,
                    String seriesLabel, Color color)
    {
        LocalDate eventDate = price.getDate();
        String valueFormat = Values.Quote.format(price.getValue());
        double value = price.getValue() / Values.Quote.divider();

        if (chartConfig.contains(ChartDetails.SHOW_MARKER_LINES))
        {
            chart.addMarkerLine(eventDate, color, valueFormat);
        }
        else
        {
            Date zonedDate = Date.from(eventDate.atStartOfDay(ZoneId.systemDefault()).toInstant());

            ILineSeries inner = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, seriesLabel);
            inner.setYAxisId(0);
            inner.setSymbolType(plotSymbolType);
            inner.setSymbolSize(6);
            inner.setSymbolColor(color);
            configureSeriesPainter(inner, new Date[] { zonedDate }, new double[] { value }, color, 0, LineStyle.NONE,
                            false, true);

            if (chartConfig.contains(ChartDetails.SHOW_DATA_LABELS))
            {
                customPaintListeners.add(event -> {
                    IAxis xAxis = chart.getAxisSet().getXAxis(0);
                    IAxis yAxis = chart.getAxisSet().getYAxis(0);

                    int x = xAxis.getPixelCoordinate(zonedDate.getTime());
                    int y = yAxis.getPixelCoordinate(value);
                    Point textExtent = event.gc.textExtent(valueFormat);

                    event.gc.setForeground(Colors.theme().defaultForeground());
                    event.gc.drawText(valueFormat, x - (textExtent.x / 2), y + labelOffset, true);
                });
            }
        }
    }

    private void addBollingerBandsMarkerLines(ChartInterval chartInterval, int bollingerBandsDays,
                    double bollingerBandsFactor)
    {
        BollingerBands bands = new BollingerBands(bollingerBandsDays, bollingerBandsFactor, this.security,
                        chartInterval);

        ChartLineSeriesAxes lowerBand = bands.getLowerBand();
        if (lowerBand == null || lowerBand.getValues() == null || lowerBand.getDates() == null)
            return;

        ILineSeries lineSeriesBollingerBandsLowerBand = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                        Messages.LabelChartDetailIndicatorBollingerBandsLower);
        lineSeriesBollingerBandsLowerBand.setXDateSeries(lowerBand.getDates());
        lineSeriesBollingerBandsLowerBand.setLineStyle(LineStyle.SOLID);
        lineSeriesBollingerBandsLowerBand.setLineWidth(2);
        lineSeriesBollingerBandsLowerBand.setSymbolType(PlotSymbolType.NONE);
        lineSeriesBollingerBandsLowerBand.setYSeries(lowerBand.getValues());
        lineSeriesBollingerBandsLowerBand.setAntialias(swtAntialias);
        lineSeriesBollingerBandsLowerBand.setLineColor(colorBollingerBands);
        lineSeriesBollingerBandsLowerBand.setYAxisId(0);
        lineSeriesBollingerBandsLowerBand.setVisibleInLegend(false);

        ChartLineSeriesAxes middleBand = bands.getMiddleBand();
        ILineSeries lineSeriesBollingerBandsMiddleBand = (ILineSeries) chart.getSeriesSet()
                        .createSeries(SeriesType.LINE, Messages.LabelChartDetailIndicatorBollingerBands);
        lineSeriesBollingerBandsMiddleBand.setXDateSeries(middleBand.getDates());
        lineSeriesBollingerBandsMiddleBand.setLineWidth(2);
        lineSeriesBollingerBandsMiddleBand.setLineStyle(LineStyle.DOT);
        lineSeriesBollingerBandsMiddleBand.setSymbolType(PlotSymbolType.NONE);
        lineSeriesBollingerBandsMiddleBand.setYSeries(middleBand.getValues());
        lineSeriesBollingerBandsMiddleBand.setAntialias(swtAntialias);
        lineSeriesBollingerBandsMiddleBand.setLineColor(colorBollingerBands);
        lineSeriesBollingerBandsMiddleBand.setYAxisId(0);
        lineSeriesBollingerBandsMiddleBand.setVisibleInLegend(true);

        ChartLineSeriesAxes upperBand = bands.getUpperBand();
        ILineSeries lineSeriesBollingerBandsUpperBand = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                        Messages.LabelChartDetailIndicatorBollingerBandsUpper);
        lineSeriesBollingerBandsUpperBand.setXDateSeries(upperBand.getDates());
        lineSeriesBollingerBandsUpperBand.setLineWidth(2);
        lineSeriesBollingerBandsUpperBand.setLineStyle(LineStyle.SOLID);
        lineSeriesBollingerBandsUpperBand.setSymbolType(PlotSymbolType.NONE);
        lineSeriesBollingerBandsUpperBand.setYSeries(upperBand.getValues());
        lineSeriesBollingerBandsUpperBand.setAntialias(swtAntialias);
        lineSeriesBollingerBandsUpperBand.setLineColor(colorBollingerBands);
        lineSeriesBollingerBandsUpperBand.setYAxisId(0);
        lineSeriesBollingerBandsUpperBand.setVisibleInLegend(false);
    }

    private void addFIFOPurchasePrice(ChartInterval chartInterval)
    {
        // securities w/o currency (e.g. index) cannot be bought and hence have
        // no purchase price
        if (security.getCurrencyCode() == null)
            return;

        // create a list of dates that are relevant for FIFO purchase price
        // changes (i.e. all purchase and sell events)

        Client filteredClient = new ClientSecurityFilter(security).filter(client);
        CurrencyConverter securityCurrency = converter.with(security.getCurrencyCode());

        List<LocalDate> candidates = client.getPortfolios().stream() //
                        .flatMap(p -> p.getTransactions().stream()) //
                        .filter(t -> t.getSecurity().equals(security))
                        .filter(t -> !(t.getType() == PortfolioTransaction.Type.TRANSFER_IN
                                        || t.getType() == PortfolioTransaction.Type.TRANSFER_OUT))
                        .filter(t -> !t.getDateTime().toLocalDate().isAfter(chartInterval.getEnd()))
                        .map(t -> chartInterval.contains(t.getDateTime()) ? t.getDateTime().toLocalDate()
                                        : chartInterval.getStart())
                        .distinct() //
                        .sorted() //
                        .collect(Collectors.toList());

        // calculate FIFO purchase price for each event - separate lineSeries
        // per holding period

        List<Double> values = new ArrayList<>();
        List<LocalDate> dates = new ArrayList<>();
        int seriesCounter = 0;

        for (LocalDate eventDate : candidates)
        {
            Optional<Double> purchasePrice = getPurchasePrice(filteredClient, securityCurrency, eventDate);

            if (purchasePrice.isPresent())
            {
                dates.add(eventDate);
                values.add(purchasePrice.get());
            }
            else
            {
                if (!dates.isEmpty())
                {
                    // add previous value if the data series ends here (no more
                    // future events)

                    dates.add(eventDate);
                    values.add(values.get(values.size() - 1));

                    createFIFOPurchaseLineSeries(values, dates, seriesCounter++);

                    values.clear();
                    dates.clear();
                }
                else if (dates.isEmpty())
                {
                    // if no holding period exists, then do not add the event at
                    // all
                }
            }
        }

        // add today if needed

        getPurchasePrice(filteredClient, securityCurrency, chartInterval.getEnd()).ifPresent(price -> {
            dates.add(chartInterval.getEnd());
            values.add(price);
        });

        if (!dates.isEmpty())
            createFIFOPurchaseLineSeries(values, dates, seriesCounter);
    }

    private void createFIFOPurchaseLineSeries(List<Double> values, List<LocalDate> dates, int seriesCounter)
    {
        String label = seriesCounter == 0 ? Messages.LabelChartDetailMarkerPurchaseFIFO
                        : MessageFormat.format(Messages.LabelChartDetailMarkerPurchaseFIFOHoldingPeriod,
                                        seriesCounter + 1);

        ILineSeries series = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, label);

        series.setSymbolType(PlotSymbolType.NONE);
        series.setYAxisId(0);
        series.enableStep(true);

        configureSeriesPainter(series, TimelineChart.toJavaUtilDate(dates.toArray(new LocalDate[0])),
                        Doubles.toArray(values), colorFifoPurchasePrice, 2, LineStyle.SOLID, false, seriesCounter == 0);
    }

    private void addMovingAveragePurchasePrice(ChartInterval chartInterval)
    {
        // securities w/o currency (e.g. index) cannot be bought and hence have
        // no purchase price
        if (security.getCurrencyCode() == null)
            return;

        // create a list of dates that are relevant for floating avg purchase
        // price
        // changes (i.e. all purchase and sell events)

        Client filteredClient = new ClientSecurityFilter(security).filter(client);
        CurrencyConverter securityCurrency = converter.with(security.getCurrencyCode());

        List<LocalDate> candidates = client.getPortfolios().stream() //
                        .flatMap(p -> p.getTransactions().stream()) //
                        .filter(t -> t.getSecurity().equals(security))
                        .filter(t -> !(t.getType() == PortfolioTransaction.Type.TRANSFER_IN
                                        || t.getType() == PortfolioTransaction.Type.TRANSFER_OUT))
                        .filter(t -> !t.getDateTime().toLocalDate().isAfter(chartInterval.getEnd()))
                        .map(t -> chartInterval.contains(t.getDateTime()) ? t.getDateTime().toLocalDate()
                                        : chartInterval.getStart())
                        .distinct() //
                        .sorted() //
                        .collect(Collectors.toList());

        // calculate floating avg purchase price for each event - separate
        // lineSeries
        // per holding period

        List<Double> values = new ArrayList<>();
        List<LocalDate> dates = new ArrayList<>();
        int seriesCounter = 0;

        for (LocalDate eventDate : candidates)
        {
            Optional<Double> purchasePrice = getMovingAveragePurchasePrice(filteredClient, securityCurrency, eventDate);

            if (purchasePrice.isPresent())
            {
                dates.add(eventDate);
                values.add(purchasePrice.get());
            }
            else
            {
                if (!dates.isEmpty())
                {
                    // add previous value if the data series ends here (no more
                    // future events)

                    dates.add(eventDate);
                    values.add(values.get(values.size() - 1));

                    createMovingAveragePurchaseLineSeries(values, dates, seriesCounter++);

                    values.clear();
                    dates.clear();
                }
                else if (dates.isEmpty())
                {
                    // if no holding period exists, then do not add the event at
                    // all
                }
            }
        }

        // add today if needed

        getMovingAveragePurchasePrice(filteredClient, securityCurrency, chartInterval.getEnd()).ifPresent(price -> {
            dates.add(chartInterval.getEnd());
            values.add(price);
        });

        if (!dates.isEmpty())
            createMovingAveragePurchaseLineSeries(values, dates, seriesCounter);
    }

    private void createMovingAveragePurchaseLineSeries(List<Double> values, List<LocalDate> dates, int seriesCounter)
    {
        String label = seriesCounter == 0 ? Messages.LabelChartDetailMarkerPurchaseMovingAverage
                        : MessageFormat.format(Messages.LabelChartDetailMarkerPurchaseMovingAverageHoldingPeriod,
                                        seriesCounter + 1);

        ILineSeries series = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, label);

        series.setSymbolType(PlotSymbolType.NONE);
        series.setYAxisId(0);
        series.enableStep(true);

        configureSeriesPainter(series, TimelineChart.toJavaUtilDate(dates.toArray(new LocalDate[0])),
                        Doubles.toArray(values), colorMovingAveragePurchasePrice, 2, LineStyle.SOLID, false,
                        seriesCounter == 0);
    }

    private Optional<Double> getLatestPurchasePrice()
    {
        // securities w/o currency (e.g. index) cannot be bought and hence have
        // no purchase price
        if (security.getCurrencyCode() == null)
            return Optional.empty();

        return getPurchasePrice(new ClientSecurityFilter(security).filter(client),
                        converter.with(security.getCurrencyCode()), LocalDate.now());
    }

    private Optional<Double> getPurchasePrice(Client filteredClient, CurrencyConverter currencyConverter,
                    LocalDate date)
    {
        return SecurityPerformanceSnapshot
                        .create(filteredClient, currencyConverter, Interval.of(LocalDate.MIN, date),
                                        SecurityPerformanceIndicator.Costs.class)
                        .getRecord(security) //
                        .filter(r -> !r.getFifoCostPerSharesHeld().isZero()) //
                        .map(r -> r.getFifoCostPerSharesHeld().getAmount() / Values.Quote.divider());
    }

    private Optional<Double> getMovingAveragePurchasePrice(Client filteredClient, CurrencyConverter currencyConverter,
                    LocalDate date)
    {
        return SecurityPerformanceSnapshot
                        .create(filteredClient, currencyConverter, Interval.of(LocalDate.MIN, date),
                                        SecurityPerformanceIndicator.Costs.class)
                        .getRecord(security) //
                        .filter(r -> !r.getFifoCostPerSharesHeld().isZero()) //
                        .map(r -> r.getMovingAverageCostPerSharesHeld().getAmount() / Values.Quote.divider());
    }
}

package name.abuchen.portfolio.ui.views;

import java.text.DecimalFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
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.IAction;
import org.eclipse.jface.action.IMenuManager;
import org.eclipse.jface.action.MenuManager;
import org.eclipse.jface.action.ToolBarManager;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.layout.FillLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Label;
import org.swtchart.IAxis;
import org.swtchart.ILegend;
import org.swtchart.ILineSeries;
import org.swtchart.ILineSeries.PlotSymbolType;
import org.swtchart.ISeries;
import org.swtchart.ISeries.SeriesType;
import org.swtchart.LineStyle;
import org.swtchart.Range;

import com.google.common.primitives.Doubles;
import com.ibm.icu.text.MessageFormat;

import name.abuchen.portfolio.model.AccountTransaction;
import name.abuchen.portfolio.model.AttributeType;
import name.abuchen.portfolio.model.Client;
import name.abuchen.portfolio.model.LimitPrice;
import name.abuchen.portfolio.model.PortfolioTransaction;
import name.abuchen.portfolio.model.Security;
import name.abuchen.portfolio.model.SecurityEvent;
import name.abuchen.portfolio.model.SecurityPrice;
import name.abuchen.portfolio.model.Transaction;
import name.abuchen.portfolio.model.Transaction.Unit;
import name.abuchen.portfolio.model.TransactionPair;
import name.abuchen.portfolio.money.CurrencyConverter;
import name.abuchen.portfolio.money.Money;
import name.abuchen.portfolio.money.Values;
import name.abuchen.portfolio.snapshot.ClientSnapshot;
import name.abuchen.portfolio.snapshot.filter.ClientSecurityFilter;
import name.abuchen.portfolio.snapshot.filter.ReadOnlyClient;
import name.abuchen.portfolio.snapshot.security.SecurityPerformanceIndicator;
import name.abuchen.portfolio.snapshot.security.SecurityPerformanceSnapshot;
import name.abuchen.portfolio.ui.Images;
import name.abuchen.portfolio.ui.Messages;
import name.abuchen.portfolio.ui.util.Colors;
import name.abuchen.portfolio.ui.util.DropDown;
import name.abuchen.portfolio.ui.util.SimpleAction;
import name.abuchen.portfolio.ui.util.chart.TimelineChart;
import name.abuchen.portfolio.ui.util.chart.TimelineChartToolTip;
import name.abuchen.portfolio.util.FormatHelper;
import name.abuchen.portfolio.util.Interval;
import name.abuchen.portfolio.util.TradeCalendar;
import name.abuchen.portfolio.util.TradeCalendarManager;

/**
 * Chart of historical quotes for a given security
 */
public class SecuritiesChart
{
    /**
     * A <em>closed</em> interval that includes start and end date.
     * <p/>
     * We create a separate {@code ChartInterval} class because - for historical
     * reasons - the {@link Interval} is half-open, i.e. it does not include the
     * start date. When working with charts, however, it is easier to work with
     * a closed interval that includes both start and end date.
     */
    public static class ChartInterval
    {
        private final LocalDate start;
        private final LocalDate end;

        public ChartInterval(LocalDate start, LocalDate end)
        {
            this.start = start;
            this.end = end;
        }

        public LocalDate getStart()
        {
            return start;
        }

        public LocalDate getEnd()
        {
            return end;
        }

        public boolean contains(LocalDate other)
        {
            return !other.isBefore(start) && !other.isAfter(end);
        }

        public boolean contains(LocalDateTime other)
        {
            return contains(other.toLocalDate());
        }
    }

    public enum IntervalOption
    {
        M1(Messages.SecurityTabChart1M, Messages.SecurityTabChart1MToolTip), //
        M2(Messages.SecurityTabChart2M, Messages.SecurityTabChart2MToolTip), //
        M6(Messages.SecurityTabChart6M, Messages.SecurityTabChart6MToolTip), //
        Y1(Messages.SecurityTabChart1Y, Messages.SecurityTabChart1YToolTip), //
        Y2(Messages.SecurityTabChart2Y, Messages.SecurityTabChart2YToolTip), //
        Y3(Messages.SecurityTabChart3Y, Messages.SecurityTabChart3YToolTip), //
        Y5(Messages.SecurityTabChart5Y, Messages.SecurityTabChart5YToolTip), //
        Y10(Messages.SecurityTabChart10Y, Messages.SecurityTabChart10YToolTip), //
        YTD(Messages.SecurityTabChartYTD, Messages.SecurityTabChartYTDToolTip), //
        H(Messages.SecurityTabChartHoldingPeriod, Messages.SecurityTabChartHoldingPeriodToolTip), //
        ALL(Messages.SecurityTabChartAll, Messages.SecurityTabChartAllToolTip);

        private final String label;
        private final String tooltip;

        private IntervalOption(String label, String tooltip)
        {
            this.label = label;
            this.tooltip = tooltip;
        }

        public String getLabel()
        {
            return label;
        }

        public String getTooltip()
        {
            return tooltip;
        }

        public ChartInterval getInverval(Client client, CurrencyConverter converter, Security security)
        {
            LocalDate now = LocalDate.now();

            switch (this)
            {
                case M1:
                    return new ChartInterval(now.minus(Period.ofMonths(1)), now);
                case M2:
                    return new ChartInterval(now.minus(Period.ofMonths(2)), now);
                case M6:
                    return new ChartInterval(now.minus(Period.ofMonths(6)), now);
                case Y1:
                    return new ChartInterval(now.minus(Period.ofYears(1)), now);
                case Y2:
                    return new ChartInterval(now.minus(Period.ofYears(2)), now);
                case Y3:
                    return new ChartInterval(now.minus(Period.ofYears(3)), now);
                case Y5:
                    return new ChartInterval(now.minus(Period.ofYears(5)), now);
                case Y10:
                    return new ChartInterval(now.minus(Period.ofYears(10)), now);
                case YTD:
                    return new ChartInterval(now.minus(Period.ofDays(now.getDayOfYear() - 1)), now);
                case H:
                    List<TransactionPair<?>> tx = security.getTransactions(client);
                    if (tx.isEmpty())
                        return new ChartInterval(now, now);

                    Collections.sort(tx, new TransactionPair.ByDate());
                    boolean hasHoldings = ClientSnapshot.create(client, converter, LocalDate.now())
                                    .getPositionsByVehicle().containsKey(security);

                    return new ChartInterval(tx.get(0).getTransaction().getDateTime().toLocalDate(), hasHoldings
                                    ? LocalDate.now()
                                    : tx.get(tx.size() - 1).getTransaction().getDateTime().toLocalDate());
                case ALL:
                    List<SecurityPrice> prices = security.getPricesIncludingLatest();

                    if (prices.isEmpty())
                        return new ChartInterval(now, now);
                    else
                        return new ChartInterval(prices.get(0).getDate(), prices.get(prices.size() - 1).getDate());

                default:
                    throw new IllegalArgumentException();
            }
        }
    }

    private enum ChartDetails
    {
        SCALING_LINEAR(Messages.LabelChartDetailChartScalingLinear), //
        SCALING_LOG(Messages.LabelChartDetailChartScalingLog), //
        CLOSING(Messages.LabelChartDetailChartDevelopmentClosing), //
        PURCHASEPRICE(Messages.LabelChartDetailChartDevelopmentClosingFIFO), //
        INVESTMENT(Messages.LabelChartDetailMarkerInvestments), //
        DIVIDENDS(Messages.LabelChartDetailMarkerDividends), //
        EVENTS(Messages.LabelChartDetailMarkerSplits), //
        EXTREMES(Messages.LabelChartDetailMarkerHighLow), //
        FIFOPURCHASE(Messages.LabelChartDetailMarkerPurchaseFIFO), //
        FLOATINGAVGPURCHASE(Messages.LabelChartDetailMarkerPurchaseMovingAverage), //
        BOLLINGERBANDS(Messages.LabelChartDetailIndicatorBollingerBands), //
        SMA_5DAYS(Messages.LabelChartDetailMovingAverage_5days), //
        SMA_20DAYS(Messages.LabelChartDetailMovingAverage_20days), //
        SMA_30DAYS(Messages.LabelChartDetailMovingAverage_30days), //
        SMA_38DAYS(Messages.LabelChartDetailMovingAverage_38days), //
        SMA_50DAYS(Messages.LabelChartDetailMovingAverage_50days), //
        SMA_90DAYS(Messages.LabelChartDetailMovingAverage_90days), //
        SMA_100DAYS(Messages.LabelChartDetailMovingAverage_100days), //
        SMA_200DAYS(Messages.LabelChartDetailMovingAverage_200days), //
        EMA_5DAYS(Messages.LabelChartDetailMovingAverage_5days), //
        EMA_20DAYS(Messages.LabelChartDetailMovingAverage_20days), //
        EMA_30DAYS(Messages.LabelChartDetailMovingAverage_30days), //
        EMA_38DAYS(Messages.LabelChartDetailMovingAverage_38days), //
        EMA_50DAYS(Messages.LabelChartDetailMovingAverage_50days), //
        EMA_90DAYS(Messages.LabelChartDetailMovingAverage_90days), //
        EMA_100DAYS(Messages.LabelChartDetailMovingAverage_100days), //
        EMA_200DAYS(Messages.LabelChartDetailMovingAverage_200days), //
        SHOW_MARKER_LINES(Messages.LabelChartDetailSettingsShowMarkerLines), //
        SHOW_DATA_LABELS(Messages.LabelChartDetailSettingsShowDataLabel), //
        SHOW_MISSING_TRADING_DAYS(Messages.LabelChartDetailSettingsShowMissingTradingDays), //
        SHOW_LIMITS(Messages.LabelChartDetailSettingsShowLimits);

        private final String label;

        private ChartDetails(String label)
        {
            this.label = label;
        }

        @Override
        public String toString()
        {
            return label;
        }
    }

    /* testing */ static class ChartRange
    {
        public final int start;
        public final int size;

        public final LocalDate startDate;
        public final LocalDate endDate;

        public ChartRange(int start, int end, LocalDate startDate, LocalDate endDate)
        {
            this.start = start;
            this.size = end - start;
            this.startDate = startDate;
            this.endDate = endDate;
        }

        /**
         * Maps the given {@link ChartInterval} to a range in the list of
         * security prices. Returns null if the interval does not intersect with
         * the list of prices.
         */
        public static ChartRange createFor(List<SecurityPrice> prices, ChartInterval chartInterval)
        {
            int start = Collections.binarySearch(prices, new SecurityPrice(chartInterval.getStart(), 0),
                            new SecurityPrice.ByDate());

            if (start < 0)
                start = -start - 1;

            if (start >= prices.size())
                return null;

            int end = Collections.binarySearch(prices, new SecurityPrice(chartInterval.getEnd(), 0),
                            new SecurityPrice.ByDate());

            if (end < 0)
                end = -end - 1;
            else
                end += 1; // include the entry that has been found

            if (end <= start)
                return null;

            return new ChartRange(start, end, prices.get(start).getDate(),
                            prices.get(Math.min(end, prices.size() - 1)).getDate());
        }
    }

    private Color colorQuote;

    private static final Color colorEventPurchase = Colors.getColor(26, 173, 33);
    private static final Color colorEventSale = Colors.getColor(232, 51, 69);
    private static final Color colorEventDividend = Colors.getColor(128, 0, 128);

    private static final Color colorHigh = Colors.getColor(0, 102, 0);
    private static final Color colorLow = Colors.getColor(128, 0, 0);

    private static final Color colorFifoPurchasePrice = Colors.getColor(226, 122, 121);
    private static final Color colorMovingAveragePurchasePrice = Colors.getColor(150, 82, 81);
    private static final Color colorBollingerBands = Colors.getColor(201, 141, 68);
    private static final Color colorSMA1 = Colors.getColor(179, 107, 107); // #B36B6B
    private static final Color colorSMA2 = Colors.getColor(179, 167, 107); // #B3A76B
    private static final Color colorSMA3 = Colors.getColor(131, 179, 107); // #83B36B
    private static final Color colorSMA4 = Colors.getColor(107, 179, 143); // #6BB38F
    private static final Color colorSMA5 = Colors.getColor(107, 155, 179); // #6B9BB3
    private static final Color colorSMA6 = Colors.getColor(119, 107, 179); // #776BB3
    private static final Color colorSMA7 = Colors.getColor(179, 107, 179); // #B36BB3

    private static final Color colorEMA1 = Colors.getColor(200, 107, 107); // #C86B6B
    private static final Color colorEMA2 = Colors.getColor(200, 167, 107); // #C8A76B
    private static final Color colorEMA3 = Colors.getColor(131, 200, 107); // #83C86B
    private static final Color colorEMA4 = Colors.getColor(107, 200, 143); // #6BC88F
    private static final Color colorEMA5 = Colors.getColor(107, 155, 200); // #6B9BC8
    private static final Color colorEMA6 = Colors.getColor(119, 107, 200); // #776BC8
    private static final Color colorEMA7 = Colors.getColor(200, 107, 200); // #C86BB3

    private static final Color colorAreaPositive = Colors.getColor(90, 114, 226);
    private static final Color colorAreaNegative = Colors.getColor(226, 91, 90);

    private static final Color colorNonTradingDay = Colors.getColor(255, 137, 89);

    private static final String PREF_KEY = "security-chart-details"; //$NON-NLS-1$

    private DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("d LLL"); //$NON-NLS-1$

    private Composite container;

    private Client client;
    private CurrencyConverter converter;
    private Security security;

    private TimelineChart chart;

    /**
     * Calculates dynamically for each security the interval of security prices
     * to be shown.
     */
    private IntervalOption intervalOption = IntervalOption.Y2;

    private EnumSet<ChartDetails> chartConfig = EnumSet.of(ChartDetails.INVESTMENT, ChartDetails.EVENTS,
                    ChartDetails.SCALING_LINEAR);

    private List<PaintListener> customPaintListeners = new ArrayList<>();
    private List<PaintListener> customBehindPaintListener = new ArrayList<>();
    private List<Transaction> customTooltipEvents = new ArrayList<>();

    private int swtAntialias = SWT.ON;

    public SecuritiesChart(Composite parent, Client client, CurrencyConverter converter)
    {
        this.client = client;
        this.converter = converter;

        readChartConfig(client);

        container = new Composite(parent, SWT.NONE);
        container.setLayout(new FillLayout());

        chart = new TimelineChart(container);
        chart.getTitle().setVisible(false);

        chart.getPlotArea().addPaintListener(event -> customPaintListeners.forEach(l -> l.paintControl(event)));
        chart.getPlotArea().addPaintListener(event -> customBehindPaintListener.forEach(l -> l.paintControl(event)));

        setupTooltip();

        ILegend legend = chart.getLegend();
        legend.setPosition(SWT.BOTTOM);
        legend.setVisible(true);
    }

    public IntervalOption getIntervalOption()
    {
        return intervalOption;
    }

    public void setIntervalOption(IntervalOption intervalOption)
    {
        this.intervalOption = intervalOption;
    }

    public void setQuoteColor(Color color)
    {
        this.colorQuote = color;
    }

    private void setupTooltip()
    {
        TimelineChartToolTip toolTip = chart.getToolTip();

        toolTip.showToolTipOnlyForDatesInDataSeries(Messages.ColumnQuote);

        toolTip.setDefaultValueFormat(new DecimalFormat(Values.Quote.pattern()));
        toolTip.addSeriesExclude(Messages.LabelChartDetailChartDevelopment + "Positive"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.LabelChartDetailChartDevelopment + "Negative"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.LabelChartDetailChartDevelopment + "Zero"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.SecurityMenuBuy);
        toolTip.addSeriesExclude(Messages.SecurityMenuBuy + "1"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.SecurityMenuBuy + "2"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.SecurityMenuSell);
        toolTip.addSeriesExclude(Messages.SecurityMenuSell + "1"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.SecurityMenuSell + "2"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.LabelChartDetailMarkerDividends);
        toolTip.addSeriesExclude(Messages.LabelChartDetailMarkerDividends + "1"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.LabelChartDetailMarkerDividends + "2"); //$NON-NLS-1$
        toolTip.addSeriesExclude(Messages.LabelChartDetailIndicatorBollingerBands);

        int precision = FormatHelper.getCalculatedQuoteDisplayPrecision();
        DecimalFormat calculatedFormat = new DecimalFormat(Values.CalculatedQuote.pattern());
        calculatedFormat.setMinimumFractionDigits(precision);
        calculatedFormat.setMaximumFractionDigits(precision);
        for (String period : new String[] { Messages.LabelChartDetailMovingAverage_5days,
                        Messages.LabelChartDetailMovingAverage_20days, Messages.LabelChartDetailMovingAverage_30days,
                        Messages.LabelChartDetailMovingAverage_38days, Messages.LabelChartDetailMovingAverage_50days,
                        Messages.LabelChartDetailMovingAverage_100days,
                        Messages.LabelChartDetailMovingAverage_200days, })
        {
            toolTip.overrideValueFormat(String.format("%s (%s)", Messages.LabelChartDetailMovingAverageEMA, period), //$NON-NLS-1$
                            calculatedFormat);
            toolTip.overrideValueFormat(String.format("%s (%s)", Messages.LabelChartDetailMovingAverageSMA, period), //$NON-NLS-1$
                            calculatedFormat);
        }
        toolTip.overrideValueFormat(Messages.LabelChartDetailIndicatorBollingerBandsLower, calculatedFormat);
        toolTip.overrideValueFormat(Messages.LabelChartDetailIndicatorBollingerBandsUpper, calculatedFormat);
        toolTip.overrideValueFormat(Messages.LabelChartDetailMarkerPurchaseFIFO, calculatedFormat);
        toolTip.overrideValueFormat(Messages.LabelChartDetailMarkerPurchaseMovingAverage, calculatedFormat);

        toolTip.addExtraInfo((composite, focus) -> {
            if (focus instanceof Date)
            {
                Instant instant = ((Date) focus).toInstant();
                ZonedDateTime zdt = instant.atZone(ZoneId.systemDefault());
                LocalDate date = zdt.toLocalDate();

                Interval displayInterval = Interval.of(date.minusDays(5), date.plusDays(5));

                customTooltipEvents.stream() //
                                .filter(t -> displayInterval.contains(t.getDateTime())) //
                                .forEach(t -> {
                                    if (t instanceof AccountTransaction)
                                        addDividendTooltip(composite, (AccountTransaction) t);
                                    else if (t instanceof PortfolioTransaction)
                                        addInvestmentTooltip(composite, (PortfolioTransaction) t);
                                });
            }
        });
    }

    private void addInvestmentTooltip(Composite composite, PortfolioTransaction t)
    {
        Label label = new Label(composite, SWT.NONE);
        label.setText(MessageFormat.format(Messages.LabelToolTipTransactionSummary, t.getType().toString(),
                        dateTimeFormatter.format(t.getDateTime().toLocalDate()), t.getMonetaryAmount().toString()));

        label = new Label(composite, SWT.NONE);
        label.setText(MessageFormat.format(Messages.LabelToolTipInvestmentDetails, Values.Share.format(t.getShares()),
                        Values.Quote.format(
                                        t.getGrossPricePerShare(converter.with(t.getSecurity().getCurrencyCode())))));
    }

    private void addDividendTooltip(Composite composite, AccountTransaction t)
    {
        Label label = new Label(composite, SWT.NONE);
        String amount = t.getMonetaryAmount().toString();
        label.setText(MessageFormat.format(Messages.LabelToolTipTransactionSummary, t.getType().toString(),
                        dateTimeFormatter.format(t.getDateTime().toLocalDate()), amount));

        if (t.getShares() == 0L)
        {
            label = new Label(composite, SWT.NONE);
            label.setText("\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();
            String currency = grossValue.isPresent() ? grossValue.get().getForex().getCurrencyCode()
                            : t.getCurrencyCode();

            // gross value in either forex currency or transaction currency
            String grossAmount = Money.of(currency, gross).toString();

            // gross value in transaction currency
            String grossValueAmount = Money.of(t.getCurrencyCode(), t.getGrossValueAmount()).toString();

            // display gross value in transaction currency, different gross
            // value in security currency exists
            if (!grossValueAmount.equals(grossAmount))
            {
                label = new Label(composite, SWT.NONE);
                label.setText(MessageFormat.format(Messages.LabelToolTipDividendDetailsGross, grossValueAmount));
            }

            // display gross value, if different to net amount
            if (!grossAmount.equals(amount))
            {
                label = new Label(composite, SWT.NONE);
                label.setText(MessageFormat.format(Messages.LabelToolTipDividendDetailsGross, grossAmount));
            }

            // display dividend per share in security currency
            label = new Label(composite, SWT.NONE);
            label.setText(MessageFormat.format(Messages.LabelToolTipDividendDetails, Values.Share.format(t.getShares()),
                            currency, Values.Quote.format(Math.round(gross * Values.Share.divider()
                                            * Values.Quote.factorToMoney() / t.getShares()))));

        }
    }

    private void configureSeriesPainter(ILineSeries series, Date[] dates, double[] values, Color color, int lineWidth,
                    LineStyle lineStyle, boolean enableArea, boolean visibleInLegend)
    {
        if (lineWidth != 0)
            series.setLineWidth(lineWidth);
        series.setLineStyle(lineStyle);
        series.setXDateSeries(dates);
        series.enableArea(enableArea);
        series.setYSeries(values);
        series.setAntialias(swtAntialias);

        if (color != null)
            series.setLineColor(color);
        series.setVisibleInLegend(visibleInLegend);
    }

    private final void readChartConfig(Client client)
    {
        String pref = ReadOnlyClient.unwrap(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 ignore)
            {
                // do not print exception to the log as it confuses users. The
                // old SMA200 label has been renamed, nothing we can change
                // anymore
            }
        }
    }

    public void addButtons(ToolBarManager toolBar)
    {
        List<Action> viewActions = new ArrayList<>();

        for (IntervalOption option : IntervalOption.values())
        {
            SimpleAction action = new SimpleAction(option.getLabel(), IAction.AS_CHECK_BOX, option.getTooltip(), a -> {
                this.intervalOption = option;
                updateChart();
                for (Action viewAction : viewActions)
                    viewAction.setChecked(a.equals(viewAction));
            });
            if (intervalOption == option)
                action.setChecked(true);
            viewActions.add(action);
            toolBar.add(action);
        }

        toolBar.add(new DropDown(Messages.MenuConfigureChart, Images.CONFIG, SWT.NONE, this::chartConfigAboutToShow));
    }

    private void chartConfigAboutToShow(IMenuManager manager)
    {
        MenuManager subMenuChartScaling = new MenuManager(Messages.LabelChartDetailChartScaling, null);
        MenuManager subMenuChartDevelopment = new MenuManager(Messages.LabelChartDetailChartDevelopment, null);
        MenuManager subMenuChartMarker = new MenuManager(Messages.LabelChartDetailMarker, null);
        MenuManager subMenuChartIndicator = new MenuManager(Messages.LabelChartDetailIndicator, null);
        MenuManager subMenuChartMovingAverage = new MenuManager(Messages.LabelChartDetailMovingAverage, null);
        MenuManager subMenuChartMovingAverageSMA = new MenuManager(Messages.LabelChartDetailMovingAverageSMA, null);
        MenuManager subMenuChartMovingAverageEMA = new MenuManager(Messages.LabelChartDetailMovingAverageEMA, null);
        MenuManager subMenuChartSettings = new MenuManager(Messages.LabelChartDetailSettings, null);

        subMenuChartScaling.add(addMenuAction(ChartDetails.SCALING_LINEAR));
        subMenuChartScaling.add(addMenuAction(ChartDetails.SCALING_LOG));
        subMenuChartDevelopment.add(addMenuAction(ChartDetails.CLOSING));
        subMenuChartDevelopment.add(addMenuAction(ChartDetails.PURCHASEPRICE));
        subMenuChartMarker.add(addMenuAction(ChartDetails.INVESTMENT));
        subMenuChartMarker.add(addMenuAction(ChartDetails.DIVIDENDS));
        subMenuChartMarker.add(addMenuAction(ChartDetails.EVENTS));
        subMenuChartMarker.add(addMenuAction(ChartDetails.EXTREMES));
        subMenuChartMarker.add(addMenuAction(ChartDetails.FIFOPURCHASE));
        subMenuChartMarker.add(addMenuAction(ChartDetails.FLOATINGAVGPURCHASE));
        subMenuChartMarker.add(addMenuAction(ChartDetails.SHOW_LIMITS));
        subMenuChartIndicator.add(addMenuAction(ChartDetails.BOLLINGERBANDS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_5DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_20DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_30DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_38DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_50DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_90DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_100DAYS));
        subMenuChartMovingAverageSMA.add(addMenuAction(ChartDetails.SMA_200DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_5DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_20DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_30DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_38DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_50DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_90DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_100DAYS));
        subMenuChartMovingAverageEMA.add(addMenuAction(ChartDetails.EMA_200DAYS));
        subMenuChartSettings.add(addMenuAction(ChartDetails.SHOW_MARKER_LINES));
        subMenuChartSettings.add(addMenuAction(ChartDetails.SHOW_DATA_LABELS));
        subMenuChartSettings.add(addMenuAction(ChartDetails.SHOW_MISSING_TRADING_DAYS));
        manager.add(subMenuChartScaling);
        manager.add(subMenuChartDevelopment);
        manager.add(subMenuChartMarker);
        manager.add(subMenuChartIndicator);
        manager.add(subMenuChartMovingAverage);
        subMenuChartMovingAverage.add(subMenuChartMovingAverageSMA);
        subMenuChartMovingAverage.add(subMenuChartMovingAverageEMA);
        manager.add(subMenuChartSettings);
    }

    private Action addMenuAction(ChartDetails detail)
    {
        Action action = new SimpleAction(detail.toString(), a -> {
            boolean isActive = chartConfig.contains(detail);

            if (isActive)
                chartConfig.remove(detail);
            else
                chartConfig.add(detail);

            if (!isActive)
            {
                switch (detail)
                {
                    case SCALING_LINEAR:
                        chartConfig.remove(ChartDetails.SCALING_LOG);
                        break;
                    case SCALING_LOG:
                        chartConfig.remove(ChartDetails.SCALING_LINEAR);
                        chartConfig.remove(ChartDetails.PURCHASEPRICE);
                        chartConfig.remove(ChartDetails.CLOSING);
                        break;
                    case CLOSING:
                        chartConfig.remove(ChartDetails.PURCHASEPRICE);
                        chartConfig.remove(ChartDetails.SCALING_LOG);
                        break;
                    case PURCHASEPRICE:
                        chartConfig.remove(ChartDetails.CLOSING);
                        chartConfig.remove(ChartDetails.SCALING_LOG);
                        break;
                    default:
                        break;
                }
            }

            if (!chartConfig.contains(ChartDetails.SCALING_LINEAR) && !chartConfig.contains(ChartDetails.SCALING_LOG))
                chartConfig.add(ChartDetails.SCALING_LINEAR);

            ReadOnlyClient.unwrap(client).setProperty(PREF_KEY, String.join(",", //$NON-NLS-1$
                            chartConfig.stream().map(ChartDetails::name).collect(Collectors.toList())));

            updateChart();

        });

        action.setChecked(chartConfig.contains(detail));
        return action;
    }

    public void updateChart(Client client, Security security)
    {
        this.client = client;
        this.security = security;
        updateChart();
    }

    public Control getControl()
    {
        return container;
    }

    private void updateChart()
    {
        chart.setRedraw(false);

        try
        {
            // delete all line series (quotes + possibly moving average)
            ISeries[] series = chart.getSeriesSet().getSeries();
            for (ISeries s : series)
                chart.getSeriesSet().deleteSeries(s.getId());

            chart.clearMarkerLines();
            chart.clearNonTradingDayMarker();
            customPaintListeners.clear();
            customBehindPaintListener.clear();
            customTooltipEvents.clear();

            if (security == null || security.getPrices().isEmpty())
            {
                chart.redraw();
                return;
            }

            boolean showAreaRelativeToFirstQuote = chartConfig.contains(ChartDetails.CLOSING)
                            || chartConfig.contains(ChartDetails.PURCHASEPRICE);

            // determine the interval to be shown in the chart

            ChartInterval chartInterval = intervalOption.getInverval(client, converter, security);

            // determine index range for given interval in prices list

            List<SecurityPrice> prices = security.getPricesIncludingLatest();

            ChartRange range = ChartRange.createFor(prices, chartInterval);
            if (range == null)
            {
                chart.redraw();
                return;
            }

            // prepare value arrays

            LocalDate[] dates = new LocalDate[range.size];

            double[] values = new double[range.size];
            double[] valuesRelative = new double[range.size];
            double[] valuesRelativePositive = new double[range.size];
            double[] valuesRelativeNegative = new double[range.size];
            double[] valuesZeroLine = new double[range.size];
            double firstQuote = 0;

            // Disable SWT antialias for more than 1000 records due to SWT
            // performance issue in Drawing
            swtAntialias = range.size > 1000 ? SWT.OFF : SWT.ON;

            if (!chartConfig.contains(ChartDetails.PURCHASEPRICE))
            {
                SecurityPrice p2 = prices.get(range.start);
                firstQuote = (p2.getValue() / Values.Quote.divider());
            }
            else
            {
                Optional<Double> purchasePrice = getLatestPurchasePrice();

                if (purchasePrice.isPresent())
                    firstQuote = purchasePrice.get();
                else
                    showAreaRelativeToFirstQuote = false;
            }

            addChartMarkerBackground(chartInterval, range);

            for (int ii = 0; ii < range.size; ii++)
            {
                SecurityPrice p = prices.get(ii + range.start);
                dates[ii] = p.getDate();
                values[ii] = p.getValue() / Values.Quote.divider();
                if (showAreaRelativeToFirstQuote)
                {
                    valuesRelative[ii] = (p.getValue() / Values.Quote.divider()) - firstQuote;
                    valuesZeroLine[ii] = 0;
                    if (valuesRelative[ii] >= 0)
                    {
                        valuesRelativePositive[ii] = valuesRelative[ii];
                        valuesRelativeNegative[ii] = 0;
                    }
                    else
                    {
                        valuesRelativePositive[ii] = 0;
                        valuesRelativeNegative[ii] = valuesRelative[ii];
                    }
                }
            }

            Date[] javaDates = TimelineChart.toJavaUtilDate(dates);

            if (showAreaRelativeToFirstQuote)
            {

                ILineSeries lineSeries2ndNegative = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                                Messages.LabelChartDetailChartDevelopmentClosing + "Negative"); //$NON-NLS-1$
                lineSeries2ndNegative.setSymbolType(PlotSymbolType.NONE);
                lineSeries2ndNegative.setYAxisId(1);
                configureSeriesPainter(lineSeries2ndNegative, javaDates, valuesRelativeNegative, colorAreaNegative, 1,
                                LineStyle.SOLID, true, false);

                ILineSeries lineSeries2ndPositive = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                                Messages.LabelChartDetailChartDevelopmentClosing + "Positive"); //$NON-NLS-1$
                lineSeries2ndPositive.setSymbolType(PlotSymbolType.NONE);
                lineSeries2ndPositive.setYAxisId(1);
                configureSeriesPainter(lineSeries2ndPositive, javaDates, valuesRelativePositive, colorAreaPositive, 1,
                                LineStyle.SOLID, true, false);
            }

            ILineSeries lineSeries = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                            Messages.ColumnQuote);
            lineSeries.setSymbolType(PlotSymbolType.NONE);
            configureSeriesPainter(lineSeries, javaDates, values, colorQuote, 2, LineStyle.SOLID,
                            !showAreaRelativeToFirstQuote, false);

            chart.adjustRange();

            addChartMarkerForeground(chartInterval);

            chart.adjustRange();

            IAxis yAxis1st = chart.getAxisSet().getYAxis(0);
            IAxis yAxis2nd = chart.getAxisSet().getYAxis(1);
            yAxis2nd.setRange(
                            new Range(yAxis1st.getRange().lower - firstQuote, yAxis1st.getRange().upper - firstQuote));

            yAxis1st.enableLogScale(chartConfig.contains(ChartDetails.SCALING_LOG));
            yAxis2nd.enableLogScale(chartConfig.contains(ChartDetails.SCALING_LOG));

            yAxis1st.getTick().setVisible(true);

            if (chartConfig.contains(ChartDetails.SHOW_MISSING_TRADING_DAYS))
            {
                TradeCalendar tradeCalendar = TradeCalendarManager.getInstance(security);
                List<LocalDate> calendarDates = new ArrayList<>();
                for (LocalDate calendarDate = dates[0]; calendarDate
                                .isBefore(dates[dates.length - 1]); calendarDate = calendarDate.plusDays(1))
                    calendarDates.add(calendarDate);
                for (LocalDate pricingDate : dates)
                    calendarDates.remove(pricingDate);
                for (LocalDate targetDate : calendarDates)
                {
                    if (!tradeCalendar.isHoliday(targetDate))
                        chart.addNonTradingDayMarker(targetDate, colorNonTradingDay);
                }
            }
        }
        finally
        {
            chart.setRedraw(true);
            chart.redraw();
        }
    }

    private void addChartMarkerBackground(ChartInterval chartInterval, ChartRange range)
    {
        if (chartConfig.contains(ChartDetails.BOLLINGERBANDS))
            addBollingerBandsMarkerLines(chartInterval, 20, 2);

        if (chartConfig.contains(ChartDetails.SMA_5DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_5days, 5, colorSMA1);

        if (chartConfig.contains(ChartDetails.SMA_20DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_20days, 20, colorSMA2);

        if (chartConfig.contains(ChartDetails.SMA_30DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_30days, 30, colorSMA3);

        if (chartConfig.contains(ChartDetails.SMA_38DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_38days, 38, colorSMA4);

        if (chartConfig.contains(ChartDetails.SMA_50DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_50days, 50, colorSMA4);

        if (chartConfig.contains(ChartDetails.SMA_90DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_90days, 90, colorSMA5);

        if (chartConfig.contains(ChartDetails.SMA_100DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_100days, 100, colorSMA6);

        if (chartConfig.contains(ChartDetails.SMA_200DAYS))
            addSMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageSMA,
                            Messages.LabelChartDetailMovingAverage_200days, 200, colorSMA7);

        if (chartConfig.contains(ChartDetails.EMA_5DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_5days, 5, colorEMA1);

        if (chartConfig.contains(ChartDetails.EMA_20DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_20days, 20, colorEMA2);

        if (chartConfig.contains(ChartDetails.EMA_30DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_30days, 30, colorEMA3);

        if (chartConfig.contains(ChartDetails.EMA_38DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_38days, 38, colorEMA4);

        if (chartConfig.contains(ChartDetails.EMA_50DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_50days, 50, colorEMA4);

        if (chartConfig.contains(ChartDetails.EMA_90DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_90days, 90, colorEMA5);

        if (chartConfig.contains(ChartDetails.EMA_100DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_100days, 100, colorEMA6);

        if (chartConfig.contains(ChartDetails.EMA_200DAYS))
            addEMAMarkerLines(chartInterval, Messages.LabelChartDetailMovingAverageEMA,
                            Messages.LabelChartDetailMovingAverage_200days, 200, colorEMA7);

        if (chartConfig.contains(ChartDetails.SHOW_LIMITS))
            addLimitLines(chartInterval, range);
    }

    private void addChartMarkerForeground(ChartInterval chartInterval)
    {
        if (chartConfig.contains(ChartDetails.FIFOPURCHASE))
            addFIFOPurchasePrice(chartInterval);

        if (chartConfig.contains(ChartDetails.FLOATINGAVGPURCHASE))
            addMovingAveragePurchasePrice(chartInterval);

        if (chartConfig.contains(ChartDetails.INVESTMENT))
            addInvestmentMarkerLines(chartInterval);

        if (chartConfig.contains(ChartDetails.DIVIDENDS))
            addDividendMarkerLines(chartInterval);

        if (chartConfig.contains(ChartDetails.EVENTS))
            addEventMarkerLines(chartInterval);

        if (chartConfig.contains(ChartDetails.EXTREMES))
            addExtremesMarkerLines(chartInterval);
    }

    private void addLimitLines(ChartInterval chartInterval, ChartRange range)
    {
        this.security.getAttributes().getMap().forEach((key, val) -> {
            // null OR not Limit Price --> ignore
            if (val == null || val.getClass() != LimitPrice.class)
                return;

            LimitPrice limitAttribute = (LimitPrice) val;

            Optional<AttributeType> attributeName = client.getSettings().getAttributeTypes()
                            .filter(attr -> attr.getId().equals(key)).findFirst();
            // could not find name of limit attribute --> don't draw
            if (attributeName.isEmpty())
                return;

            String lineID = attributeName.get().getName() + " (" + limitAttribute.toString() + ")"; //$NON-NLS-1$ //$NON-NLS-2$

            // horizontal line: only two points required
            LocalDate[] dates = new LocalDate[2];
            dates[0] = range.startDate;
            dates[1] = range.endDate;

            // both points with same y-value
            double[] values = new double[2];
            values[0] = values[1] = limitAttribute.getValue() / Values.Quote.divider();

            ILineSeries lineSeriesLimit = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, lineID);
            lineSeriesLimit.setXDateSeries(TimelineChart.toJavaUtilDate(dates));
            lineSeriesLimit.setLineWidth(2);
            lineSeriesLimit.setLineStyle(LineStyle.DASH);
            lineSeriesLimit.enableArea(false);
            lineSeriesLimit.setSymbolType(PlotSymbolType.NONE);
            lineSeriesLimit.setYSeries(values);
            lineSeriesLimit.setAntialias(swtAntialias);
            lineSeriesLimit.setLineColor(Colors.ICON_ORANGE);
            lineSeriesLimit.setYAxisId(0);
            lineSeriesLimit.setVisibleInLegend(true);
        });
    }

    private void addSMAMarkerLines(ChartInterval chartInterval, String smaSeries, String smaDaysWording, int smaDays,
                    Color smaColor)
    {
        ChartLineSeriesAxes smaLines = new SimpleMovingAverage(smaDays, this.security, chartInterval).getSMA();
        if (smaLines == null || smaLines.getValues() == null || smaLines.getDates() == null)
            return;

        @SuppressWarnings("nls")
        String lineID = smaSeries + " (" + smaDaysWording + ")";

        ILineSeries lineSeriesSMA = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, lineID);
        lineSeriesSMA.setXDateSeries(smaLines.getDates());
        lineSeriesSMA.setLineWidth(2);
        lineSeriesSMA.enableArea(false);
        lineSeriesSMA.setSymbolType(PlotSymbolType.NONE);
        lineSeriesSMA.setYSeries(smaLines.getValues());
        lineSeriesSMA.setAntialias(swtAntialias);
        lineSeriesSMA.setLineColor(smaColor);
        lineSeriesSMA.setYAxisId(0);
        lineSeriesSMA.setVisibleInLegend(true);
    }

    private void addEMAMarkerLines(ChartInterval chartInterval, String emaSeries, String emaDaysWording, int emaDays,
                    Color emaColor)
    {
        ChartLineSeriesAxes emaLines = new ExponentialMovingAverage(emaDays, this.security, chartInterval).getEMA();
        if (emaLines == null || emaLines.getValues() == null || emaLines.getDates() == null)
            return;

        @SuppressWarnings("nls")
        String lineID = emaSeries + " (" + emaDaysWording + ")";

        ILineSeries lineSeriesEMA = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, lineID);
        lineSeriesEMA.setXDateSeries(emaLines.getDates());
        lineSeriesEMA.setLineWidth(2);
        lineSeriesEMA.enableArea(false);
        lineSeriesEMA.setSymbolType(PlotSymbolType.NONE);
        lineSeriesEMA.setYSeries(emaLines.getValues());
        lineSeriesEMA.setAntialias(swtAntialias);
        lineSeriesEMA.setLineColor(emaColor);
        lineSeriesEMA.setYAxisId(0);
        lineSeriesEMA.setVisibleInLegend(true);
    }

    private void addInvestmentMarkerLines(ChartInterval chartInterval)
    {
        List<PortfolioTransaction> purchase = client.getPortfolios().stream().flatMap(p -> p.getTransactions().stream())
                        .filter(t -> t.getSecurity() == security)
                        .filter(t -> t.getType() == PortfolioTransaction.Type.BUY
                                        || t.getType() == PortfolioTransaction.Type.DELIVERY_INBOUND)
                        .filter(t -> chartInterval.contains(t.getDateTime())) //
                        .sorted(new Transaction.ByDate()).collect(Collectors.toList());

        addInvestmentMarkers(purchase, Messages.SecurityMenuBuy, colorEventPurchase);

        List<PortfolioTransaction> sales = client.getPortfolios().stream().flatMap(p -> p.getTransactions().stream())
                        .filter(t -> t.getSecurity() == security)
                        .filter(t -> t.getType() == PortfolioTransaction.Type.SELL
                                        || t.getType() == PortfolioTransaction.Type.DELIVERY_OUTBOUND)
                        .filter(t -> chartInterval.contains(t.getDateTime())) //
                        .sorted(new Transaction.ByDate()).collect(Collectors.toList());

        addInvestmentMarkers(sales, Messages.SecurityMenuSell, colorEventSale);
    }

    private void addInvestmentMarkers(List<PortfolioTransaction> transactions, String seriesLabel, Color color)
    {
        if (transactions.isEmpty())
            return;

        customTooltipEvents.addAll(transactions);

        if (chartConfig.contains(ChartDetails.SHOW_MARKER_LINES))
        {
            transactions.forEach(t -> {
                String label = Values.Share.format(t.getType().isPurchase() ? t.getShares() : -t.getShares());
                double value = t.getGrossPricePerShare(converter.with(t.getSecurity().getCurrencyCode())).getAmount()
                                / Values.Quote.divider();
                chart.addMarkerLine(t.getDateTime().toLocalDate(), color, label, value);
            });
        }
        else
        {
            Date[] dates = transactions.stream().map(PortfolioTransaction::getDateTime)
                            .map(d -> Date.from(d.atZone(ZoneId.systemDefault()).toInstant()))
                            .collect(Collectors.toList()).toArray(new Date[0]);

            double[] values = transactions.stream().mapToDouble(
                            t -> t.getGrossPricePerShare(converter.with(t.getSecurity().getCurrencyCode())).getAmount()
                                            / Values.Quote.divider())
                            .toArray();

            ILineSeries border = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, seriesLabel + "2"); //$NON-NLS-1$
            border.setYAxisId(0);
            border.setSymbolColor(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
            border.setSymbolType(PlotSymbolType.DIAMOND);
            border.setSymbolSize(7);

            configureSeriesPainter(border, dates, values, null, 0, LineStyle.NONE, false, false);

            ILineSeries background = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                            seriesLabel + "1"); //$NON-NLS-1$
            background.setYAxisId(0);
            background.setSymbolType(PlotSymbolType.DIAMOND);
            background.setSymbolSize(6);
            background.setSymbolColor(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
            configureSeriesPainter(background, dates, values, null, 0, LineStyle.NONE, false, false);

            ILineSeries inner = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, seriesLabel);
            inner.setYAxisId(0);
            inner.setSymbolType(PlotSymbolType.DIAMOND);
            inner.setSymbolSize(4);
            inner.setSymbolColor(color);
            configureSeriesPainter(inner, dates, values, color, 0, LineStyle.NONE, false, true);

            if (chartConfig.contains(ChartDetails.SHOW_DATA_LABELS))
            {
                customPaintListeners.add(event -> {
                    IAxis xAxis = chart.getAxisSet().getXAxis(0);
                    IAxis yAxis = chart.getAxisSet().getYAxis(0);

                    for (int index = 0; index < dates.length; index++)
                    {
                        int x = xAxis.getPixelCoordinate(dates[index].getTime());
                        int y = yAxis.getPixelCoordinate(values[index]);

                        PortfolioTransaction t = transactions.get(index);
                        String label = Values.Share.format(t.getType().isPurchase() ? t.getShares() : -t.getShares());
                        Point textExtent = event.gc.textExtent(label);

                        event.gc.setForeground(Colors.theme().defaultForeground());
                        event.gc.drawText(label, x - (textExtent.x / 2), y + border.getSymbolSize(), true);
                    }
                });
            }
        }
    }

    private void addDividendMarkerLines(ChartInterval chartInterval)
    {
        List<AccountTransaction> dividends = client.getAccounts().stream().flatMap(a -> a.getTransactions().stream())
                        .filter(t -> t.getSecurity() == security)
                        .filter(t -> t.getType() == AccountTransaction.Type.DIVIDENDS)
                        .filter(t -> chartInterval.contains(t.getDateTime())).sorted(new Transaction.ByDate())
                        .collect(Collectors.toList());

        if (dividends.isEmpty())
            return;

        customTooltipEvents.addAll(dividends);

        if (chartConfig.contains(ChartDetails.SHOW_MARKER_LINES))
        {
            dividends.forEach(t -> chart.addMarkerLine(t.getDateTime().toLocalDate(), colorEventDividend,
                            getDividendLabel(t)));
        }
        else
        {
            Date[] dates = dividends.stream().map(AccountTransaction::getDateTime)
                            .map(d -> Date.from(d.atZone(ZoneId.systemDefault()).toInstant()))
                            .collect(Collectors.toList()).toArray(new Date[0]);

            IAxis yAxis1st = chart.getAxisSet().getYAxis(0);
            double yAxis1stAxisPrice = yAxis1st.getRange().lower;

            double[] values = new double[dates.length];
            Arrays.fill(values, yAxis1stAxisPrice);

            ILineSeries border = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                            Messages.LabelChartDetailMarkerDividends + "2"); //$NON-NLS-1$
            border.setYAxisId(0);
            border.setSymbolType(PlotSymbolType.SQUARE);
            border.setSymbolSize(6);
            border.setSymbolColor(Display.getDefault().getSystemColor(SWT.COLOR_BLACK));
            configureSeriesPainter(border, dates, values, null, 0, LineStyle.NONE, false, false);

            ILineSeries background = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                            Messages.LabelChartDetailMarkerDividends + "1"); //$NON-NLS-1$
            background.setYAxisId(0);
            background.setSymbolType(PlotSymbolType.SQUARE);
            background.setSymbolSize(5);
            background.setSymbolColor(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
            configureSeriesPainter(background, dates, values, null, 0, LineStyle.NONE, false, false);

            ILineSeries inner = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                            Messages.LabelChartDetailMarkerDividends);
            inner.setYAxisId(0);
            inner.setSymbolType(PlotSymbolType.SQUARE);
            inner.setSymbolSize(3);
            inner.setSymbolColor(colorEventDividend);
            configureSeriesPainter(inner, dates, values, null, 0, LineStyle.NONE, false, true);

            if (chartConfig.contains(ChartDetails.SHOW_DATA_LABELS))
            {
                customPaintListeners.add(event -> {
                    IAxis xAxis = chart.getAxisSet().getXAxis(0);
                    IAxis yAxis = chart.getAxisSet().getYAxis(0);

                    int yPosLabel = 0;
                    int lastWriteLabelLevel1 = 0;
                    int lastWriteLabelLevel2 = 0;
                    int lastWriteLabelLevel3 = 0;

                    for (int index = 0; index < dates.length; index++)
                    {
                        boolean freeSpaceForLabelLevel1 = true;
                        boolean freeSpaceForLabelLevel2 = true;
                        boolean freeSpaceForLabelLevel3 = true;

                        int x = xAxis.getPixelCoordinate(dates[index].getTime());
                        int y = yAxis.getPixelCoordinate(values[index]);

                        String label = getDividendLabel(dividends.get(index));
                        Point textExtent = event.gc.textExtent(label);

                        event.gc.setForeground(Colors.theme().defaultForeground());

                        if (((x - (textExtent.x / 2)) - lastWriteLabelLevel1) <= 0)
                            freeSpaceForLabelLevel1 = false;

                        if (((x - (textExtent.x / 2)) - lastWriteLabelLevel2) <= 0)
                            freeSpaceForLabelLevel2 = false;

                        if (((x - (textExtent.x / 2)) - lastWriteLabelLevel3) <= 0)
                            freeSpaceForLabelLevel3 = false;

                        if (freeSpaceForLabelLevel1 || freeSpaceForLabelLevel2 || freeSpaceForLabelLevel3)
                        {
                            if (freeSpaceForLabelLevel1)
                            {
                                yPosLabel = y - textExtent.y - border.getSymbolSize();
                                lastWriteLabelLevel1 = (x + (textExtent.x / 2));
                            }
                            if (freeSpaceForLabelLevel2 && !freeSpaceForLabelLevel1)
                            {
                                yPosLabel = yPosLabel - textExtent.y;
                                lastWriteLabelLevel2 = (x + (textExtent.x / 2));
                            }
                            if (freeSpaceForLabelLevel3 && !freeSpaceForLabelLevel2 && !freeSpaceForLabelLevel1)
                            {
                                yPosLabel = yPosLabel - textExtent.y;
                                lastWriteLabelLevel3 = (x + (textExtent.x / 2));
                            }
                            event.gc.drawText(label, x - (textExtent.x / 2), yPosLabel, true);
                        }
                    }
                });
            }
        }
    }

    private String getDividendLabel(AccountTransaction t)
    {
        if (t.getShares() == 0L)
        {
            return "\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());

            return Values.Quote.format(perShare);
        }
    }

    private void addEventMarkerLines(ChartInterval chartInterval)
    {
        security.getEvents().stream() //
                        .filter(e -> chartInterval.contains(e.getDate())) //
                        .filter(e -> e.getType() != SecurityEvent.Type.DIVIDEND_PAYMENT) //
                        .forEach(e -> chart.addMarkerLine(e.getDate(),
                                        Display.getDefault().getSystemColor(SWT.COLOR_DARK_GRAY), e.getDetails()));
    }

    private void addExtremesMarkerLines(ChartInterval chartInterval)
    {
        Optional<SecurityPrice> max = security.getPricesIncludingLatest().stream() //
                        .filter(p -> chartInterval.contains(p.getDate())) //
                        .max(Comparator.comparing(SecurityPrice::getValue));

        Optional<SecurityPrice> min = security.getPricesIncludingLatest().stream() //
                        .filter(p -> chartInterval.contains(p.getDate())) //
                        .min(Comparator.comparing(SecurityPrice::getValue));

        max.ifPresent(high -> addExtremeMarker(high, PlotSymbolType.TRIANGLE, // 
                        Messages.LabelChartDetailMarkerHigh, colorHigh));
        min.ifPresent(low -> addExtremeMarker(low, PlotSymbolType.INVERTED_TRIANGLE, // 
                        Messages.LabelChartDetailMarkerLow, colorLow));
    }

    private void addExtremeMarker(SecurityPrice price, PlotSymbolType plotSymbolType, String seriesLabel, Color color)
    {
        LocalDate eventDate = price.getDate();
        String valueFormat = Values.Quote.format(price.getValue());
        double value = price.getValue() / Values.Quote.divider();

        if (chartConfig.contains(ChartDetails.SHOW_MARKER_LINES))
        {
            chart.addMarkerLine(eventDate, color, valueFormat);
        }
        else
        {
            Date zonedDate = Date.from(eventDate.atStartOfDay(ZoneId.systemDefault()).toInstant());

            ILineSeries inner = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, seriesLabel);
            inner.setYAxisId(0);
            inner.setSymbolType(plotSymbolType);
            inner.setSymbolSize(6);
            inner.setSymbolColor(color);
            configureSeriesPainter(inner, new Date[] { zonedDate }, new double[] { value }, color, 0, LineStyle.NONE,
                            false, true);

            if (chartConfig.contains(ChartDetails.SHOW_DATA_LABELS))
            {
                customPaintListeners.add(event -> {
                    IAxis xAxis = chart.getAxisSet().getXAxis(0);
                    IAxis yAxis = chart.getAxisSet().getYAxis(0);

                    int x = xAxis.getPixelCoordinate(zonedDate.getTime());
                    int y = yAxis.getPixelCoordinate(value);
                    Point textExtent = event.gc.textExtent(valueFormat);

                    event.gc.setForeground(Colors.theme().defaultForeground());

                    if (inner.getSymbolColor() == colorHigh)
                        y = y - textExtent.y - inner.getSymbolSize();
                    else
                        y = y + inner.getSymbolSize();

                    event.gc.drawText(valueFormat, x - (textExtent.x / 2), y, true);
                });
            }
        }
    }

    private void addBollingerBandsMarkerLines(ChartInterval chartInterval, int bollingerBandsDays,
                    double bollingerBandsFactor)
    {
        BollingerBands bands = new BollingerBands(bollingerBandsDays, bollingerBandsFactor, this.security,
                        chartInterval);

        ChartLineSeriesAxes lowerBand = bands.getLowerBand();
        if (lowerBand == null || lowerBand.getValues() == null || lowerBand.getDates() == null)
            return;

        ILineSeries lineSeriesBollingerBandsLowerBand = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                        Messages.LabelChartDetailIndicatorBollingerBandsLower);
        lineSeriesBollingerBandsLowerBand.setXDateSeries(lowerBand.getDates());
        lineSeriesBollingerBandsLowerBand.setLineStyle(LineStyle.SOLID);
        lineSeriesBollingerBandsLowerBand.setLineWidth(2);
        lineSeriesBollingerBandsLowerBand.setSymbolType(PlotSymbolType.NONE);
        lineSeriesBollingerBandsLowerBand.setYSeries(lowerBand.getValues());
        lineSeriesBollingerBandsLowerBand.setAntialias(swtAntialias);
        lineSeriesBollingerBandsLowerBand.setLineColor(colorBollingerBands);
        lineSeriesBollingerBandsLowerBand.setYAxisId(0);
        lineSeriesBollingerBandsLowerBand.setVisibleInLegend(false);

        ChartLineSeriesAxes middleBand = bands.getMiddleBand();
        ILineSeries lineSeriesBollingerBandsMiddleBand = (ILineSeries) chart.getSeriesSet()
                        .createSeries(SeriesType.LINE, Messages.LabelChartDetailIndicatorBollingerBands);
        lineSeriesBollingerBandsMiddleBand.setXDateSeries(middleBand.getDates());
        lineSeriesBollingerBandsMiddleBand.setLineWidth(2);
        lineSeriesBollingerBandsMiddleBand.setLineStyle(LineStyle.DOT);
        lineSeriesBollingerBandsMiddleBand.setSymbolType(PlotSymbolType.NONE);
        lineSeriesBollingerBandsMiddleBand.setYSeries(middleBand.getValues());
        lineSeriesBollingerBandsMiddleBand.setAntialias(swtAntialias);
        lineSeriesBollingerBandsMiddleBand.setLineColor(colorBollingerBands);
        lineSeriesBollingerBandsMiddleBand.setYAxisId(0);
        lineSeriesBollingerBandsMiddleBand.setVisibleInLegend(true);

        ChartLineSeriesAxes upperBand = bands.getUpperBand();
        ILineSeries lineSeriesBollingerBandsUpperBand = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE,
                        Messages.LabelChartDetailIndicatorBollingerBandsUpper);
        lineSeriesBollingerBandsUpperBand.setXDateSeries(upperBand.getDates());
        lineSeriesBollingerBandsUpperBand.setLineWidth(2);
        lineSeriesBollingerBandsUpperBand.setLineStyle(LineStyle.SOLID);
        lineSeriesBollingerBandsUpperBand.setSymbolType(PlotSymbolType.NONE);
        lineSeriesBollingerBandsUpperBand.setYSeries(upperBand.getValues());
        lineSeriesBollingerBandsUpperBand.setAntialias(swtAntialias);
        lineSeriesBollingerBandsUpperBand.setLineColor(colorBollingerBands);
        lineSeriesBollingerBandsUpperBand.setYAxisId(0);
        lineSeriesBollingerBandsUpperBand.setVisibleInLegend(false);
    }

    private void addFIFOPurchasePrice(ChartInterval chartInterval)
    {
        // securities w/o currency (e.g. index) cannot be bought and hence have
        // no purchase price
        if (security.getCurrencyCode() == null)
            return;

        // create a list of dates that are relevant for FIFO purchase price
        // changes (i.e. all purchase and sell events)

        Client filteredClient = new ClientSecurityFilter(security).filter(client);
        CurrencyConverter securityCurrency = converter.with(security.getCurrencyCode());

        List<LocalDate> candidates = client.getPortfolios().stream() //
                        .flatMap(p -> p.getTransactions().stream()) //
                        .filter(t -> t.getSecurity().equals(security))
                        .filter(t -> !(t.getType() == PortfolioTransaction.Type.TRANSFER_IN
                                        || t.getType() == PortfolioTransaction.Type.TRANSFER_OUT))
                        .filter(t -> !t.getDateTime().toLocalDate().isAfter(chartInterval.getEnd()))
                        .map(t -> chartInterval.contains(t.getDateTime()) ? t.getDateTime().toLocalDate()
                                        : chartInterval.getStart())
                        .distinct() //
                        .sorted() //
                        .collect(Collectors.toList());

        // calculate FIFO purchase price for each event - separate lineSeries
        // per holding period

        List<Double> values = new ArrayList<>();
        List<LocalDate> dates = new ArrayList<>();
        int seriesCounter = 0;

        for (LocalDate eventDate : candidates)
        {
            Optional<Double> purchasePrice = getPurchasePrice(filteredClient, securityCurrency, eventDate);

            if (purchasePrice.isPresent())
            {
                dates.add(eventDate);
                values.add(purchasePrice.get());
            }
            else
            {
                if (!dates.isEmpty())
                {
                    // add previous value if the data series ends here (no more
                    // future events)

                    dates.add(eventDate);
                    values.add(values.get(values.size() - 1));

                    createFIFOPurchaseLineSeries(values, dates, seriesCounter++);

                    values.clear();
                    dates.clear();
                }
                else if (dates.isEmpty())
                {
                    // if no holding period exists, then do not add the event at
                    // all
                }
            }
        }

        // add today if needed

        getPurchasePrice(filteredClient, securityCurrency, chartInterval.getEnd()).ifPresent(price -> {
            dates.add(chartInterval.getEnd());
            values.add(price);
        });

        if (!dates.isEmpty())
            createFIFOPurchaseLineSeries(values, dates, seriesCounter);
    }

    private void createFIFOPurchaseLineSeries(List<Double> values, List<LocalDate> dates, int seriesCounter)
    {
        String label = seriesCounter == 0 ? Messages.LabelChartDetailMarkerPurchaseFIFO
                        : MessageFormat.format(Messages.LabelChartDetailMarkerPurchaseFIFOHoldingPeriod,
                                        seriesCounter + 1);

        ILineSeries series = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, label);

        series.setSymbolType(PlotSymbolType.NONE);
        series.setYAxisId(0);
        series.enableStep(true);

        configureSeriesPainter(series, TimelineChart.toJavaUtilDate(dates.toArray(new LocalDate[0])),
                        Doubles.toArray(values), colorFifoPurchasePrice, 2, LineStyle.SOLID, false, seriesCounter == 0);
    }

    private void addMovingAveragePurchasePrice(ChartInterval chartInterval)
    {
        // securities w/o currency (e.g. index) cannot be bought and hence have
        // no purchase price
        if (security.getCurrencyCode() == null)
            return;

        // create a list of dates that are relevant for floating avg purchase
        // price
        // changes (i.e. all purchase and sell events)

        Client filteredClient = new ClientSecurityFilter(security).filter(client);
        CurrencyConverter securityCurrency = converter.with(security.getCurrencyCode());

        List<LocalDate> candidates = client.getPortfolios().stream() //
                        .flatMap(p -> p.getTransactions().stream()) //
                        .filter(t -> t.getSecurity().equals(security))
                        .filter(t -> !(t.getType() == PortfolioTransaction.Type.TRANSFER_IN
                                        || t.getType() == PortfolioTransaction.Type.TRANSFER_OUT))
                        .filter(t -> !t.getDateTime().toLocalDate().isAfter(chartInterval.getEnd()))
                        .map(t -> chartInterval.contains(t.getDateTime()) ? t.getDateTime().toLocalDate()
                                        : chartInterval.getStart())
                        .distinct() //
                        .sorted() //
                        .collect(Collectors.toList());

        // calculate floating avg purchase price for each event - separate
        // lineSeries
        // per holding period

        List<Double> values = new ArrayList<>();
        List<LocalDate> dates = new ArrayList<>();
        int seriesCounter = 0;

        for (LocalDate eventDate : candidates)
        {
            Optional<Double> purchasePrice = getMovingAveragePurchasePrice(filteredClient, securityCurrency, eventDate);

            if (purchasePrice.isPresent())
            {
                dates.add(eventDate);
                values.add(purchasePrice.get());
            }
            else
            {
                if (!dates.isEmpty())
                {
                    // add previous value if the data series ends here (no more
                    // future events)

                    dates.add(eventDate);
                    values.add(values.get(values.size() - 1));

                    createMovingAveragePurchaseLineSeries(values, dates, seriesCounter++);

                    values.clear();
                    dates.clear();
                }
                else if (dates.isEmpty())
                {
                    // if no holding period exists, then do not add the event at
                    // all
                }
            }
        }

        // add today if needed

        getMovingAveragePurchasePrice(filteredClient, securityCurrency, chartInterval.getEnd()).ifPresent(price -> {
            dates.add(chartInterval.getEnd());
            values.add(price);
        });

        if (!dates.isEmpty())
            createMovingAveragePurchaseLineSeries(values, dates, seriesCounter);
    }

    private void createMovingAveragePurchaseLineSeries(List<Double> values, List<LocalDate> dates, int seriesCounter)
    {
        String label = seriesCounter == 0 ? Messages.LabelChartDetailMarkerPurchaseMovingAverage
                        : MessageFormat.format(Messages.LabelChartDetailMarkerPurchaseMovingAverageHoldingPeriod,
                                        seriesCounter + 1);

        ILineSeries series = (ILineSeries) chart.getSeriesSet().createSeries(SeriesType.LINE, label);

        series.setSymbolType(PlotSymbolType.NONE);
        series.setYAxisId(0);
        series.enableStep(true);

        configureSeriesPainter(series, TimelineChart.toJavaUtilDate(dates.toArray(new LocalDate[0])),
                        Doubles.toArray(values), colorMovingAveragePurchasePrice, 2, LineStyle.SOLID, false,
                        seriesCounter == 0);
    }

    private Optional<Double> getLatestPurchasePrice()
    {
        // securities w/o currency (e.g. index) cannot be bought and hence have
        // no purchase price
        if (security.getCurrencyCode() == null)
            return Optional.empty();

        return getPurchasePrice(new ClientSecurityFilter(security).filter(client),
                        converter.with(security.getCurrencyCode()), LocalDate.now());
    }

    private Optional<Double> getPurchasePrice(Client filteredClient, CurrencyConverter currencyConverter,
                    LocalDate date)
    {
        return SecurityPerformanceSnapshot
                        .create(filteredClient, currencyConverter, Interval.of(LocalDate.MIN, date),
                                        SecurityPerformanceIndicator.Costs.class)
                        .getRecord(security) //
                        .filter(r -> !r.getFifoCostPerSharesHeld().isZero()) //
                        .map(r -> r.getFifoCostPerSharesHeld().getAmount() / Values.Quote.divider());
    }

    private Optional<Double> getMovingAveragePurchasePrice(Client filteredClient, CurrencyConverter currencyConverter,
                    LocalDate date)
    {
        return SecurityPerformanceSnapshot
                        .create(filteredClient, currencyConverter, Interval.of(LocalDate.MIN, date),
                                        SecurityPerformanceIndicator.Costs.class)
                        .getRecord(security) //
                        .filter(r -> !r.getFifoCostPerSharesHeld().isZero()) //
                        .map(r -> r.getMovingAverageCostPerSharesHeld().getAmount() / Values.Quote.divider());
    }
}

Why not using a ternary operator instead of doing an assignement to `true`and maybe switching it to `false` into a for loop?