/* =========================================================== * TradeManager : a application to trade strategies for the Java(tm) platform * =========================================================== * * (C) Copyright 2011-2011, by Simon Allen and Contributors. * * Project Info: org.trade * * This library is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation; either version 2.1 of the License, or * (at your option) any later version. * * This library is distributed in the hope that it will be useful, but * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public * License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, * USA. * * [Java is a trademark or registered trademark of Oracle, Inc. * in the United States and other countries.] * * (C) Copyright 2011-2011, by Simon Allen and Contributors. * * Original Author: Simon Allen; * Contributor(s): -; * * Changes * ------- * */ package org.trade.ui.chart; import java.awt.BasicStroke; import java.awt.BorderLayout; import java.awt.Color; import java.awt.Stroke; import java.text.SimpleDateFormat; import java.time.ZonedDateTime; import java.util.ArrayList; import java.util.List; import javax.swing.JPanel; import org.jfree.chart.ChartMouseEvent; import org.jfree.chart.ChartMouseListener; import org.jfree.chart.ChartPanel; import org.jfree.chart.JFreeChart; import org.jfree.chart.annotations.XYPointerAnnotation; import org.jfree.chart.annotations.XYTextAnnotation; import org.jfree.chart.axis.AxisLocation; import org.jfree.chart.axis.DateAxis; import org.jfree.chart.axis.DateTickMarkPosition; import org.jfree.chart.axis.NumberAxis; import org.jfree.chart.axis.SegmentedTimeline; import org.jfree.chart.axis.SegmentedTimeline.Segment; import org.jfree.chart.block.BlockContainer; import org.jfree.chart.block.BorderArrangement; import org.jfree.chart.block.EmptyBlock; import org.jfree.chart.entity.PlotEntity; import org.jfree.chart.entity.XYItemEntity; import org.jfree.chart.plot.CombinedDomainXYPlot; import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.plot.ValueMarker; import org.jfree.chart.plot.XYPlot; import org.jfree.chart.renderer.xy.StandardXYItemRenderer; import org.jfree.chart.renderer.xy.XYItemRenderer; import org.jfree.chart.title.CompositeTitle; import org.jfree.chart.title.TextTitle; import org.jfree.data.Range; import org.jfree.data.general.SeriesChangeEvent; import org.jfree.data.general.SeriesChangeListener; import org.jfree.data.xy.XYDataset; import org.jfree.ui.RectangleEdge; import org.jfree.ui.TextAnchor; import org.trade.core.util.TradingCalendar; import org.trade.core.valuetype.Money; import org.trade.core.valuetype.ValueTypeException; import org.trade.dictionary.valuetype.Action; import org.trade.persistent.dao.Tradingday; import org.trade.strategy.data.CandleSeries; import org.trade.strategy.data.IndicatorDataset; import org.trade.strategy.data.IndicatorSeries; import org.trade.strategy.data.StrategyData; import org.trade.strategy.data.candle.CandleItem; /** */ public class CandlestickChart extends JPanel implements SeriesChangeListener { private static final long serialVersionUID = 2842422936659217811L; private JFreeChart chart = null; private static final String TIME_FORMAT = "HH:mm:ss"; private static final String TIME_FORMAT_SHORT = "HH:mm"; private final TextTitle titleLegend1 = new TextTitle(" Time: 0, Price :0.0"); private final TextTitle titleLegend2 = new TextTitle( "Time:00:00 Open: 0.0 High: 0.0 Low: 0.0 Close: 0.0 Vwap: 0.0"); private Stroke stroke = null; private ValueMarker valueMarker = null; private XYTextAnnotation closePriceLine = null; private XYTextAnnotation clickCrossHairs = null; private StrategyData strategyData = null; /** * A demonstration application showing a candlestick chart. * * @param title * the frame title. * @param strategyData * StrategyData */ public CandlestickChart(final String title, StrategyData strategyData, Tradingday tradingday) { this.strategyData = strategyData; this.setLayout(new BorderLayout()); // Used to mark the current price stroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[] { 10, 3 }, 0); valueMarker = new ValueMarker(0.00, Color.black, stroke); this.chart = createChart(this.strategyData, title, tradingday); BlockContainer container = new BlockContainer(new BorderArrangement()); container.add(titleLegend1, RectangleEdge.LEFT); container.add(titleLegend2, RectangleEdge.RIGHT); container.add(new EmptyBlock(2000, 0)); CompositeTitle legends = new CompositeTitle(container); legends.setPosition(RectangleEdge.BOTTOM); this.chart.addSubtitle(legends); final ChartPanel chartPanel = new ChartPanel(this.chart); chartPanel.setFillZoomRectangle(true); chartPanel.setMouseZoomable(true, true); chartPanel.setRefreshBuffer(true); chartPanel.setDoubleBuffered(true); chartPanel.setVerticalAxisTrace(true); chartPanel.setHorizontalAxisTrace(true); chartPanel.addChartMouseListener(new ChartMouseListener() { public void chartMouseMoved(ChartMouseEvent e) { } public void chartMouseClicked(final ChartMouseEvent e) { CombinedDomainXYPlot combinedXYplot = (CombinedDomainXYPlot) e.getChart().getPlot(); @SuppressWarnings("unchecked") List<XYPlot> subplots = combinedXYplot.getSubplots(); if (e.getTrigger().getClickCount() == 2) { double xItem = 0; double yItem = 0; if (e.getEntity() instanceof XYItemEntity) { XYItemEntity xYItemEntity = ((XYItemEntity) e.getEntity()); xItem = xYItemEntity.getDataset().getXValue(xYItemEntity.getSeriesIndex(), xYItemEntity.getItem()); yItem = xYItemEntity.getDataset().getYValue(xYItemEntity.getSeriesIndex(), xYItemEntity.getItem()); } else { PlotEntity plotEntity = ((PlotEntity) e.getEntity()); XYPlot plot = (XYPlot) plotEntity.getPlot(); xItem = plot.getDomainCrosshairValue(); yItem = plot.getRangeCrosshairValue(); } for (XYPlot xyplot : subplots) { double x = xyplot.getDomainCrosshairValue(); double y = xyplot.getRangeCrosshairValue(); /* * If the cross hair is from a right-hand y axis we need * to convert this to a left-hand y axis. */ String rightAxisName = ", Price: "; double rangeLowerLeft = 0; double rangeUpperLeft = 0; double rangeLowerRight = 0; double rangeUpperRight = 0; double yRightLocation = 0; for (int index = 0; index < xyplot.getRangeAxisCount(); index++) { AxisLocation axisLocation = xyplot.getRangeAxisLocation(index); Range range = xyplot.getRangeAxis(index).getRange(); if (axisLocation.equals(AxisLocation.BOTTOM_OR_LEFT) || axisLocation.equals(AxisLocation.TOP_OR_LEFT)) { rangeLowerLeft = range.getLowerBound(); rangeUpperLeft = range.getUpperBound(); rightAxisName = ", " + xyplot.getRangeAxis(index).getLabel() + ": "; } if (y >= range.getLowerBound() && y <= range.getUpperBound() && (axisLocation.equals(AxisLocation.BOTTOM_OR_RIGHT) || axisLocation.equals(AxisLocation.TOP_OR_RIGHT))) { rangeUpperRight = range.getUpperBound(); rangeLowerRight = range.getLowerBound(); } } if ((rangeUpperRight - rangeLowerRight) > 0) { yRightLocation = rangeLowerLeft + ((rangeUpperLeft - rangeLowerLeft) * ((y - rangeLowerRight) / (rangeUpperRight - rangeLowerRight))); } else { yRightLocation = y; } String text = " Time: " + TradingCalendar.getFormattedDate( TradingCalendar.getZonedDateTimeFromMilli((long) (x)), TIME_FORMAT_SHORT) + rightAxisName + new Money(y); if (x == xItem && y == yItem) { titleLegend1.setText(text); if (null == clickCrossHairs) { clickCrossHairs = new XYTextAnnotation(text, x, yRightLocation); clickCrossHairs.setTextAnchor(TextAnchor.BOTTOM_LEFT); xyplot.addAnnotation(clickCrossHairs); } else { clickCrossHairs.setText(text); clickCrossHairs.setX(x); clickCrossHairs.setY(yRightLocation); } } } } else if (e.getTrigger().getClickCount() == 1 && null != clickCrossHairs) { for (XYPlot xyplot : subplots) { if (xyplot.removeAnnotation(clickCrossHairs)) { clickCrossHairs = null; titleLegend1.setText(" Time: 0, Price :0.0"); break; } } } } }); this.add(chartPanel, null); this.strategyData.getCandleDataset().getSeries(0).addChangeListener(this); } public void removeChart() { this.strategyData.getCandleDataset().getSeries(0).removeChangeListener(this); this.chart.getXYPlot().clearAnnotations(); this.chart.getXYPlot().clearDomainAxes(); this.chart.getXYPlot().clearDomainMarkers(); this.chart.getXYPlot().clearRangeAxes(); this.chart.getXYPlot().clearRangeMarkers(); } /** * Method getChart. * * @return JFreeChart */ public JFreeChart getChart() { return this.chart; } /** * Method createChart. * * @param strategyData * StrategyData * @param title * String * @return JFreeChart */ private JFreeChart createChart(StrategyData strategyData, String title, Tradingday tradingday) { DateAxis dateAxis = new DateAxis("Date"); dateAxis.setVerticalTickLabels(true); dateAxis.setDateFormatOverride(new SimpleDateFormat("dd/MM hh:mm")); dateAxis.setTickMarkPosition(DateTickMarkPosition.START); NumberAxis priceAxis = new NumberAxis("Price"); priceAxis.setAutoRange(true); priceAxis.setAutoRangeIncludesZero(false); XYPlot pricePlot = new XYPlot(strategyData.getCandleDataset(), dateAxis, priceAxis, strategyData.getCandleDataset().getRenderer()); pricePlot.setOrientation(PlotOrientation.VERTICAL); pricePlot.setDomainPannable(true); pricePlot.setRangePannable(true); pricePlot.setDomainCrosshairVisible(true); pricePlot.setDomainCrosshairLockedOnData(true); pricePlot.setRangeCrosshairVisible(true); pricePlot.setRangeCrosshairLockedOnData(true); pricePlot.setRangeGridlinePaint(new Color(204, 204, 204)); pricePlot.setDomainGridlinePaint(new Color(204, 204, 204)); pricePlot.setBackgroundPaint(Color.white); /* * Calculate the number of 15min segments in this trading day. i.e. * 6.5hrs/15min = 26 and there are a total of 96 = one day */ int segments15min = (int) (TradingCalendar.getDurationInSeconds(tradingday.getOpen(), tradingday.getClose()) / (60 * 15)); SegmentedTimeline segmentedTimeline = new SegmentedTimeline(SegmentedTimeline.FIFTEEN_MINUTE_SEGMENT_SIZE, segments15min, (96 - segments15min)); ZonedDateTime startDate = tradingday.getOpen(); ZonedDateTime endDate = tradingday.getClose(); if (!strategyData.getCandleDataset().getSeries(0).isEmpty()) { startDate = ((CandleItem) strategyData.getCandleDataset().getSeries(0).getDataItem(0)).getPeriod() .getStart(); startDate = TradingCalendar.getDateAtTime(startDate, tradingday.getOpen()); endDate = ((CandleItem) strategyData.getCandleDataset().getSeries(0) .getDataItem(strategyData.getCandleDataset().getSeries(0).getItemCount() - 1)).getPeriod() .getStart(); endDate = TradingCalendar.getDateAtTime(endDate, tradingday.getClose()); } segmentedTimeline.setStartTime(TradingCalendar.geMillisFromZonedDateTime(startDate)); segmentedTimeline.addExceptions(getNonTradingPeriods(startDate, endDate, tradingday.getOpen(), tradingday.getClose(), segmentedTimeline)); dateAxis.setTimeline(segmentedTimeline); // Build Combined Plot CombinedDomainXYPlot mainPlot = new CombinedDomainXYPlot(dateAxis); mainPlot.add(pricePlot, 4); int axixIndex = 0; int datasetIndex = 0; /* * Change the List of indicators so that the candle dataset is the first * one in the list. The main chart must be plotted first. */ List<IndicatorDataset> indicators = new ArrayList<IndicatorDataset>(0); for (IndicatorDataset item : strategyData.getIndicators()) { if (IndicatorSeries.CandleSeries.equals(item.getType(0))) { indicators.add(item); } } for (IndicatorDataset item : strategyData.getIndicators()) { if (!IndicatorSeries.CandleSeries.equals(item.getType(0))) { indicators.add(item); } } for (int i = 0; i < indicators.size(); i++) { IndicatorDataset indicator = indicators.get(i); if (indicator.getDisplaySeries(0)) { if (indicator.getSubChart(0)) { String axisName = "Price"; if (IndicatorSeries.CandleSeries.equals(indicator.getType(0))) { axisName = ((CandleSeries) indicator.getSeries(0)).getSymbol(); } else { org.trade.dictionary.valuetype.IndicatorSeries code = org.trade.dictionary.valuetype.IndicatorSeries .newInstance(indicator.getType(0)); axisName = code.getDisplayName(); } NumberAxis subPlotAxis = new NumberAxis(axisName); subPlotAxis.setAutoRange(true); subPlotAxis.setAutoRangeIncludesZero(false); XYPlot subPlot = new XYPlot((XYDataset) indicator, dateAxis, subPlotAxis, indicator.getRenderer()); subPlot.setOrientation(PlotOrientation.VERTICAL); subPlot.setDomainPannable(true); subPlot.setRangePannable(true); subPlot.setDomainCrosshairVisible(true); subPlot.setDomainCrosshairLockedOnData(true); subPlot.setRangeCrosshairVisible(true); subPlot.setRangeCrosshairLockedOnData(true); subPlot.setRangeGridlinePaint(new Color(204, 204, 204)); subPlot.setDomainGridlinePaint(new Color(204, 204, 204)); subPlot.setBackgroundPaint(Color.white); XYItemRenderer renderer = subPlot.getRendererForDataset((XYDataset) indicator); for (int seriesIndex = 0; seriesIndex < ((XYDataset) indicator).getSeriesCount(); seriesIndex++) { renderer.setSeriesPaint(seriesIndex, indicator.getSeriesColor(seriesIndex)); } mainPlot.add(subPlot, 1); } else { datasetIndex++; pricePlot.setDataset(datasetIndex, (XYDataset) indicator); if (IndicatorSeries.CandleSeries.equals(indicator.getType(0))) { // add secondary axis axixIndex++; final NumberAxis axis2 = new NumberAxis(((CandleSeries) indicator.getSeries(0)).getSymbol()); axis2.setAutoRange(true); axis2.setAutoRangeIncludesZero(false); pricePlot.setRangeAxis(datasetIndex, axis2); pricePlot.setRangeAxisLocation(i + 1, AxisLocation.BOTTOM_OR_RIGHT); pricePlot.mapDatasetToRangeAxis(datasetIndex, axixIndex); pricePlot.setRenderer(datasetIndex, new StandardXYItemRenderer()); } else { pricePlot.setRenderer(datasetIndex, indicator.getRenderer()); } XYItemRenderer renderer = pricePlot.getRendererForDataset((XYDataset) indicator); for (int seriesIndex = 0; seriesIndex < ((XYDataset) indicator).getSeriesCount(); seriesIndex++) { renderer.setSeriesPaint(seriesIndex, indicator.getSeriesColor(seriesIndex)); } } } } JFreeChart jfreechart = new JFreeChart(title, null, mainPlot, true); jfreechart.setAntiAlias(false); return jfreechart; } /** * Method seriesChanged. * * @param event * SeriesChangeEvent * @see org.jfree.data.general.SeriesChangeListener#seriesChanged(SeriesChangeEvent) */ public void seriesChanged(SeriesChangeEvent event) { Object series = event.getSource(); if (series instanceof CandleSeries) { CandleSeries candleSeries = (CandleSeries) series; if (!candleSeries.isEmpty()) { CombinedDomainXYPlot combinedXYplot = (CombinedDomainXYPlot) this.chart.getPlot(); @SuppressWarnings("unchecked") List<XYPlot> subplots = combinedXYplot.getSubplots(); XYPlot xyplot = subplots.get(0); CandleItem candleItem = (CandleItem) candleSeries.getDataItem(candleSeries.getItemCount() - 1); String msg = "Time: " + TradingCalendar.getFormattedDate(candleItem.getLastUpdateDate(), TIME_FORMAT) + " Open: " + new Money(candleItem.getOpen()) + " High: " + new Money(candleItem.getHigh()) + " Low: " + new Money(candleItem.getLow()) + " Close: " + new Money(candleItem.getClose()) + " Vwap: " + new Money(candleItem.getVwap()); titleLegend2.setText(msg); valueMarker.setValue(candleItem.getClose()); double x = TradingCalendar.geMillisFromZonedDateTime( TradingCalendar.getDateAtTime(candleItem.getPeriod().getStart(), candleSeries.getStartTime())); String annotationText = "(" + TradingCalendar.getFormattedDate(candleItem.getLastUpdateDate(), TIME_FORMAT) + ", " + new Money(candleItem.getClose()) + ")"; if (null == closePriceLine) { closePriceLine = new XYTextAnnotation(annotationText, x, candleItem.getY()); closePriceLine.setTextAnchor(TextAnchor.BOTTOM_RIGHT); xyplot.addAnnotation(closePriceLine); xyplot.addRangeMarker(valueMarker); } else { closePriceLine.setText(annotationText); closePriceLine.setX(x); closePriceLine.setY(candleItem.getY()); } this.chart.fireChartChanged(); } } } /** * Method addBuySellTradeArrow. * * @param action * String * @param price * Money * @param time * ZonedDateTime * @param quantity * Integer * @throws ValueTypeException */ public void addBuySellTradeArrow(String action, Money price, ZonedDateTime time, Integer quantity) throws ValueTypeException { String label = Action.newInstance(action) + " " + quantity + "@" + price; XYPointerAnnotation arrow = new XYPointerAnnotation(label, TradingCalendar.geMillisFromZonedDateTime(time), price.doubleValue(), 90d); arrow.setLabelOffset(5.0); arrow.setBackgroundPaint(Color.GREEN); if (action.equals(Action.SELL)) { arrow.setAngle(-90d); arrow.setBackgroundPaint(Color.RED); } CombinedDomainXYPlot combinedXYplot = (CombinedDomainXYPlot) this.chart.getPlot(); @SuppressWarnings("unchecked") List<XYPlot> subplots = combinedXYplot.getSubplots(); XYPlot xyplot = subplots.get(0); xyplot.addAnnotation(arrow); this.chart.fireChartChanged(); } /** * Method setNonTradingPeriods. * * @param start * ZonedDateTime * @param end * ZonedDateTime * * @param openDate * ZonedDateTime * * @param closeDate * ZonedDateTime * * @param segments15min * int * @return List<Date> */ private List<java.util.Date> getNonTradingPeriods(ZonedDateTime startDate, ZonedDateTime endDate, ZonedDateTime openDate, ZonedDateTime closeDate, SegmentedTimeline segmentedTimeline) { /* * Add all 15min periods that are not trading times. */ List<java.util.Date> noneTradingSegments = new ArrayList<>(); do { /* * 96 15min periods per day */ for (int j = 0; j < 96; j++) { ZonedDateTime segmentStartDate = TradingCalendar.getDateAtTime(startDate, openDate); segmentStartDate = segmentStartDate.plusMinutes(j * 15); if (!TradingCalendar.isTradingDay(segmentStartDate) || !TradingCalendar.isMarketHours(openDate, closeDate, segmentStartDate)) { Segment segment = segmentedTimeline .getSegment(TradingCalendar.geMillisFromZonedDateTime(segmentStartDate)); if (segment.inIncludeSegments()) { noneTradingSegments .add(new java.util.Date(TradingCalendar.geMillisFromZonedDateTime(segmentStartDate))); } } } startDate = startDate.plusDays(1); } while (endDate.isAfter(startDate)); return noneTradingSegments; } }