diff --git a/README.md b/README.md index 4b8490fac..e9379dc02 100644 --- a/README.md +++ b/README.md @@ -253,15 +253,19 @@ mvn exec:java - - + + - - + +
FinancialCandlestickSample
FinancialCandlestickSample.java (Several Themes Supported)
FinancialHiLowSample
FinancialHiLowSample.java (OHLC Renderer)
FinancialCandlestickSample
FinancialCandlestickSample.java (Several Themes Supported)
FinancialHiLowSample
FinancialHiLowSample.java (OHLC Renderer)
FinancialAdvancedCandlestickSample
FinancialAdvancedCandlestickSample.java (Advanced PaintBars and Extension Points)
FinancialAdvancedCandlestickSample
FinancialRealtimeCandlestickSample.java (OHLC Tick Replay Real-time processing)
FinancialAdvancedCandlestickSample
FinancialAdvancedCandlestickSample.java (Advanced PaintBars and Extension Points)
FinancialAdvancedCandlestickSample
FinancialRealtimeCandlestickSample.java (OHLC Tick Replay Real-time processing)
+#### Financial Footprint Chart + +
FinancialAdvancedCandlestickSample
FinancialRealtimeFootprintSample.java (FOOTPRINT Tick Replay Real-time processing)
+ ### Math- & Signal-Processing related examples The math samples can be started by running: diff --git a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/AbstractFinancialRenderer.java b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/AbstractFinancialRenderer.java index 5061b3830..d85bda633 100644 --- a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/AbstractFinancialRenderer.java +++ b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/AbstractFinancialRenderer.java @@ -95,7 +95,7 @@ protected double[] findAreaDistances(FindAreaDistances findAreaDistances, * @return the specific paint bar Paint */ protected Paint getPaintBarColor(OhlcvRendererEpData data) { - if (paintBarMarker != null) { + if (paintBarMarker != null && data != null) { return paintBarMarker.getPaintBy(data); } return null; diff --git a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/FootprintRenderer.java b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/FootprintRenderer.java new file mode 100644 index 000000000..40ed34ecc --- /dev/null +++ b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/FootprintRenderer.java @@ -0,0 +1,512 @@ +package de.gsi.chart.renderer.spi.financial; + +import static com.sun.javafx.scene.control.skin.Utils.computeTextWidth; + +import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.*; +import static de.gsi.chart.renderer.spi.financial.service.footprint.FootprintRendererAttributes.BID_ASK_VOLUME_FONTS; +import static de.gsi.dataset.DataSet.DIM_X; + +import java.security.InvalidParameterException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import javafx.collections.ObservableList; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.shape.StrokeLineCap; +import javafx.scene.shape.StrokeLineJoin; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; + +import com.sun.javafx.tk.FontLoader; +import com.sun.javafx.tk.FontMetrics; +import com.sun.javafx.tk.Toolkit; + +import de.gsi.chart.Chart; +import de.gsi.chart.XYChart; +import de.gsi.chart.axes.Axis; +import de.gsi.chart.axes.spi.CategoryAxis; +import de.gsi.chart.renderer.Renderer; +import de.gsi.chart.renderer.spi.financial.service.OhlcvRendererEpData; +import de.gsi.chart.renderer.spi.financial.service.RendererPaintAfterEP; +import de.gsi.chart.renderer.spi.financial.service.RendererPaintAfterEPAware; +import de.gsi.chart.renderer.spi.financial.service.footprint.FootprintRendererAttributes; +import de.gsi.chart.renderer.spi.financial.service.footprint.NbColumnColorGroup; +import de.gsi.chart.renderer.spi.financial.service.footprint.NbColumnColorGroup.FontColor; +import de.gsi.chart.renderer.spi.utils.DefaultRenderColorScheme; +import de.gsi.chart.utils.StyleParser; +import de.gsi.dataset.DataSet; +import de.gsi.dataset.spi.financial.api.attrs.AttributeModelAware; +import de.gsi.dataset.spi.financial.api.ohlcv.IOhlcvItem; +import de.gsi.dataset.spi.financial.api.ohlcv.IOhlcvItemAware; +import de.gsi.dataset.utils.ProcessingProfiler; + +/** + *

Footprint Chart Renderer

+ *

+ * Footprint chart is a type of candlestick chart that provides additional information, such as trade volume and order flow, + * in addition to price. It is multi-dimensional in nature, and can provide an investor with more information for analysis, + * beyond just the security's price. This tool is a unique offering that is gaining popularity amongst leading charting software providers. + *

+ * Footprint charts provide the benefit of analyzing multiple variables in a focused diagram. + * Common footprint charts include footprint profile, bid/ask footprint, delta footprint, and volume footprint. + *

+ * Bid/Ask Footprint: Adds color to the real-time volume, for easier visualization of buyers and sellers probing the bid or ask. + * With this footprint, traders can see whether the buyers or the sellers are the responsible parties, for influencing a price move. + *

+ * @see Footprint Charts Investopedia + * + * @author afischer + */ +@SuppressWarnings({ "PMD.ExcessiveMethodLength", "PMD.NPathComplexity", "PMD.ExcessiveParameterList" }) +// designated purpose of this class +public class FootprintRenderer extends AbstractFinancialRenderer implements Renderer, RendererPaintAfterEPAware { + private final static double FONT_RATIO = 13.0; + + private final boolean paintVolume; + private final boolean paintPoc; + private final boolean paintPullbackColumn; + private final FindAreaDistances findAreaDistances; + private final IFootprintRenderedAPI footprintRenderedApi; + private final FootprintRendererAttributes footprintAttrs; + private final FontLoader fontLoader; + + private AttributeModelAware attrs; + private IOhlcvItemAware itemAware; + private boolean isEpAvailable; + private Color pocColor; + private Color footprintDefaultFontColor; + private Color footprintCrossLineColor; + private Color footprintBoxLongColor; + private Color fooprintBoxShortColor; + private Color footprintVolumeLongColor; + private Color footprintVolumeShortColor; + private double[] distances; + private int iMin; + private int iMax; + private double localBarWidth; + private double barWidthHalf; + private double ratio; + private Font basicFont; + private Font selectedFont; + private double fontGap; + private double basicGap; + private float heightText; + + protected List paintAfterEPS = new ArrayList<>(); + + public FootprintRenderer(IFootprintRenderedAPI footprintRenderedApi, boolean paintVolume, boolean paintPoc, boolean paintPullbackColumn) { + this.footprintRenderedApi = footprintRenderedApi; + this.footprintAttrs = footprintRenderedApi.getFootprintAttributes(); + this.paintVolume = paintVolume; + this.paintPoc = paintPoc; + this.paintPullbackColumn = paintPullbackColumn; + this.findAreaDistances = paintVolume ? new XMinVolumeMaxAreaDistances() : new XMinAreaDistances(); + fontLoader = Toolkit.getToolkit().getFontLoader(); + } + + public FootprintRenderer(IFootprintRenderedAPI footprintRenderedApi) { + this(footprintRenderedApi, false, true, true); + } + + public boolean isPaintVolume() { + return paintVolume; + } + + public boolean isPaintPoc() { + return paintPoc; + } + + public boolean isPaintPullbackColumn() { + return paintPullbackColumn; + } + + @Override + public Canvas drawLegendSymbol(DataSet dataSet, int dsIndex, int width, int height) { + final Canvas canvas = new Canvas(width, height); + final GraphicsContext gc = canvas.getGraphicsContext2D(); + final String style = dataSet.getStyle(); + + gc.save(); + Color candleLongColor = StyleParser.getColorPropertyValue(style, DATASET_CANDLESTICK_LONG_COLOR, Color.GREEN); + Color candleShortColor = StyleParser.getColorPropertyValue(style, DATASET_CANDLESTICK_SHORT_COLOR, Color.RED); + + gc.setFill(candleLongColor); + gc.setStroke(candleLongColor); + gc.fillRect(1, 3, width / 2.0 - 2.0, height - 8.0); + double x = width / 4.0; + gc.strokeLine(x, 1, x, height - 2.0); + + gc.setFill(candleShortColor); + gc.setStroke(candleShortColor); + gc.fillRect(width / 2.0 + 2.0, 4, width - 2.0, height - 12.0); + x = 3.0 * width / 4.0 + 1.5; + gc.strokeLine(x, 1, x, height - 3.0); + gc.restore(); + + return canvas; + } + + @Override + protected FootprintRenderer getThis() { + return this; + } + + @Override + public List render(final GraphicsContext gc, final Chart chart, final int dataSetOffset, + final ObservableList datasets) { + if (!(chart instanceof XYChart)) { + throw new InvalidParameterException( + "must be derivative of XYChart for renderer - " + this.getClass().getSimpleName()); + } + final XYChart xyChart = (XYChart) chart; + + // make local copy and add renderer specific data sets + final List localDataSetList = new ArrayList<>(datasets); + localDataSetList.addAll(super.getDatasets()); + + long start = 0; + if (ProcessingProfiler.getDebugState()) { + start = ProcessingProfiler.getTimeStamp(); + } + + final Axis xAxis = xyChart.getXAxis(); + final Axis yAxis = xyChart.getYAxis(); + + final double xAxisWidth = xAxis.getWidth(); + final double xmin = xAxis.getValueForDisplay(0); + final double xmax = xAxis.getValueForDisplay(xAxisWidth); + int index = 0; + + for (final DataSet ds : localDataSetList) { + if (ds.getDimension() < 7) + continue; + final int lindex = index; + + ds.lock().readLockGuardOptimistic(() -> { + // update categories in case of category axes for the first (index == '0') indexed data set + if (lindex == 0 && xyChart.getXAxis() instanceof CategoryAxis) { + final CategoryAxis axis = (CategoryAxis) xyChart.getXAxis(); + axis.updateCategories(ds); + } + attrs = null; + if (ds instanceof AttributeModelAware) { + attrs = (AttributeModelAware) ds; + } + itemAware = (IOhlcvItemAware) ds; + isEpAvailable = !paintAfterEPS.isEmpty() || paintBarMarker != null; + + gc.save(); + // default styling level + String style = ds.getStyle(); + DefaultRenderColorScheme.setLineScheme(gc, style, lindex); + DefaultRenderColorScheme.setGraphicsContextAttributes(gc, style); + + // footprint settings + Font basicFontTemplate = footprintAttrs.getRequiredAttribute(BID_ASK_VOLUME_FONTS)[1]; + Font selectedFontTemplate = footprintAttrs.getRequiredAttribute(BID_ASK_VOLUME_FONTS)[2]; + + // financial styling level + pocColor = StyleParser.getColorPropertyValue(style, DATASET_FOOTPRINT_POC_COLOR, Color.rgb(255, 255, 0)); + footprintDefaultFontColor = StyleParser.getColorPropertyValue(style, DATASET_FOOTPRINT_DEFAULT_FONT_COLOR, Color.rgb(255, 255, 255, 0.58)); + footprintCrossLineColor = StyleParser.getColorPropertyValue(style, DATASET_FOOTPRINT_CROSS_LINE_COLOR, Color.GRAY); + footprintBoxLongColor = StyleParser.getColorPropertyValue(style, DATASET_FOOTPRINT_LONG_COLOR, Color.GREEN); + fooprintBoxShortColor = StyleParser.getColorPropertyValue(style, DATASET_FOOTPRINT_SHORT_COLOR, Color.RED); + footprintVolumeLongColor = StyleParser.getColorPropertyValue(style, DATASET_FOOTPRINT_VOLUME_LONG_COLOR, Color.rgb(139, 199, 194, 0.2)); + footprintVolumeShortColor = StyleParser.getColorPropertyValue(style, DATASET_FOOTPRINT_VOLUME_SHORT_COLOR, Color.rgb(235, 160, 159, 0.2)); + double barWidthPercent = StyleParser.getFloatingDecimalPropertyValue(style, DATASET_FOOTPRINT_BAR_WIDTH_PERCENTAGE, 0.5d); + double positionPaintMainRatio = StyleParser.getFloatingDecimalPropertyValue(style, DATASET_FOOTPRINT_PAINT_MAIN_RATIO, 5.157d); + + if (ds.getDataCount() > 0) { + iMin = ds.getIndex(DIM_X, xmin); + if (iMin < 0) + iMin = 0; + iMax = Math.min(ds.getIndex(DIM_X, xmax) + 1, ds.getDataCount()); + + distances = null; + double minRequiredWidth = 0.0; + if (lindex == 0) { + distances = findAreaDistances(findAreaDistances, ds, xAxis, yAxis, xmin, xmax); + minRequiredWidth = distances[0]; + } + localBarWidth = minRequiredWidth * barWidthPercent; + barWidthHalf = localBarWidth / 2.0; + ratio = Math.pow(localBarWidth, 0.25) * positionPaintMainRatio; + + // calculate ratio depended attributes + basicFont = getFontWithRatio(basicFontTemplate, ratio); + selectedFont = getFontWithRatio(selectedFontTemplate, ratio); + fontGap = getFontGap(5.0, ratio); + basicGap = getFontGap(1.0, ratio); + + FontMetrics metricsBasicFont = getFontMetrics(basicFont); + heightText = metricsBasicFont.getLeading() + metricsBasicFont.getAscent(); + + for (int i = iMin; i < iMax; i++) { + double x0 = xAxis.getDisplayPosition(ds.get(DIM_X, i)); + // get all additional information for footprints + IOhlcvItem ohlcvItem = itemAware.getItem(i); + IOhlcvItem lastOhlcvItem = itemAware.getLastItem(); + boolean isLastBar = lastOhlcvItem == null || lastOhlcvItem.getTimeStamp().equals(ohlcvItem.getTimeStamp()); + if (!footprintRenderedApi.isFootprintAvailable(ohlcvItem)) { + continue; + } + synchronized (footprintRenderedApi.getLock(ohlcvItem)) { + drawFootprintItem(gc, yAxis, ds, i, x0, ohlcvItem, isEpAvailable, isLastBar, paintVolume); + + if (isLastBar && paintPullbackColumn) { + IOhlcvItem pullbackColumn = footprintRenderedApi.getPullbackColumn(ohlcvItem); + if (pullbackColumn != null) { + x0 = x0 + localBarWidth + barWidthHalf; + drawFootprintItem(gc, yAxis, ds, i, x0, pullbackColumn, false, true, false); + } + } + } + } + } + gc.restore(); + }); + // possibility to re-arrange y-axis by min/max of dataset (after paint) + if (computeLocalRange()) { + applyLocalYRange(ds, yAxis, xmin, xmax); + } + index++; + } + if (ProcessingProfiler.getDebugState()) { + ProcessingProfiler.getTimeDiff(start); + } + + return localDataSetList; + } + + private void drawFootprintItem(GraphicsContext gc, Axis yAxis, DataSet ds, int i, + double x0, IOhlcvItem ohlcvItem, boolean isEpAvailable, boolean isLastBar, boolean paintVolume) { + double yOpen = yAxis.getDisplayPosition(ohlcvItem.getOpen()); + double yHigh = yAxis.getDisplayPosition(ohlcvItem.getHigh()); + double yLow = yAxis.getDisplayPosition(ohlcvItem.getLow()); + double yClose = yAxis.getDisplayPosition(ohlcvItem.getClose()); + double open = ohlcvItem.getOpen(); + double close = ohlcvItem.getClose(); + + // call api + Collection priceVolumeList = footprintRenderedApi.getPriceVolumeList(ohlcvItem); + double pocPrice = footprintRenderedApi.getPocPrice(ohlcvItem); + NbColumnColorGroup resultColorGroups = footprintRenderedApi.getColumnColorGroup(ohlcvItem); + + double yDiff = yOpen - yClose; + double yMin = yDiff > 0 ? yClose : yOpen; + + // prepare extension point data (if EPs available) + OhlcvRendererEpData data = null; + if (isEpAvailable) { + data = new OhlcvRendererEpData(); + data.gc = gc; + data.ds = ds; + data.attrs = attrs; + data.ohlcvItemAware = itemAware; + data.ohlcvItem = ohlcvItem; + data.index = i; + data.minIndex = iMin; + data.maxIndex = iMax; + data.barWidth = localBarWidth; + data.barWidthHalf = barWidthHalf; + data.xCenter = x0; + data.yOpen = yOpen; + data.yHigh = yHigh; + data.yLow = yLow; + data.yClose = yClose; + data.yDiff = yDiff; + data.yMin = yMin; + } + + // paint volume + if (paintVolume) { + assert distances != null; + paintVolume(gc, ds, i, footprintVolumeLongColor, footprintVolumeShortColor, yAxis, distances, localBarWidth, barWidthHalf, x0); + } + + // choose color of the bar boxes (left part of the footprint) + Paint barPaint = null; + if (data != null) { + barPaint = getPaintBarColor(data); + } + + // draw footprint chart + // draw cross-line + gc.setStroke(footprintCrossLineColor); + gc.strokeLine(x0, yHigh - heightText / 2.0, x0, yLow + heightText / 2.0); + + // draw bid-ask rows + double maxWidthTextBid = -Double.MAX_VALUE; + for (Double[] priceVolume : priceVolumeList) { + double price = priceVolume[0]; + double bidVolume = priceVolume[1]; + double askVolume = priceVolume[2]; + boolean isLastBarAndLastPrice = isLastBar && price == close; + + double widthTextBidBasic = computeTextWidth(basicFont, getFormattedVolume(bidVolume), 0); + double widthTextBidSelected = computeTextWidth(selectedFont, getFormattedVolume(bidVolume), 0); + double widthTextAskBasic = computeTextWidth(basicFont, getFormattedVolume(askVolume), 0); + double widthTextAskSelected = computeTextWidth(selectedFont, getFormattedVolume(askVolume), 0); + double widthTextBid = isLastBarAndLastPrice ? widthTextBidSelected : widthTextBidBasic; + double widthTextAsk = isLastBarAndLastPrice ? widthTextAskSelected : widthTextAskBasic; + + if (widthTextBidBasic > maxWidthTextBid) + maxWidthTextBid = widthTextBidBasic; + double xxBid = x0 - widthTextBid - fontGap; + double xxAsk = x0 + fontGap; + double bidAskVolumeY = yAxis.getDisplayPosition(price) + heightText / 2.0; // center of text to price value + + // paint POC rectangle + if (paintPoc && price == pocPrice) { + gc.setStroke(pocColor); + gc.setLineCap(StrokeLineCap.BUTT); + gc.setLineJoin(StrokeLineJoin.MITER); + gc.setMiterLimit(10.0f); + gc.setLineWidth(1.5f); + gc.strokeRect(x0 - widthTextBid - fontGap - 2.0 * basicGap, + bidAskVolumeY - heightText - basicGap, + widthTextBid + widthTextAsk + 2.0 * fontGap + 2.0 * basicGap, + heightText + 4.0 * basicGap); + } + // paint area bid/ask text description + if (resultColorGroups != null) { + // color and font palette of numbers bars + FontColor fontColor = resultColorGroups.fontColorMap.get(price); + gc.setFont(isLastBarAndLastPrice ? selectedFont : fontColor.bidFont); + gc.setFont(new Font(calcFontSize(gc.getFont().getSize(), ratio))); + gc.setFill(fontColor.bidColor); + gc.fillText(getFormattedVolume(bidVolume), xxBid, bidAskVolumeY); + gc.setFont(isLastBarAndLastPrice ? selectedFont : fontColor.askFont); + gc.setFont(new Font(calcFontSize(gc.getFont().getSize(), ratio))); + gc.setFill(fontColor.askColor); + gc.fillText(getFormattedVolume(askVolume), xxAsk, bidAskVolumeY); + + } else { + gc.setFont(isLastBarAndLastPrice ? selectedFont : basicFont); + gc.setFill(footprintDefaultFontColor); + gc.fillText(getFormattedVolume(bidVolume), xxBid, bidAskVolumeY); + gc.fillText(getFormattedVolume(askVolume), xxAsk, bidAskVolumeY); + } + } // for + + // paint body box indicator + for (Double[] priceVolume : priceVolumeList) { + double price = priceVolume[0]; + double bidAskVolumeY = yAxis.getDisplayPosition(price) + heightText / 2.0; + if ((close > open && price >= open && price <= close) || (close <= open && price <= open && price >= close)) { + gc.setLineWidth(1.0f); + if (close > open) { + if (barPaint != null) { + gc.setFill(barPaint); + + } else { + gc.setFill(footprintBoxLongColor); + } + } else { + if (barPaint != null) { + gc.setFill(barPaint); + + } else { + gc.setFill(fooprintBoxShortColor); + } + } + gc.fillRect(x0 - maxWidthTextBid - fontGap - 10.0 * basicGap, + bidAskVolumeY - heightText, 4.0 * basicGap, heightText); + } + } + + // extension point - paint after footprint painting + if (isEpAvailable) { + // renderer EP extension data + EpDataAddon epDataAddon = new EpDataAddon(); + epDataAddon.basicGap = basicGap; + epDataAddon.fontGap = fontGap; + epDataAddon.heightText = heightText; + epDataAddon.maxWidthTextBid = maxWidthTextBid; + data.addon = epDataAddon; + + paintAfter(data); + } + } + + //-------------- helpers ------------------ + + private String getFormattedVolume(double askVolume) { + return String.format("%1.0f", askVolume); + } + + private Font getFontWithRatio(Font fontTemplate, double ratio) { + return Font.font(fontTemplate.getFamily(), FontWeight.findByName(fontTemplate.getStyle()), + calcFontSize(fontTemplate.getSize(), ratio)); + } + + private double calcFontSize(double size, double ratio) { + return size / FONT_RATIO * ratio; + } + + private double getFontGap(double gap, double ratio) { + return gap / FONT_RATIO * ratio; + } + + private FontMetrics getFontMetrics(Font font) { + return fontLoader.getFontMetrics(font); + } + + /** + * Handle extension point PaintAfter + * + * @param data filled domain object which is provided to external extension points. + */ + protected void paintAfter(OhlcvRendererEpData data) { + for (RendererPaintAfterEP paintAfterEP : paintAfterEPS) { + paintAfterEP.paintAfter(data); + } + } + + //-------------- API ------------------ + + /** + * API Footprint Service + * Service provides additional footprint data for each ohlcv item which has to be painted. + */ + public interface IFootprintRenderedAPI { + // Check if the footprint is available for this OHLCV item data + boolean isFootprintAvailable(IOhlcvItem ohlcvItem); + // Footprint configuration attributes + FootprintRendererAttributes getFootprintAttributes(); + // list of price, ask, bid values per row + Collection getPriceVolumeList(IOhlcvItem ohlcvItem); + // get POC price (Point of control) + double getPocPrice(IOhlcvItem ohlcvItem); + // column font and colors for each NP value + NbColumnColorGroup getColumnColorGroup(IOhlcvItem ohlcvItem); + // try get pullback column (if the feature is active) + IOhlcvItem getPullbackColumn(IOhlcvItem ohlcvItem); + // get lock for synch between data consolidation and painting process + Object getLock(IOhlcvItem ohlcvItem); + } + + // painting additional data for extension points + public static class EpDataAddon { + public double heightText; // height of the row + public double fontGap; // font gap from cross line to ask/bid number + public double basicGap; // basic smallest gap for spacing (calculated with ratio) + public double maxWidthTextBid; // maximal text with for bid number (left side of bar) + } + + //-------------- injections -------------------------------------------- + + @Override + public void addPaintAfterEp(RendererPaintAfterEP paintAfterEP) { + paintAfterEPS.add(paintAfterEP); + } + + @Override + public List getPaintAfterEps() { + return paintAfterEPS; + } +} diff --git a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/css/FinancialColorSchemeConfig.java b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/css/FinancialColorSchemeConfig.java index 74c8eef55..3e2a8fbda 100644 --- a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/css/FinancialColorSchemeConfig.java +++ b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/css/FinancialColorSchemeConfig.java @@ -14,6 +14,7 @@ import de.gsi.chart.axes.spi.AbstractAxisParameter; import de.gsi.chart.renderer.Renderer; import de.gsi.chart.renderer.spi.financial.CandleStickRenderer; +import de.gsi.chart.renderer.spi.financial.FootprintRenderer; import de.gsi.chart.renderer.spi.financial.HighLowRenderer; import de.gsi.chart.renderer.spi.financial.PositionFinancialRendererPaintAfterEP; import de.gsi.chart.renderer.spi.financial.service.DataSetAware; @@ -86,6 +87,33 @@ else if (renderer instanceof HighLowRenderer) { throw new IllegalArgumentException("HighLowRenderer: Not implemented yet. ColorScheme=" + theme); } } + // driven-by FootprintRenderer + else if (renderer instanceof FootprintRenderer) { + switch (theme) { + case CLASSIC: + dataSet.setStyle("footprintLongColor=green; footprintShortColor=red; footprintCrossLineColor=grey; footprintDefaultFontColor=rgba(255,255,255,0.58); footprintPocColor=#d1d100; " + + "footprintVolumeLongColor=rgba(139,199,194,0.4); footprintVolumeShortColor=rgba(235,160,159,0.4)"); + break; + case CLEARLOOK: + dataSet.setStyle("footprintLongColor=#4c4c4c; footprintShortColor=red; footprintCrossLineColor=grey; footprintDefaultFontColor=rgba(255,255,255,0.58); footprintPocColor=#d1d100; " + + "footprintVolumeLongColor=rgba(139,199,194,0.4); footprintVolumeShortColor=rgba(235,160,159,0.4)"); + break; + case SAND: + dataSet.setStyle("footprintLongColor=#00aa00; footprintShortColor=red; footprintCrossLineColor=black; footprintDefaultFontColor=rgba(255,255,255,0.58); footprintPocColor=#d1d100; " + + "candleShadowColor=rgba(72,72,72,0.2); footprintVolumeLongColor=rgba(139,199,194,0.4); footprintVolumeShortColor=rgba(235,160,159,0.4)"); + break; + case BLACKBERRY: + dataSet.setStyle("footprintLongColor=#00022e; footprintShortColor=#780000; footprintCrossLineColor=grey; footprintDefaultFontColor=rgba(255,255,255,0.58); footprintPocColor=yellow; " + + "candleLongWickColor=white; candleShortWickColor=red; footprintVolumeLongColor=rgba(139,199,194,0.4); footprintVolumeShortColor=rgba(235,160,159,0.4)"); + break; + case DARK: + dataSet.setStyle("footprintLongColor=#298988; footprintShortColor=#963838; footprintCrossLineColor=grey; footprintDefaultFontColor=rgba(255,255,255,0.58); footprintPocColor=yellow; " + + "footprintVolumeLongColor=rgba(139,199,194,0.4); footprintVolumeShortColor=rgba(235,160,159,0.4)"); + break; + default: + throw new IllegalArgumentException("FootprintRenderer: Not implemented yet. ColorScheme=" + theme); + } + } // extension points configuration support if (renderer instanceof RendererPaintAfterEPAware) { @@ -203,8 +231,6 @@ public void applyTo(String theme, String customColorScheme, XYChart chart) throw ((AbstractAxisParameter) chart.getYAxis()).setTickLabelFill(Color.rgb(194, 194, 194)); } break; - default: - throw new IllegalArgumentException("Theme is not implemented yet. Theme=" + theme); } } diff --git a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/css/FinancialCss.java b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/css/FinancialCss.java index 1af47e0a3..800733701 100644 --- a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/css/FinancialCss.java +++ b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/css/FinancialCss.java @@ -180,6 +180,53 @@ public class FinancialCss { // NOPMD decide not to rename it for the time being */ public static final String DATASET_POSITION_ORDER_LINKAGE_LINE_WIDTH = "positionOrderLinkageLineWidth"; + // FOOTPRINT ---------------------------------------------------------- + + /** + * Footprint bar relative width against actual scaled view. Defined in percentage range: {@literal <}0.0, 1.0{@literal >} + */ + public static final String DATASET_FOOTPRINT_BAR_WIDTH_PERCENTAGE = "footprintBarWidthPercent"; + + /** + * Footprint renderer the main ratio for resizing of the final footprint bar paint + */ + public static final String DATASET_FOOTPRINT_PAINT_MAIN_RATIO = "footprintPaintMainRatio"; + + /** + * The footprint candle boxes color for candle's upstick + */ + public static final String DATASET_FOOTPRINT_LONG_COLOR = "footprintLongColor"; + + /** + * The footprint candle boxed color for candle's downstick + */ + public static final String DATASET_FOOTPRINT_SHORT_COLOR = "footprintShortColor"; + + /** + * Volume Long bars with this defined color and transparency, if paintVolume=true, the volume bars are painted. + */ + public static final String DATASET_FOOTPRINT_VOLUME_LONG_COLOR = "footprintVolumeLongColor"; + + /** + * Volume Short bars with this defined color and transparency, if paintVolume=true, the volume bars are painted. + */ + public static final String DATASET_FOOTPRINT_VOLUME_SHORT_COLOR = "footprintVolumeShortColor"; + + /** + * Footprint division line between bid and ask numbers (cross-line vertical) + */ + public static final String DATASET_FOOTPRINT_CROSS_LINE_COLOR = "footprintCrossLineColor"; + + /** + * Footprint default font color. If the column color grouping is disabled, this color is taken. + */ + public static final String DATASET_FOOTPRINT_DEFAULT_FONT_COLOR = "footprintDefaultFontColor"; + + /** + * Footprint POC color. POC = Point of control. + */ + public static final String DATASET_FOOTPRINT_POC_COLOR = "footprintPocColor"; + private FinancialCss() { } } diff --git a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/OhlcvRendererEpData.java b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/OhlcvRendererEpData.java index 523220332..6027028dc 100644 --- a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/OhlcvRendererEpData.java +++ b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/OhlcvRendererEpData.java @@ -28,4 +28,5 @@ public class OhlcvRendererEpData { public double yClose; // close in display coords public double yDiff; // diff = open - close public double yMin; // minimal y coord of bar + public Object addon; // addon defined by specific renderer } diff --git a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/footprint/FootprintRendererAttributes.java b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/footprint/FootprintRendererAttributes.java new file mode 100644 index 000000000..47455f8c1 --- /dev/null +++ b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/footprint/FootprintRendererAttributes.java @@ -0,0 +1,112 @@ +package de.gsi.chart.renderer.spi.financial.service.footprint; + +import static de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConstants.*; + +import javafx.scene.paint.Color; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; + +import de.gsi.dataset.spi.financial.api.attrs.AttributeKey; +import de.gsi.dataset.spi.financial.api.attrs.AttributeModel; + +public class FootprintRendererAttributes extends AttributeModel { + /** Column coloring group feature active, default true */ + public static final AttributeKey COLUMN_COLORING_FEATURE_ACTIVE = AttributeKey.create(Boolean.class, "COLUMN_COLORING_FEATURE_ACTIVE"); + + /** Draw pullback column, default true */ + public static final AttributeKey DRAW_PULLBACK_COLUMN = AttributeKey.create(Boolean.class, "DRAW_PULLBACK_COLUMN"); + + /** Draw rectangle of POC of each bar, default true */ + public static final AttributeKey DRAW_POC_RECTANGLE_OF_EACH_BAR = AttributeKey.create(Boolean.class, "DRAW_POC_RECTANGLE_OF_EACH_BAR"); + + /** + * Column color group settings: + * 1st column bid [0, 1, 2, 3] groups + * 2nd column ask [0, 1, 2, 3] groups + */ + public static final AttributeKey COLUMN_COLOR_GROUP_SETTINGS = AttributeKey.create(Color[][].class, "COLUMN_COLOR_GROUP_SETTINGS"); + + /** + * Column color group thresholds: + * three thresholds for calculation of column color group choosing process + */ + public static final AttributeKey COLUMN_COLOR_GROUP_THRESHOLDS = AttributeKey.create(Double[].class, "COLUMN_COLOR_GROUP_THRESHOLDS"); + + /** Bolding/Plain font bid/ask under defined threshold, 0 means disabled, default 30 */ + public static final AttributeKey BID_ASK_BOLD_THRESHOLD = AttributeKey.create(Double.class, "BID_ASK_BOLD_THRESHOLD"); + + /** + * Bid/Ask volume fonts: + * 0 - plain normal font, number is less BID_ASK_BOLD_THRESHOLD + * 1 - bold normal font, number is higher or equal than BID_ASK_BOLD_THRESHOLD + * 2 - bold big font, the number is last bar and last price + */ + public static final AttributeKey BID_ASK_VOLUME_FONTS = AttributeKey.create(Font[].class, "BID_ASK_VOLUME_FONTS"); + + /** + * Configure Footprint by default values. Good practise is create these defaults and apply your changes to this instance by direct call setAttribute method. + * + * @param scheme the coloring scheme + * @return define default values + */ + public static FootprintRendererAttributes getDefaultValues(String scheme) { + FootprintRendererAttributes model = new FootprintRendererAttributes(); + + model.setAttribute(COLUMN_COLORING_FEATURE_ACTIVE, true); + + model.setAttribute(DRAW_POC_RECTANGLE_OF_EACH_BAR, true); + + model.setAttribute(DRAW_PULLBACK_COLUMN, true); + + model.setAttribute(COLUMN_COLOR_GROUP_THRESHOLDS, new Double[] { 40.0d, 100.0d, 150.0d }); + + model.setAttribute(BID_ASK_BOLD_THRESHOLD, 30.0d); + + model.setAttribute(BID_ASK_VOLUME_FONTS, new Font[] { + Font.font("Segoe UI", FontWeight.NORMAL, 13), // plain normal font, number is less BID_ASK_BOLD_THRESHOLD + Font.font("Segoe UI", FontWeight.BOLD, 12), // bold normal font, number is higher or equal than BID_ASK_BOLD_THRESHOLD + Font.font("Segoe UI", FontWeight.BOLD, 15) // bold big font, the number is last bar and last price + }); + + Color[][] columnColorGroupSettings; + switch (scheme) { + case SAND: + case CLASSIC: + case CLEARLOOK: + columnColorGroupSettings = new Color[][] { + { + Color.rgb(0, 128, 255), // RANGE 0 BID COLOR, color: light blue + Color.rgb(128, 128, 128), // RANGE 1 BID COLOR, color: white + Color.rgb(255, 128, 192), // RANGE 2 BID COLOR, color: pink + Color.rgb(242, 0, 0) // RANGE 3 BID COLOR, color: red + }, + { + Color.rgb(0, 128, 255), // RANGE 0 ASK COLOR, color: light blue + Color.rgb(128, 128, 128), // RANGE 1 ASK COLOR, color: white + Color.rgb(124, 190, 190), // RANGE 2 ASK COLOR, color: light green + Color.rgb(0, 128, 0) // RANGE 3 ASK COLOR, color: green + } + }; + break; + default: + columnColorGroupSettings = new Color[][] { + { + Color.rgb(0, 128, 255), // RANGE 0 BID COLOR, color: light blue + Color.rgb(255, 255, 255), // RANGE 1 BID COLOR, color: white + Color.rgb(255, 128, 192), // RANGE 2 BID COLOR, color: pink + Color.rgb(242, 0, 0) // RANGE 3 BID COLOR, color: red + }, + { + Color.rgb(0, 128, 255), // RANGE 0 ASK COLOR, color: light blue + Color.rgb(255, 255, 255), // RANGE 1 ASK COLOR, color: white + Color.rgb(124, 190, 190), // RANGE 2 ASK COLOR, color: light green + Color.rgb(0, 128, 0) // RANGE 3 ASK COLOR, color: green + } + }; + break; + } + model.setAttribute(COLUMN_COLOR_GROUP_SETTINGS, columnColorGroupSettings); + + return model; + } +} diff --git a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/footprint/NbColumnColorGroup.java b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/footprint/NbColumnColorGroup.java new file mode 100644 index 000000000..b6e684360 --- /dev/null +++ b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/footprint/NbColumnColorGroup.java @@ -0,0 +1,29 @@ +package de.gsi.chart.renderer.spi.financial.service.footprint; + +import java.util.HashMap; +import java.util.Map; + +import javafx.scene.paint.Color; +import javafx.scene.text.Font; + +/** + * The domain object provides data for footprint column colors. + * Defines specific colors for each footprint lines by column color services. + */ +public class NbColumnColorGroup { + public Map fontColorMap = new HashMap<>(); + + public static class FontColor { + public final Font bidFont; + public final Color bidColor; + public final Font askFont; + public final Color askColor; + + public FontColor(Font bidFont, Color bidColor, Font askFont, Color askColor) { + this.bidFont = bidFont; + this.bidColor = bidColor; + this.askFont = askFont; + this.askColor = askColor; + } + } +} diff --git a/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/CandleStickRendererTest.java b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/CandleStickRendererTest.java index 55501c2da..b39e3a1cc 100644 --- a/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/CandleStickRendererTest.java +++ b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/CandleStickRendererTest.java @@ -8,6 +8,7 @@ import java.util.Calendar; import javafx.scene.Scene; +import javafx.scene.canvas.Canvas; import javafx.scene.paint.Color; import javafx.stage.Stage; @@ -22,6 +23,7 @@ import de.gsi.chart.axes.spi.CategoryAxis; import de.gsi.chart.axes.spi.DefaultNumericAxis; import de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConfig; +import de.gsi.chart.renderer.spi.financial.service.OhlcvRendererEpData; import de.gsi.chart.renderer.spi.financial.utils.CalendarUtils; import de.gsi.chart.renderer.spi.financial.utils.FinancialTestUtils; import de.gsi.chart.renderer.spi.financial.utils.FinancialTestUtils.TestChart; @@ -49,7 +51,7 @@ public void start(Stage stage) throws Exception { rendererTested.setComputeLocalRange(false); rendererTested.setComputeLocalRange(true); - assertNull(rendererTested.getPaintBarColor(null)); + assertNull(rendererTested.getPaintBarColor(new OhlcvRendererEpData())); final DefaultNumericAxis xAxis = new DefaultNumericAxis("time", "iso"); xAxis.setTimeAxis(true); @@ -134,7 +136,7 @@ public void testVolumeConstructor() { @Test public void noXyChartInstance() { - assertThrows(InvalidParameterException.class, () -> rendererTested.render(null, new TestChart(), 0, null)); + assertThrows(InvalidParameterException.class, () -> rendererTested.render(new Canvas(300, 200).getGraphicsContext2D(), new TestChart(), 0, null)); } @Test diff --git a/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/FootprintRendererTest.java b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/FootprintRendererTest.java new file mode 100644 index 000000000..582fe80b9 --- /dev/null +++ b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/FootprintRendererTest.java @@ -0,0 +1,182 @@ +package de.gsi.chart.renderer.spi.financial; + +import static org.junit.jupiter.api.Assertions.*; + +import static de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConstants.*; + +import java.security.InvalidParameterException; +import java.util.Calendar; + +import javafx.scene.Scene; +import javafx.scene.canvas.Canvas; +import javafx.scene.paint.Color; +import javafx.stage.Stage; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.opentest4j.AssertionFailedError; +import org.testfx.framework.junit5.ApplicationExtension; +import org.testfx.framework.junit5.Start; + +import de.gsi.chart.XYChart; +import de.gsi.chart.axes.AxisLabelOverlapPolicy; +import de.gsi.chart.axes.spi.CategoryAxis; +import de.gsi.chart.axes.spi.DefaultNumericAxis; +import de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConfig; +import de.gsi.chart.renderer.spi.financial.service.OhlcvRendererEpData; +import de.gsi.chart.renderer.spi.financial.service.footprint.FootprintRendererAttributes; +import de.gsi.chart.renderer.spi.financial.utils.CalendarUtils; +import de.gsi.chart.renderer.spi.financial.utils.FinancialTestUtils; +import de.gsi.chart.renderer.spi.financial.utils.FinancialTestUtils.TestChart; +import de.gsi.chart.renderer.spi.financial.utils.FootprintRenderedAPIDummyAdapter; +import de.gsi.chart.renderer.spi.financial.utils.Interval; +import de.gsi.chart.ui.utils.JavaFXInterceptorUtils.SelectiveJavaFxInterceptor; +import de.gsi.chart.ui.utils.TestFx; +import de.gsi.dataset.DataSet; +import de.gsi.dataset.spi.AbstractDataSet; +import de.gsi.dataset.spi.financial.OhlcvDataSet; +import de.gsi.dataset.utils.ProcessingProfiler; + +@ExtendWith(ApplicationExtension.class) +@ExtendWith(SelectiveJavaFxInterceptor.class) +public class FootprintRendererTest { + private FootprintRenderer rendererTested; + private XYChart chart; + private OhlcvDataSet ohlcvDataSet; + private final String[] schemes = getDefaultColorSchemes(); + + @Start + public void start(Stage stage) throws Exception { + for (String scheme : schemes) { + financialComponentTest(stage, scheme); + } + } + + private void financialComponentTest(Stage stage, String scheme) throws Exception { + ProcessingProfiler.setDebugState(false); // enable for detailed renderer tracing + ohlcvDataSet = new OhlcvDataSet("ohlc1"); + ohlcvDataSet.setData(FinancialTestUtils.createTestOhlcv()); + FootprintRendererAttributes footprintAttrs = FootprintRendererAttributes.getDefaultValues(scheme); + rendererTested = new FootprintRenderer( + new FootprintRenderedAPIDummyAdapter(footprintAttrs), + true, + true, + true); + rendererTested.setComputeLocalRange(false); + rendererTested.setComputeLocalRange(true); + + assertNull(rendererTested.getPaintBarColor(new OhlcvRendererEpData())); + + final DefaultNumericAxis xAxis = new DefaultNumericAxis("time", "iso"); + xAxis.setTimeAxis(true); + xAxis.setAutoRangeRounding(false); + xAxis.setAutoRanging(false); + Interval xrange = CalendarUtils.createByDateInterval("2020/11/18-2020/11/25"); + xAxis.set(xrange.from.getTime().getTime() / 1000.0, xrange.to.getTime().getTime() / 1000.0); + + final DefaultNumericAxis yAxis = new DefaultNumericAxis("price", "points"); + yAxis.setAutoRanging(false); + + // prepare chart structure + chart = new XYChart(xAxis, yAxis); + chart.getGridRenderer().setDrawOnTop(false); + + rendererTested.getDatasets().add(ohlcvDataSet); + chart.getRenderers().clear(); + chart.getRenderers().add(rendererTested); + + // PaintBar extension usage + rendererTested.setPaintBarMarker(d -> d.ohlcvItem != null ? Math.abs(d.ohlcvItem.getOpen() - d.ohlcvItem.getClose()) > 2.0 ? Color.MAGENTA : null : null); + + // Extension point usage + rendererTested.addPaintAfterEp(data -> assertNotNull(data.gc)); + assertEquals(1, rendererTested.getPaintAfterEps().size()); + + new FinancialColorSchemeConfig().applyTo(scheme, chart); + + stage.setScene(new Scene(chart, 800, 600)); + stage.show(); + } + + @TestFx + public void categoryAxisTest() { + final CategoryAxis xAxis = new CategoryAxis("time [iso]"); + xAxis.setTickLabelRotation(90); + xAxis.setOverlapPolicy(AxisLabelOverlapPolicy.SKIP_ALT); + ohlcvDataSet.setCategoryBased(true); + + chart.getAxes().add(0, xAxis); + chart.layoutChildren(); + } + + @TestFx + public void checkMinimalDimRequired() { + rendererTested.getDatasets().clear(); + rendererTested.getDatasets().add(new AbstractDataSet("testDim", 6) { + @Override + public double get(int dimIndex, int index) { + return 0; + } + + @Override + public int getDataCount() { + return 1; + } + + @Override + public DataSet set(DataSet other, boolean copy) { + return null; + } + }); + var ref = new Object() { + AssertionFailedError e = null; + }; + rendererTested.addPaintAfterEp(data -> ref.e = new AssertionFailedError("The renderer method cannot be call, because dimensions are lower as required!")); + chart.layoutChildren(); + if (ref.e != null) { + throw ref.e; + } + } + + @Test + public void testShortConstructor() { + FootprintRendererAttributes footprintAttrs = FootprintRendererAttributes.getDefaultValues(schemes[0]); + FootprintRenderer renderer = new FootprintRenderer( + new FootprintRenderedAPIDummyAdapter(footprintAttrs)); + assertFalse(renderer.isPaintVolume()); + assertTrue(renderer.isPaintPoc()); + assertTrue(renderer.isPaintPullbackColumn()); + } + + @Test + public void testLongConstructor() { + FootprintRendererAttributes footprintAttrs = FootprintRendererAttributes.getDefaultValues(schemes[0]); + FootprintRenderer renderer = new FootprintRenderer( + new FootprintRenderedAPIDummyAdapter(footprintAttrs), + true, + true, + true); + assertTrue(renderer.isPaintVolume()); + assertTrue(renderer.isPaintPoc()); + assertTrue(renderer.isPaintPullbackColumn()); + + renderer = new FootprintRenderer( + new FootprintRenderedAPIDummyAdapter(footprintAttrs), + false, + false, + false); + assertFalse(renderer.isPaintVolume()); + assertFalse(renderer.isPaintPoc()); + assertFalse(renderer.isPaintPullbackColumn()); + } + + @Test + public void noXyChartInstance() { + assertThrows(InvalidParameterException.class, () -> rendererTested.render(new Canvas(300, 200).getGraphicsContext2D(), new TestChart(), 0, null)); + } + + @Test + void getThis() { + assertEquals(FootprintRenderer.class, rendererTested.getThis().getClass()); + } +} diff --git a/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/HighLowRendererTest.java b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/HighLowRendererTest.java index 526b6194c..2bdd48a52 100644 --- a/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/HighLowRendererTest.java +++ b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/HighLowRendererTest.java @@ -8,6 +8,7 @@ import java.util.Calendar; import javafx.scene.Scene; +import javafx.scene.canvas.Canvas; import javafx.scene.paint.Color; import javafx.stage.Stage; @@ -22,6 +23,7 @@ import de.gsi.chart.axes.spi.CategoryAxis; import de.gsi.chart.axes.spi.DefaultNumericAxis; import de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConfig; +import de.gsi.chart.renderer.spi.financial.service.OhlcvRendererEpData; import de.gsi.chart.renderer.spi.financial.utils.CalendarUtils; import de.gsi.chart.renderer.spi.financial.utils.FinancialTestUtils; import de.gsi.chart.renderer.spi.financial.utils.FinancialTestUtils.TestChart; @@ -49,7 +51,7 @@ public void start(Stage stage) throws Exception { rendererTested.setComputeLocalRange(false); rendererTested.setComputeLocalRange(true); - assertNull(rendererTested.getPaintBarColor(null)); + assertNull(rendererTested.getPaintBarColor(new OhlcvRendererEpData())); final DefaultNumericAxis xAxis = new DefaultNumericAxis("time", "iso"); xAxis.setTimeAxis(true); @@ -134,7 +136,7 @@ public void testVolumeContructor() { @Test public void noXyChartInstance() { - assertThrows(InvalidParameterException.class, () -> rendererTested.render(null, new TestChart(), 0, null)); + assertThrows(InvalidParameterException.class, () -> rendererTested.render(new Canvas(300, 200).getGraphicsContext2D(), new TestChart(), 0, null)); } @Test diff --git a/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/css/FinancialColorSchemeConfigTest.java b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/css/FinancialColorSchemeConfigTest.java index 6016710c6..9dc505b5f 100644 --- a/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/css/FinancialColorSchemeConfigTest.java +++ b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/css/FinancialColorSchemeConfigTest.java @@ -24,13 +24,11 @@ import de.gsi.chart.Chart; import de.gsi.chart.XYChart; import de.gsi.chart.renderer.Renderer; -import de.gsi.chart.renderer.spi.financial.AbstractFinancialRenderer; -import de.gsi.chart.renderer.spi.financial.CandleStickRenderer; -import de.gsi.chart.renderer.spi.financial.HighLowRenderer; -import de.gsi.chart.renderer.spi.financial.PositionFinancialRendererPaintAfterEP; +import de.gsi.chart.renderer.spi.financial.*; import de.gsi.chart.renderer.spi.financial.service.RendererPaintAfterEP; import de.gsi.chart.renderer.spi.financial.service.RendererPaintAfterEPAware; import de.gsi.chart.renderer.spi.financial.utils.FinancialTestUtils; +import de.gsi.chart.renderer.spi.financial.utils.FootprintRenderedAPIDummyAdapter; import de.gsi.chart.renderer.spi.financial.utils.PositionFinancialDataSetDummy; import de.gsi.chart.ui.utils.JavaFXInterceptorUtils.SelectiveJavaFxInterceptor; import de.gsi.chart.ui.utils.TestFx; @@ -80,6 +78,12 @@ void applySchemeToDataset() { financialColorSchemeConfig.applySchemeToDataset(colorScheme, null, ohlcvDataSet, renderer); } assertThrows(IllegalArgumentException.class, () -> financialColorSchemeConfig.applySchemeToDataset("NOT_EXIST", null, ohlcvDataSet, renderer)); + + renderer = new FootprintRenderer(new FootprintRenderedAPIDummyAdapter(null)); + for (String colorScheme : getDefaultColorSchemes()) { + financialColorSchemeConfig.applySchemeToDataset(colorScheme, null, ohlcvDataSet, renderer); + } + assertThrows(IllegalArgumentException.class, () -> financialColorSchemeConfig.applySchemeToDataset("NOT_EXIST", null, ohlcvDataSet, renderer)); } @Test @@ -95,6 +99,12 @@ void testApplySchemeToDataset() { } assertThrows(IllegalArgumentException.class, () -> financialColorSchemeConfig.applySchemeToDataset("NOT_EXIST", ohlcvDataSet, renderer)); + renderer = new FootprintRenderer(new FootprintRenderedAPIDummyAdapter(null)); + for (String colorScheme : getDefaultColorSchemes()) { + financialColorSchemeConfig.applySchemeToDataset(colorScheme, ohlcvDataSet, renderer); + } + assertThrows(IllegalArgumentException.class, () -> financialColorSchemeConfig.applySchemeToDataset("NOT_EXIST", ohlcvDataSet, renderer)); + renderer = new EmptyFinancialRenderer(); ((EmptyFinancialRenderer) renderer).addPaintAfterEp(new PositionFinancialRendererPaintAfterEP(new PositionFinancialDataSetDummy(new ArrayList<>()), chart)); assertThrows(IllegalArgumentException.class, () -> financialColorSchemeConfig.applySchemeToDataset("NOT_EXIST", ohlcvDataSet, renderer)); diff --git a/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/utils/FootprintRenderedAPIDummyAdapter.java b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/utils/FootprintRenderedAPIDummyAdapter.java new file mode 100644 index 000000000..9d5eef481 --- /dev/null +++ b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/utils/FootprintRenderedAPIDummyAdapter.java @@ -0,0 +1,88 @@ +package de.gsi.chart.renderer.spi.financial.utils; + +import static de.gsi.chart.renderer.spi.financial.service.footprint.FootprintRendererAttributes.BID_ASK_VOLUME_FONTS; +import static de.gsi.chart.renderer.spi.financial.service.footprint.FootprintRendererAttributes.COLUMN_COLOR_GROUP_SETTINGS; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Random; + +import javafx.scene.paint.Color; +import javafx.scene.text.Font; + +import de.gsi.chart.renderer.spi.financial.FootprintRenderer; +import de.gsi.chart.renderer.spi.financial.service.footprint.FootprintRendererAttributes; +import de.gsi.chart.renderer.spi.financial.service.footprint.NbColumnColorGroup; +import de.gsi.chart.renderer.spi.financial.service.footprint.NbColumnColorGroup.FontColor; +import de.gsi.dataset.spi.financial.api.ohlcv.IOhlcvItem; + +public class FootprintRenderedAPIDummyAdapter implements FootprintRenderer.IFootprintRenderedAPI { + private final FootprintRendererAttributes footprintAttrs; + private final Random randomNum = new Random(); + private int idx = -1; + + public FootprintRenderedAPIDummyAdapter(FootprintRendererAttributes footprintAttrs) { + this.footprintAttrs = footprintAttrs; + } + + @Override + public FootprintRendererAttributes getFootprintAttributes() { + return footprintAttrs; + } + + @Override + public boolean isFootprintAvailable(IOhlcvItem ohlcvItem) { + idx++; + return idx != 2; + } + + @Override + public Collection getPriceVolumeList(IOhlcvItem ohlcvItem) { + List pba = new ArrayList<>(); + pba.add(new Double[] { ohlcvItem.getOpen(), genRand100(), genRand100() }); + pba.add(new Double[] { ohlcvItem.getHigh(), genRand100(), genRand100() }); + pba.add(new Double[] { ohlcvItem.getLow(), genRand100(), genRand100() }); + pba.add(new Double[] { ohlcvItem.getClose(), genRand100(), genRand100() }); + + return pba; + } + + @Override + public double getPocPrice(IOhlcvItem ohlcvItem) { + return ohlcvItem.getClose(); + } + + @Override + public IOhlcvItem getPullbackColumn(IOhlcvItem ohlcvItem) { + if (idx == 0 || idx == 5) { + return ohlcvItem; + } + return null; + } + + @Override + public Object getLock(IOhlcvItem ohlcvItem) { + return new Object(); + } + + @Override + public NbColumnColorGroup getColumnColorGroup(IOhlcvItem ohlcvItem) { + if (idx == 0 || idx == 1) { + Font[] fonts = footprintAttrs.getAttribute(BID_ASK_VOLUME_FONTS); + Color[][] colors = footprintAttrs.getAttribute(COLUMN_COLOR_GROUP_SETTINGS); + NbColumnColorGroup nbColumnColorGroup = new NbColumnColorGroup(); + nbColumnColorGroup.fontColorMap.put(ohlcvItem.getOpen(), new FontColor(fonts[1], colors[1][3], fonts[1], colors[0][3])); + nbColumnColorGroup.fontColorMap.put(ohlcvItem.getHigh(), new FontColor(fonts[2], colors[1][2], fonts[2], colors[0][2])); + nbColumnColorGroup.fontColorMap.put(ohlcvItem.getLow(), new FontColor(fonts[1], colors[1][1], fonts[2], colors[0][1])); + nbColumnColorGroup.fontColorMap.put(ohlcvItem.getClose(), new FontColor(fonts[1], colors[1][0], fonts[2], colors[0][0])); + + return nbColumnColorGroup; + } + return null; + } + + private double genRand100() { + return randomNum.nextInt(100); + } +} diff --git a/chartfx-dataset/src/main/java/de/gsi/dataset/spi/financial/OhlcvDataSet.java b/chartfx-dataset/src/main/java/de/gsi/dataset/spi/financial/OhlcvDataSet.java index 3474c1e72..5c1571a7d 100644 --- a/chartfx-dataset/src/main/java/de/gsi/dataset/spi/financial/OhlcvDataSet.java +++ b/chartfx-dataset/src/main/java/de/gsi/dataset/spi/financial/OhlcvDataSet.java @@ -129,6 +129,7 @@ public IOhlcvItem getItem(int index) { return ohlcv.getOhlcvItem(index); } + @Override public IOhlcvItem getLastItem() { int size = ohlcv.size(); if (size == 0) { diff --git a/chartfx-dataset/src/main/java/de/gsi/dataset/spi/financial/api/attrs/AttributeModel.java b/chartfx-dataset/src/main/java/de/gsi/dataset/spi/financial/api/attrs/AttributeModel.java index e579d4ff1..951238b5f 100644 --- a/chartfx-dataset/src/main/java/de/gsi/dataset/spi/financial/api/attrs/AttributeModel.java +++ b/chartfx-dataset/src/main/java/de/gsi/dataset/spi/financial/api/attrs/AttributeModel.java @@ -1,9 +1,6 @@ package de.gsi.dataset.spi.financial.api.attrs; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.Set; +import java.util.*; public class AttributeModel implements Cloneable { private Map, Object> attributes; @@ -194,7 +191,7 @@ public synchronized AttributeModel deepCopyAttributes() { //HashMap, Object> _attributes = (HashMap)cloner.deepClone(attributes); AttributeModel copiedModel = (AttributeModel) clone(); // clone the included attribute models - for (AttributeKey attributeKey : copiedModel.getAttributes()) { + for (AttributeKey attributeKey : new HashSet<>(copiedModel.getAttributes())) { if (AttributeModel.class.isAssignableFrom(attributeKey.getType())) { AttributeModel attributeModel = (AttributeModel) copiedModel.getAttribute(attributeKey); attributeModel = attributeModel.deepCopyAttributes(); diff --git a/chartfx-dataset/src/main/java/de/gsi/dataset/spi/financial/api/ohlcv/IOhlcvItemAware.java b/chartfx-dataset/src/main/java/de/gsi/dataset/spi/financial/api/ohlcv/IOhlcvItemAware.java index 2833371d8..0925801bd 100644 --- a/chartfx-dataset/src/main/java/de/gsi/dataset/spi/financial/api/ohlcv/IOhlcvItemAware.java +++ b/chartfx-dataset/src/main/java/de/gsi/dataset/spi/financial/api/ohlcv/IOhlcvItemAware.java @@ -10,4 +10,10 @@ public interface IOhlcvItemAware { * @return the filled ohlcv item */ IOhlcvItem getItem(int index); + + /** + * Provides the last available item in the OHLC/V structure + * @return the last filled ohlcv item + */ + IOhlcvItem getLastItem(); } diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/RunChartSamples.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/RunChartSamples.java index 2b3779f6d..ec23ac8b8 100644 --- a/chartfx-samples/src/main/java/de/gsi/chart/samples/RunChartSamples.java +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/RunChartSamples.java @@ -60,6 +60,7 @@ public void start(final Stage primaryStage) { buttons.getChildren().add(new MyButton("FinancialAdvancedCandlestickSample", new FinancialAdvancedCandlestickSample())); buttons.getChildren().add(new MyButton("FinancialPositionSample", new FinancialPositionSample())); buttons.getChildren().add(new MyButton("FinancialRealtimeCandlestickSample", new FinancialRealtimeCandlestickSample())); + buttons.getChildren().add(new MyButton("FinancialRealtimeFootprintSample", new FinancialRealtimeFootprintSample())); buttons.getChildren().add(new MyButton("FxmlSample", new FxmlSample())); buttons.getChildren().add(new MyButton("GridRendererSample", new GridRendererSample())); buttons.getChildren().add(new MyButton("HexagonSamples", new HexagonSamples())); diff --git a/chartfx-samples/src/main/java/de/gsi/financial/samples/AbstractBasicFinancialApplication.java b/chartfx-samples/src/main/java/de/gsi/financial/samples/AbstractBasicFinancialApplication.java index c58309b4f..8dd607514 100644 --- a/chartfx-samples/src/main/java/de/gsi/financial/samples/AbstractBasicFinancialApplication.java +++ b/chartfx-samples/src/main/java/de/gsi/financial/samples/AbstractBasicFinancialApplication.java @@ -8,6 +8,7 @@ import java.time.ZoneOffset; import java.util.Arrays; import java.util.Calendar; +import java.util.Map; import javafx.application.Application; import javafx.application.Platform; @@ -38,12 +39,7 @@ import de.gsi.chart.renderer.spi.financial.AbstractFinancialRenderer; import de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeAware; import de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConfig; -import de.gsi.financial.samples.dos.Interval; -import de.gsi.financial.samples.service.CalendarUtils; -import de.gsi.financial.samples.service.SimpleOhlcvDailyParser; -import de.gsi.financial.samples.service.SimpleOhlcvReplayDataSet; -import de.gsi.financial.samples.service.SimpleOhlcvReplayDataSet.DataInput; -import de.gsi.financial.samples.service.period.IntradayPeriod; +import de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConstants; import de.gsi.chart.ui.ProfilerInfoBox; import de.gsi.chart.ui.geometry.Side; import de.gsi.dataset.spi.DefaultDataSet; @@ -51,6 +47,13 @@ import de.gsi.dataset.spi.financial.api.ohlcv.IOhlcv; import de.gsi.dataset.spi.financial.api.ohlcv.IOhlcvItem; import de.gsi.dataset.utils.ProcessingProfiler; +import de.gsi.financial.samples.dos.Interval; +import de.gsi.financial.samples.service.CalendarUtils; +import de.gsi.financial.samples.service.SimpleOhlcvDailyParser; +import de.gsi.financial.samples.service.SimpleOhlcvReplayDataSet; +import de.gsi.financial.samples.service.SimpleOhlcvReplayDataSet.DataInput; +import de.gsi.financial.samples.service.consolidate.OhlcvConsolidationAddon; +import de.gsi.financial.samples.service.period.IntradayPeriod; /** * Base class for demonstration of financial charts. @@ -70,15 +73,18 @@ public abstract class AbstractBasicFinancialApplication extends Application { private final double UPDATE_PERIOD = 10.0; // replay multiple protected int DEBUG_UPDATE_RATE = 500; + protected String title; // application title + protected String theme = FinancialColorSchemeConstants.SAND; protected String resource = "@ES-[TF1D]"; protected String timeRange = "2020/08/24 0:00-2020/11/12 0:00"; protected String tt; protected String replayFrom; protected IntradayPeriod period; protected OhlcvDataSet ohlcvDataSet; + protected Map consolidationAddons; // injection - private final FinancialColorSchemeAware financialColorScheme = new FinancialColorSchemeConfig(); + protected final FinancialColorSchemeAware financialColorScheme = new FinancialColorSchemeConfig(); private final Spinner updatePeriod = new Spinner<>(1.0, 500.0, UPDATE_PERIOD, 1.0); private final CheckBox localRange = new CheckBox("auto-y"); @@ -95,6 +101,7 @@ public void start(final Stage primaryStage) { ProcessingProfiler.getTimeDiff(startTime, "adding data to chart"); startTime = ProcessingProfiler.getTimeStamp(); + configureApp(); Scene scene = prepareScene(); ProcessingProfiler.getTimeDiff(startTime, "adding chart into StackPane"); @@ -109,6 +116,10 @@ public void start(final Stage primaryStage) { stopTimer(); } + protected void configureApp() { + // configure shared variables for application sample tests + } + protected void closeDemo(final WindowEvent evt) { if (evt.getEventType().equals(WindowEvent.WINDOW_CLOSE_REQUEST) && LOGGER.isInfoEnabled()) { LOGGER.atInfo().log("requested demo to shut down"); @@ -136,7 +147,9 @@ protected ToolBar getTestToolBar(Chart chart, AbstractFinancialRenderer rende // repetitively generate new data periodicTimer = new Button("replay"); periodicTimer.setTooltip(new Tooltip("replay instrument data in realtime")); - periodicTimer.setOnAction(evt -> pauseResumeTimer()); + periodicTimer.setOnAction(evt -> { + pauseResumeTimer(); + }); updatePeriod.valueProperty().addListener((ch, o, n) -> updateTimer()); updatePeriod.setEditable(true); @@ -191,7 +204,8 @@ protected Chart getDefaultFinancialTestChart(final String theme) { period, timeRangeInt, ttInt, - replayFromCal); + replayFromCal, + consolidationAddons); } catch (ParseException e) { throw new IllegalArgumentException(e.getMessage(), e); } @@ -246,11 +260,7 @@ protected Chart getDefaultFinancialTestChart(final String theme) { prepareRenderers(chart, ohlcvDataSet, indiSet); // apply color scheme - try { - financialColorScheme.applyTo(theme, chart); - } catch (Exception e) { - throw new IllegalArgumentException(e.getMessage(), e); - } + applyColorScheme(theme, chart); // zoom to specific time range if (timeRange != null) { @@ -260,6 +270,14 @@ protected Chart getDefaultFinancialTestChart(final String theme) { return chart; } + protected void applyColorScheme(String theme, XYChart chart) { + try { + financialColorScheme.applyTo(theme, chart); + } catch (Exception e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } + /** * Show required part of the OHLC resource * diff --git a/chartfx-samples/src/main/java/de/gsi/financial/samples/FinancialFootprintSample.java b/chartfx-samples/src/main/java/de/gsi/financial/samples/FinancialFootprintSample.java new file mode 100644 index 000000000..850207d04 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/financial/samples/FinancialFootprintSample.java @@ -0,0 +1,39 @@ +package de.gsi.financial.samples; + +import javafx.application.Application; + +import de.gsi.chart.XYChart; +import de.gsi.chart.renderer.ErrorStyle; +import de.gsi.chart.renderer.spi.ErrorDataSetRenderer; +import de.gsi.chart.renderer.spi.financial.CandleStickRenderer; +import de.gsi.dataset.spi.DefaultDataSet; +import de.gsi.dataset.spi.financial.OhlcvDataSet; + +/** + * Footprint Renderer Sample + * + * @author afischer + */ +public class FinancialFootprintSample extends AbstractBasicFinancialApplication { + protected void prepareRenderers(XYChart chart, OhlcvDataSet ohlcvDataSet, DefaultDataSet indiSet) { + // create and apply renderers + CandleStickRenderer candleStickRenderer = new CandleStickRenderer(); + candleStickRenderer.getDatasets().addAll(ohlcvDataSet); + + ErrorDataSetRenderer avgRenderer = new ErrorDataSetRenderer(); + avgRenderer.setDrawMarker(false); + avgRenderer.setErrorType(ErrorStyle.NONE); + avgRenderer.getDatasets().addAll(indiSet); + + chart.getRenderers().clear(); + chart.getRenderers().add(candleStickRenderer); + chart.getRenderers().add(avgRenderer); + } + + /** + * @param args the command line arguments + */ + public static void main(final String[] args) { + Application.launch(args); + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/financial/samples/FinancialRealtimeCandlestickSample.java b/chartfx-samples/src/main/java/de/gsi/financial/samples/FinancialRealtimeCandlestickSample.java index a5b51168a..dcbd620db 100644 --- a/chartfx-samples/src/main/java/de/gsi/financial/samples/FinancialRealtimeCandlestickSample.java +++ b/chartfx-samples/src/main/java/de/gsi/financial/samples/FinancialRealtimeCandlestickSample.java @@ -19,10 +19,16 @@ import de.gsi.chart.axes.Axis; import de.gsi.chart.plugins.YRangeIndicator; import de.gsi.chart.plugins.YWatchValueIndicator; +import de.gsi.chart.renderer.Renderer; import de.gsi.chart.renderer.spi.financial.AbstractFinancialRenderer; import de.gsi.chart.renderer.spi.financial.CandleStickRenderer; import de.gsi.chart.renderer.spi.financial.PositionFinancialRendererPaintAfterEP; import de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConstants; +import de.gsi.chart.renderer.spi.financial.service.RendererPaintAfterEPAware; +import de.gsi.chart.utils.FXUtils; +import de.gsi.dataset.spi.DefaultDataSet; +import de.gsi.dataset.spi.financial.OhlcvDataSet; +import de.gsi.dataset.spi.financial.api.attrs.AttributeModel; import de.gsi.financial.samples.dos.OrderContainer; import de.gsi.financial.samples.dos.PositionContainer; import de.gsi.financial.samples.service.SimpleOhlcvReplayDataSet; @@ -32,10 +38,6 @@ import de.gsi.financial.samples.service.period.IntradayPeriod; import de.gsi.financial.samples.service.plan.MktOrderListTradePlan; import de.gsi.financial.samples.service.plan.MktOrderListTradePlan.SimMktOrder; -import de.gsi.chart.utils.FXUtils; -import de.gsi.dataset.spi.DefaultDataSet; -import de.gsi.dataset.spi.financial.OhlcvDataSet; -import de.gsi.dataset.spi.financial.api.attrs.AttributeModel; /** * Tick OHLC/V realtime processing. Demonstration of re-sample data to 2M timeframe. @@ -45,20 +47,25 @@ * @author afischer */ public class FinancialRealtimeCandlestickSample extends AbstractBasicFinancialApplication { - private CandleStickRenderer candleStickRenderer; - /** - * Prepare charts to the root. + * Sample App Test Configuration */ - protected Scene prepareScene() { - String title = "Replay OHLC/V Tick Data in real-time (press 'replay' button)"; - String priceFormat = "%1.1f"; + @Override + protected void configureApp() { + title = "Replay OHLC/V Tick Data in real-time (press 'replay' button, zoom by mousewheel)"; + theme = FinancialColorSchemeConstants.SAND; resource = "REALTIME_OHLC_TICK"; timeRange = "2016/07/29 00:00-2016/07/29 20:15"; tt = "00:00-23:59"; // time template whole day session replayFrom = "2016/07/29 13:58"; period = new IntradayPeriod(M, 2.0); + } + /** + * Prepare charts to the root. + */ + protected Scene prepareScene() { + String priceFormat = "%1.1f"; // simulate market orders list List orders = new ArrayList<>(); orders.add(new SimMktOrder("2016/07/29 14:06", 3)); @@ -74,7 +81,7 @@ protected Scene prepareScene() { orders.add(new SimMktOrder("2016/07/29 16:56", 1)); orders.add(new SimMktOrder("2016/07/29 18:40", 1)); - final Chart chart = getDefaultFinancialTestChart(FinancialColorSchemeConstants.SAND); + final Chart chart = getDefaultFinancialTestChart(theme); final AbstractFinancialRenderer renderer = (AbstractFinancialRenderer) chart.getRenderers().get(0); chart.setTitle(title); @@ -105,8 +112,9 @@ protected Scene prepareScene() { asset, ohlcvDataSet, context); // example of addition complex extension-point to renderer - candleStickRenderer.addPaintAfterEp(new PositionFinancialRendererPaintAfterEP( - positionFinancialDataSet, (XYChart) chart)); + if (renderer instanceof RendererPaintAfterEPAware) { + ((RendererPaintAfterEPAware) renderer).addPaintAfterEp(new PositionFinancialRendererPaintAfterEP(positionFinancialDataSet, (XYChart) chart)); + } // execution platform (has to be last added to dataset) BacktestExecutionPlatform executionPlatform = new BacktestExecutionPlatform(); @@ -132,6 +140,9 @@ protected Scene prepareScene() { chart.getPlugins().add(createRsLevel(yAxis, 4710, 4711, "Daily Support")); chart.getPlugins().add(createRsLevel(yAxis, 4731, 4733, "Daily Resistance")); + // apply all changes by addons and extensions + applyColorScheme(theme, (XYChart) chart); + VBox root = new VBox(); VBox.setVgrow(chart, Priority.SOMETIMES); root.getChildren().addAll(testVariableToolBar, chart); @@ -149,11 +160,11 @@ protected YRangeIndicator createRsLevel(Axis yAxis, double lowerBound, double up protected void prepareRenderers(XYChart chart, OhlcvDataSet ohlcvDataSet, DefaultDataSet indiSet) { // create and apply renderers - candleStickRenderer = new CandleStickRenderer(true); - candleStickRenderer.getDatasets().addAll(ohlcvDataSet); + Renderer renderer = new CandleStickRenderer(true); + renderer.getDatasets().addAll(ohlcvDataSet); chart.getRenderers().clear(); - chart.getRenderers().add(candleStickRenderer); + chart.getRenderers().add(renderer); } /** diff --git a/chartfx-samples/src/main/java/de/gsi/financial/samples/FinancialRealtimeFootprintSample.java b/chartfx-samples/src/main/java/de/gsi/financial/samples/FinancialRealtimeFootprintSample.java new file mode 100644 index 000000000..b867469a7 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/financial/samples/FinancialRealtimeFootprintSample.java @@ -0,0 +1,73 @@ +package de.gsi.financial.samples; + +import static de.gsi.financial.samples.service.period.IntradayPeriod.IntradayPeriodEnum.M; + +import java.util.HashMap; + +import javafx.application.Application; + +import de.gsi.chart.XYChart; +import de.gsi.chart.renderer.spi.financial.FootprintRenderer; +import de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConstants; +import de.gsi.chart.renderer.spi.financial.service.footprint.FootprintRendererAttributes; +import de.gsi.dataset.spi.DefaultDataSet; +import de.gsi.dataset.spi.financial.OhlcvDataSet; +import de.gsi.financial.samples.service.addon.AbsorptionConsolidationAddon; +import de.gsi.financial.samples.service.consolidate.OhlcvConsolidationAddon; +import de.gsi.financial.samples.service.footprint.AbsorptionClusterRendererPaintAfterEP; +import de.gsi.financial.samples.service.footprint.DiagonalDominantNbColumnColorGroupService; +import de.gsi.financial.samples.service.footprint.FootprintRenderedAPIAdapter; +import de.gsi.financial.samples.service.period.IntradayPeriod; + +/** + * Tick FOOTPRINT realtime processing. Demonstration of re-sample data to 2M timeframe. + * Support/Resistance range levels added. + * YWatchValueIndicator for better visualization of y-values, auto-handling of close prices and manual settings of price levels. + * + * @author afischer + */ +public class FinancialRealtimeFootprintSample extends FinancialRealtimeCandlestickSample { + @Override + protected void configureApp() { + title = "Replay FOOTPRINT Tick Data in real-time (press 'replay' button, zoom by mousewheel)"; + theme = FinancialColorSchemeConstants.DARK; + resource = "REALTIME_OHLC_TICK"; + timeRange = "2016/07/29 13:25-2016/07/29 14:25"; + tt = "00:00-23:59"; // time template whole day session + replayFrom = "2016/07/29 13:58"; + // price consolidation addons (extensions) + consolidationAddons = new HashMap<>(); + consolidationAddons.put("footprintCalcAddons", new OhlcvConsolidationAddon[] { + new AbsorptionConsolidationAddon(false, 70, 3, 0.33d, 100.0d) }); + // parameter extendedCalculation ensures calculation of all necessary data for footprints features + // parameter calculationAddonServicesType: possible add addon services for specific footprint additional features paintings + period = new IntradayPeriod(M, 2.0, 0.0, true, "footprintCalcAddons"); + } + + protected void prepareRenderers(XYChart chart, OhlcvDataSet ohlcvDataSet, DefaultDataSet indiSet) { + // configure footprint attributes (create defaults, and modify it by .setAttribute() methods + FootprintRendererAttributes footprintAttrs = FootprintRendererAttributes.getDefaultValues(theme); + + // create and apply renderers + FootprintRenderer renderer = new FootprintRenderer( + new FootprintRenderedAPIAdapter(footprintAttrs, + new DiagonalDominantNbColumnColorGroupService(footprintAttrs)), + true, + true, + true); + + // example of addition footprint extension point + renderer.addPaintAfterEp(new AbsorptionClusterRendererPaintAfterEP(ohlcvDataSet, chart)); + renderer.getDatasets().addAll(ohlcvDataSet); + + chart.getRenderers().clear(); + chart.getRenderers().add(renderer); + } + + /** + * @param args the command line arguments + */ + public static void main(final String[] args) { + Application.launch(args); + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/financial/samples/dos/PriceVolume.java b/chartfx-samples/src/main/java/de/gsi/financial/samples/dos/PriceVolume.java deleted file mode 100644 index cbdf9cf86..000000000 --- a/chartfx-samples/src/main/java/de/gsi/financial/samples/dos/PriceVolume.java +++ /dev/null @@ -1,18 +0,0 @@ -package de.gsi.financial.samples.dos; - -public class PriceVolume { - public double price; - public double volumeDown; // bid - public double volumeUp; // ask - - public PriceVolume(double price, double volumeDown, double volumeUp) { - this.price = price; - this.volumeDown = volumeDown; // bid - this.volumeUp = volumeUp; // ask - } - - @Override - public String toString() { - return "PriceVolume [price=" + price + ", bidVolume=" + volumeDown + ", askVolume=" + volumeUp + "]"; - } -} diff --git a/chartfx-samples/src/main/java/de/gsi/financial/samples/dos/PriceVolumeContainer.java b/chartfx-samples/src/main/java/de/gsi/financial/samples/dos/PriceVolumeContainer.java index 2ca03d54d..d562309ff 100644 --- a/chartfx-samples/src/main/java/de/gsi/financial/samples/dos/PriceVolumeContainer.java +++ b/chartfx-samples/src/main/java/de/gsi/financial/samples/dos/PriceVolumeContainer.java @@ -4,7 +4,7 @@ import java.util.TreeMap; public class PriceVolumeContainer { - private final TreeMap priceVolumeMap = new TreeMap<>(); + private final TreeMap priceVolumeMap = new TreeMap<>(); private double pocPrice; private double pocVolume = -Double.MAX_VALUE; @@ -15,16 +15,16 @@ public class PriceVolumeContainer { * @param volumeUp tick up volume */ public void addPriceVolume(double price, double volumeDown, double volumeUp) { - PriceVolume priceVolume = priceVolumeMap.get(price); + Double[] priceVolume = priceVolumeMap.get(price); if (priceVolume == null) { - priceVolume = new PriceVolume(price, volumeDown, volumeUp); + priceVolume = new Double[] { price, volumeDown, volumeUp }; priceVolumeMap.put(price, priceVolume); } else { - priceVolume.volumeUp += volumeUp; - priceVolume.volumeDown += volumeDown; + priceVolume[1] += volumeDown; + priceVolume[2] += volumeUp; } - double totalVolume = priceVolume.volumeUp + priceVolume.volumeDown; + double totalVolume = priceVolume[1] + priceVolume[2]; if (totalVolume > pocVolume) { pocVolume = totalVolume; pocPrice = price; @@ -35,21 +35,21 @@ public void addPriceVolume(double price, double volumeDown, double volumeUp) { * @param price return DO price volume by required price level * @return provides volume information for specific price */ - public PriceVolume getPriceVolumeBy(double price) { + public Double[] getPriceVolumeBy(double price) { return priceVolumeMap.get(price); } /** * @return provides price volume tree map */ - public TreeMap getCompletedPriceVolumeTreeMap() { + public TreeMap getCompletedPriceVolumeTreeMap() { return priceVolumeMap; } /** * @return provides price volume collection for actual bar */ - public Collection getCompletedPriceVolume() { + public Collection getCompletedPriceVolume() { return priceVolumeMap.values(); } diff --git a/chartfx-samples/src/main/java/de/gsi/financial/samples/service/SimpleOhlcvReplayDataSet.java b/chartfx-samples/src/main/java/de/gsi/financial/samples/service/SimpleOhlcvReplayDataSet.java index 5f108a742..92f40e887 100644 --- a/chartfx-samples/src/main/java/de/gsi/financial/samples/service/SimpleOhlcvReplayDataSet.java +++ b/chartfx-samples/src/main/java/de/gsi/financial/samples/service/SimpleOhlcvReplayDataSet.java @@ -6,6 +6,7 @@ import java.nio.channels.ClosedChannelException; import java.util.Calendar; import java.util.LinkedHashSet; +import java.util.Map; import java.util.Set; import javafx.beans.property.DoubleProperty; @@ -23,6 +24,7 @@ import de.gsi.financial.samples.dos.Interval; import de.gsi.financial.samples.dos.OHLCVItem; import de.gsi.financial.samples.service.consolidate.IncrementalOhlcvConsolidation; +import de.gsi.financial.samples.service.consolidate.OhlcvConsolidationAddon; import de.gsi.financial.samples.service.consolidate.OhlcvTimeframeConsolidation; import de.gsi.financial.samples.service.period.IntradayPeriod; @@ -61,10 +63,11 @@ public enum DataInput { OHLC_TICK } - public SimpleOhlcvReplayDataSet(DataInput dataInput, IntradayPeriod period, Interval timeRange, Interval tt, Calendar replayFrom) { + public SimpleOhlcvReplayDataSet(DataInput dataInput, IntradayPeriod period, Interval timeRange, + Interval tt, Calendar replayFrom, Map addons) { super(dataInput.name()); setInputSource(dataInput); - fillTestData(period, timeRange, tt, replayFrom); // NOPMD + fillTestData(period, timeRange, tt, replayFrom, addons); // NOPMD if (LOGGER.isDebugEnabled()) { LOGGER.atDebug().addArgument(SimpleOhlcvReplayDataSet.class.getSimpleName()).log("started '{}'"); } @@ -74,7 +77,7 @@ public void addOhlcvChangeListener(OhlcvChangeListener ohlcvChangeListener) { ohlcvChangeListeners.add(ohlcvChangeListener); } - public void fillTestData(IntradayPeriod period, Interval timeRange, Interval tt, Calendar replayFrom) { + public void fillTestData(IntradayPeriod period, Interval timeRange, Interval tt, Calendar replayFrom, Map addons) { lock().writeLockGuard( () -> { try { @@ -89,7 +92,7 @@ public void fillTestData(IntradayPeriod period, Interval timeRange, In ohlcv = new DefaultOHLCV(); ohlcv.setTitle(resource); - consolidation = OhlcvTimeframeConsolidation.createConsolidation(period, tt, null); + consolidation = OhlcvTimeframeConsolidation.createConsolidation(period, tt, addons); autoNotification().set(false); setData(ohlcv); diff --git a/chartfx-samples/src/main/java/de/gsi/financial/samples/service/addon/AbsorptionConsolidationAddon.java b/chartfx-samples/src/main/java/de/gsi/financial/samples/service/addon/AbsorptionConsolidationAddon.java new file mode 100644 index 000000000..13a598448 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/financial/samples/service/addon/AbsorptionConsolidationAddon.java @@ -0,0 +1,163 @@ +package de.gsi.financial.samples.service.addon; + +import java.util.NavigableMap; + +import de.gsi.financial.samples.dos.*; +import de.gsi.financial.samples.service.consolidate.OhlcvConsolidationAddon; + +public class AbsorptionConsolidationAddon implements OhlcvConsolidationAddon { + private final boolean searchDynamic; + private final int absorptionLevel; + private final int absorptionCluster; + private final double searchBarPercent; + private final double extremeAbsorptionLevelParam; + + /** + * @param searchDynamic + * - dynamic processing when the bar is painting + * @param absorptionLevel + * - defines value for volume in the price level which detects + * price for absorption cluster + * @param absorptionCluster + * - minimal length of cluster for detection + * @param searchBarPercent + * - where is accepted the cluster in whole bar in percent (e.g. + * 33% = 1/3 bar from low for bid, and high for ask) + * @param extremeAbsorptionLevelParam + * - logic for searching extreme bid/ask volumes single clusters + */ + public AbsorptionConsolidationAddon(boolean searchDynamic, int absorptionLevel, int absorptionCluster, + double searchBarPercent, double extremeAbsorptionLevelParam) { + this.searchDynamic = searchDynamic; + this.absorptionLevel = absorptionLevel; + this.absorptionCluster = absorptionCluster; + this.searchBarPercent = searchBarPercent; + this.extremeAbsorptionLevelParam = extremeAbsorptionLevelParam; + } + + @Override + public DefaultOHLCV consolidationUpdateAddon(DefaultOHLCV ohlcv, OHLCVItem incrementItem) { + return ohlcv; + } + + @Override + public DefaultOHLCV consolidationAdditionAddon(DefaultOHLCV ohlcv, OHLCVItem incrementItem) { + if (ohlcv.size() < 2) { + return ohlcv; + } + OHLCVItem lastBarItem = ohlcv.getBackOhlcvItem(1); + findClusters(lastBarItem); + + return ohlcv; + } + + @Override + public boolean isDynamic() { + return searchDynamic; + } + + protected void findClusters(OHLCVItem barItem) { + if (barItem.getExtended() == null) { + return; + } + AbsorptionClusterDO absorptionClusterDO = new AbsorptionClusterDO(); + findClustersOfSide(barItem, true, absorptionClusterDO); + findClustersOfSide(barItem, false, absorptionClusterDO); + barItem.getExtended().setAbsorptionClusterDO(absorptionClusterDO); // replace previous + } + + protected void findClustersOfSide(OHLCVItem barItem, boolean bidOrAsk, AbsorptionClusterDO absorptionClusterDO) { + PriceVolumeContainer priceVolumeContainer = barItem.getExtended().getPriceVolumeContainer(); + NavigableMap map = bidOrAsk ? priceVolumeContainer.getCompletedPriceVolumeTreeMap() + : priceVolumeContainer.getCompletedPriceVolumeTreeMap().descendingMap(); + + double length = barItem.getHigh() - barItem.getLow(); + double maxPriceBid = length * searchBarPercent + barItem.getLow(); + double minPriceAsk = barItem.getHigh() - length * searchBarPercent; + boolean clusterDetected = false; + double val1 = -1; + double val2 = -1; + int clusterActiveLength = 0; + boolean firstTime = false; + + //-------------------------------------------- + // BASIC CLUSTER DETECTION + // basic detection of cluster defined by absorption level and absorption + // cluster minimal length + for (Double[] priceVolume : map.values()) { + double volume = bidOrAsk ? priceVolume[1] : priceVolume[2]; + boolean inrange = firstTime + || (bidOrAsk ? priceVolume[0] <= maxPriceBid : priceVolume[0] >= minPriceAsk); + if (volume >= absorptionLevel && inrange) { + if (clusterActiveLength == 0) { + val1 = priceVolume[0]; + firstTime = true; + } + clusterActiveLength++; + } else { + clusterActiveLength = 0; + firstTime = false; + if (clusterDetected) { + break; + } + } + if (clusterActiveLength >= absorptionCluster) { + clusterDetected = true; + val2 = priceVolume[0]; + } + } + if (clusterDetected) { + if (bidOrAsk) { + absorptionClusterDO.addBidCluster(new Interval<>(val2, val1)); + } else { + absorptionClusterDO.addAskCluster(new Interval<>(val1, val2)); + } + } else { + //------------------------------------------------------------- + // EXTREME CLUSTER DETECTION + // basic cluster doesn't exist + // try to find extreme cluster accumulation bid/ask volume + val1 = -1; + val2 = -1; + clusterActiveLength = 0; + firstTime = false; + + double extremAbsorptionLevel = 0.0d; + for (Double[] priceVolume : map.values()) { + double volume = bidOrAsk ? priceVolume[1] : priceVolume[2]; + extremAbsorptionLevel += volume; + } + extremAbsorptionLevel = extremAbsorptionLevel / map.size(); + + for (Double[] priceVolume : map.values()) { + double volume = bidOrAsk ? priceVolume[1] : priceVolume[2]; + boolean inrange = firstTime + || (bidOrAsk ? priceVolume[0] <= maxPriceBid : priceVolume[0] >= minPriceAsk); + if (volume - extremAbsorptionLevel >= extremeAbsorptionLevelParam && inrange) { + if (clusterActiveLength == 0) { + val1 = priceVolume[0]; + firstTime = true; + } + clusterActiveLength++; + } else { + clusterActiveLength = 0; + firstTime = false; + if (clusterDetected) { + break; + } + } + if (clusterActiveLength >= 1) { + clusterDetected = true; + val2 = priceVolume[0]; + } + } + if (clusterDetected) { + if (bidOrAsk) { + absorptionClusterDO.addBidCluster(new Interval<>(val2, val1)); + } else { + absorptionClusterDO.addAskCluster(new Interval<>(val1, val2)); + } + } + } + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/financial/samples/service/footprint/AbsorptionClusterRendererPaintAfterEP.java b/chartfx-samples/src/main/java/de/gsi/financial/samples/service/footprint/AbsorptionClusterRendererPaintAfterEP.java new file mode 100644 index 000000000..58b137d01 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/financial/samples/service/footprint/AbsorptionClusterRendererPaintAfterEP.java @@ -0,0 +1,100 @@ +package de.gsi.financial.samples.service.footprint; + +import javafx.scene.paint.Color; + +import de.gsi.chart.XYChart; +import de.gsi.chart.axes.Axis; +import de.gsi.chart.renderer.spi.financial.FootprintRenderer.EpDataAddon; +import de.gsi.chart.renderer.spi.financial.service.DataSetAware; +import de.gsi.chart.renderer.spi.financial.service.OhlcvRendererEpData; +import de.gsi.chart.renderer.spi.financial.service.RendererPaintAfterEP; +import de.gsi.chart.utils.StyleParser; +import de.gsi.dataset.DataSet; +import de.gsi.financial.samples.dos.Interval; +import de.gsi.financial.samples.dos.OHLCVItem; +import de.gsi.financial.samples.dos.OHLCVItemExtended; + +/** + * Find Footprint Bid/Ask Clusters + */ +@SuppressWarnings({ "PMD.NPathComplexity" }) +public class AbsorptionClusterRendererPaintAfterEP implements RendererPaintAfterEP, DataSetAware { + public static final String DATASET_ABSORPTION_ASK_COLOR = "absorptionAskColor"; + public static final String DATASET_ABSORPTION_BID_COLOR = "absorptionAskColor"; + public static final String DATASET_ABSORPTION_ASK_TRANS_COLOR = "absorptionAskTransColor"; + public static final String DATASET_ABSORPTION_BID_TRANS_COLOR = "absorptionBidTransColor"; + + protected final DataSet ds; + protected final XYChart chart; + protected final Axis xAxis; + protected final Axis yAxis; + + private Color absorptionAskColor; + private Color absorptionBidColor; + private Color absorptionAskTransColor; + private Color absorptionBidTransColor; + private double xFrom; + private double xTo; + private double xDiff; + + public AbsorptionClusterRendererPaintAfterEP(final DataSet ohlcvDataSet, final XYChart chart) { + this.ds = ohlcvDataSet; + this.chart = chart; + xAxis = chart.getXAxis(); + yAxis = chart.getYAxis(); + } + + @Override + public DataSet getDataSet() { + return ds; + } + + protected void initByDatasetFxStyle() { + String style = ds.getStyle(); + absorptionAskColor = StyleParser.getColorPropertyValue(style, DATASET_ABSORPTION_ASK_COLOR, Color.rgb(255, 128, 128)); + absorptionBidColor = StyleParser.getColorPropertyValue(style, DATASET_ABSORPTION_BID_COLOR, Color.GREEN); + absorptionAskTransColor = StyleParser.getColorPropertyValue(style, DATASET_ABSORPTION_ASK_TRANS_COLOR, Color.rgb(255, 128, 128, 0.2)); + absorptionBidTransColor = StyleParser.getColorPropertyValue(style, DATASET_ABSORPTION_BID_TRANS_COLOR, Color.rgb(0, 255, 0, 0.2)); + } + + @Override + public void paintAfter(OhlcvRendererEpData d) { + if (d.index == d.minIndex) { + initByDatasetFxStyle(); + } + OHLCVItemExtended itemExtended = ((OHLCVItem) d.ohlcvItem).getExtended(); + if (itemExtended == null || itemExtended.getAbsorptionClusterDO() == null) + return; + + // compute constants + double x0 = d.xCenter; + EpDataAddon dd = (EpDataAddon) d.addon; + d.gc.save(); + xFrom = x0 - dd.maxWidthTextBid - dd.fontGap - dd.basicGap; + xTo = x0 + dd.maxWidthTextBid + dd.fontGap + dd.basicGap; + xDiff = xTo - xFrom; + + d.gc.setLineWidth(2.5f); + d.gc.setStroke(absorptionAskColor); + d.gc.setFill(absorptionAskTransColor); + for (Interval bidCluster : itemExtended.getAbsorptionClusterDO().getBidClusters()) { + paintCluster(d, dd, bidCluster); + } + d.gc.setStroke(absorptionBidColor); + d.gc.setFill(absorptionBidTransColor); + for (Interval askCluster : itemExtended.getAbsorptionClusterDO().getAskClusters()) { + paintCluster(d, dd, askCluster); + } + d.gc.restore(); + } + + private void paintCluster(OhlcvRendererEpData d, EpDataAddon dd, + Interval bidCluster) { + double bidFrom = yAxis.getDisplayPosition(bidCluster.from) - dd.heightText / 2.0 - 3 * dd.basicGap; + double bidTo = yAxis.getDisplayPosition(bidCluster.to) + dd.heightText / 2.0 + 4 * dd.basicGap; + + d.gc.strokeLine(xFrom, bidFrom, xTo, bidFrom); + d.gc.strokeLine(xFrom, bidTo, xTo, bidTo); + d.gc.fillRect(xFrom, bidFrom, xDiff, bidTo - bidFrom); + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/financial/samples/service/footprint/DiagonalDominantNbColumnColorGroupService.java b/chartfx-samples/src/main/java/de/gsi/financial/samples/service/footprint/DiagonalDominantNbColumnColorGroupService.java new file mode 100644 index 000000000..cd11e6ec2 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/financial/samples/service/footprint/DiagonalDominantNbColumnColorGroupService.java @@ -0,0 +1,102 @@ +package de.gsi.financial.samples.service.footprint; + +import javafx.scene.paint.Color; +import javafx.scene.text.Font; + +import de.gsi.chart.renderer.spi.financial.service.footprint.FootprintRendererAttributes; +import de.gsi.chart.renderer.spi.financial.service.footprint.NbColumnColorGroup; +import de.gsi.chart.renderer.spi.financial.service.footprint.NbColumnColorGroup.FontColor; +import de.gsi.financial.samples.dos.PriceVolumeContainer; + +/** + * Standard calculation of computation by percentage of diagonal dominant bid x ask volumes + */ +public class DiagonalDominantNbColumnColorGroupService implements NbColumnColorGroupService { + private final boolean columnColoringFeatureActive; + private final Color[][] columnColorGroupSettings; + private final Double[] columnColorGroupThresholds; + private final Font[] bidAskVolumeFonts; + private final Double bidAskBoldThreshold; + + public DiagonalDominantNbColumnColorGroupService(FootprintRendererAttributes footprintAttrs) { + columnColoringFeatureActive = footprintAttrs.getAttribute(FootprintRendererAttributes.COLUMN_COLORING_FEATURE_ACTIVE); + columnColorGroupSettings = footprintAttrs.getAttribute(FootprintRendererAttributes.COLUMN_COLOR_GROUP_SETTINGS); + columnColorGroupThresholds = footprintAttrs.getAttribute(FootprintRendererAttributes.COLUMN_COLOR_GROUP_THRESHOLDS); + bidAskBoldThreshold = footprintAttrs.getAttribute(FootprintRendererAttributes.BID_ASK_BOLD_THRESHOLD); + bidAskVolumeFonts = footprintAttrs.getAttribute(FootprintRendererAttributes.BID_ASK_VOLUME_FONTS); + } + + @Override + public NbColumnColorGroup calculate(PriceVolumeContainer priceVolumeContainer) { + if (!columnColoringFeatureActive) + return null; + + NbColumnColorGroup result = new NbColumnColorGroup(); + + Double[] prevPriceVolume = null; + Color prevBidColor; + Color askColor; + Color prevAskColor = null; + Font bidVolumeFont = null; + Font askVolumeFont = null; + + for (Double[] priceVolume : priceVolumeContainer.getCompletedPriceVolume()) { + double bidVolume = priceVolume[1]; + double askVolume = priceVolume[2]; + + if (prevPriceVolume != null) { // diagonal computation + double prevBidVolume = prevPriceVolume[1]; + + double prevBidPercentage = prevBidVolume / askVolume * 100.0; + double askPercentage = askVolume / prevBidVolume * 100.0; + + prevBidColor = getColumnColorGroup(prevBidPercentage, 0); + askColor = getColumnColorGroup(askPercentage, 1); + + result.fontColorMap.put(prevPriceVolume[0], new FontColor(bidVolumeFont, prevBidColor, askVolumeFont, prevAskColor)); + + prevAskColor = askColor; + bidVolumeFont = getFontForBidAskVolume(bidVolume); + askVolumeFont = getFontForBidAskVolume(askVolume); + + } else { // first bottom line + prevAskColor = columnColorGroupSettings[1][1]; // group 1 + bidVolumeFont = getFontForBidAskVolume(bidVolume); + askVolumeFont = getFontForBidAskVolume(askVolume); + } + + prevPriceVolume = priceVolume; + } + // last top line + prevBidColor = columnColorGroupSettings[0][1]; // group 1 + if (prevPriceVolume != null) { + result.fontColorMap.put(prevPriceVolume[0], new FontColor(bidVolumeFont, prevBidColor, askVolumeFont, prevAskColor)); + } + + return result; + } + + private Font getFontForBidAskVolume(double volume) { + if (volume >= bidAskBoldThreshold) { + return bidAskVolumeFonts[1]; + } else { + return bidAskVolumeFonts[0]; + } + } + + private Color getColumnColorGroup(double percentage, int bidOrAsk) { + if (percentage < columnColorGroupThresholds[0]) { + return columnColorGroupSettings[bidOrAsk][0]; + } + if (percentage >= columnColorGroupThresholds[0] && percentage < columnColorGroupThresholds[1]) { + return columnColorGroupSettings[bidOrAsk][1]; + } + if (percentage >= columnColorGroupThresholds[1] && percentage < columnColorGroupThresholds[2]) { + return columnColorGroupSettings[bidOrAsk][2]; + } + if (percentage >= columnColorGroupThresholds[2]) { + return columnColorGroupSettings[bidOrAsk][3]; + } + return null; + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/financial/samples/service/footprint/FootprintRenderedAPIAdapter.java b/chartfx-samples/src/main/java/de/gsi/financial/samples/service/footprint/FootprintRenderedAPIAdapter.java new file mode 100644 index 000000000..3e59818aa --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/financial/samples/service/footprint/FootprintRenderedAPIAdapter.java @@ -0,0 +1,60 @@ +package de.gsi.financial.samples.service.footprint; + +import java.util.Collection; + +import de.gsi.chart.renderer.spi.financial.FootprintRenderer; +import de.gsi.chart.renderer.spi.financial.service.footprint.FootprintRendererAttributes; +import de.gsi.chart.renderer.spi.financial.service.footprint.NbColumnColorGroup; +import de.gsi.dataset.spi.financial.api.ohlcv.IOhlcvItem; +import de.gsi.financial.samples.dos.OHLCVItem; + +/** + * Specific implementation of adapter for your trading framework. + * This adapter is just demonstration to show fast mapping to trading domain objects for providing footprint data for renderers. + * + * @author afischer + */ +public class FootprintRenderedAPIAdapter implements FootprintRenderer.IFootprintRenderedAPI { + private final FootprintRendererAttributes footprintAttrs; + private final NbColumnColorGroupService nbColumnColorGroupService; + + public FootprintRenderedAPIAdapter(FootprintRendererAttributes footprintAttrs, NbColumnColorGroupService nbColumnColorGroupService) { + this.footprintAttrs = footprintAttrs; + this.nbColumnColorGroupService = nbColumnColorGroupService; + } + + @Override + public FootprintRendererAttributes getFootprintAttributes() { + return footprintAttrs; + } + + @Override + public boolean isFootprintAvailable(IOhlcvItem ohlcvItem) { + return ((OHLCVItem) ohlcvItem).getExtended() != null; + } + + @Override + public Collection getPriceVolumeList(IOhlcvItem ohlcvItem) { + return ((OHLCVItem) ohlcvItem).getExtended().getPriceVolumeContainer().getCompletedPriceVolume(); + } + + @Override + public double getPocPrice(IOhlcvItem ohlcvItem) { + return ((OHLCVItem) ohlcvItem).getExtended().getPriceVolumeContainer().getPocPrice(); + } + + @Override + public IOhlcvItem getPullbackColumn(IOhlcvItem ohlcvItem) { + return ((OHLCVItem) ohlcvItem).getExtended().getPullbackOhlcvItem(); + } + + @Override + public Object getLock(IOhlcvItem ohlcvItem) { + return ((OHLCVItem) ohlcvItem).getExtended().lock; + } + + @Override + public NbColumnColorGroup getColumnColorGroup(IOhlcvItem ohlcvItem) { + return nbColumnColorGroupService.calculate(((OHLCVItem) ohlcvItem).getExtended().getPriceVolumeContainer()); + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/financial/samples/service/footprint/NbColumnColorGroupService.java b/chartfx-samples/src/main/java/de/gsi/financial/samples/service/footprint/NbColumnColorGroupService.java new file mode 100644 index 000000000..ac5f989c3 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/financial/samples/service/footprint/NbColumnColorGroupService.java @@ -0,0 +1,16 @@ +package de.gsi.financial.samples.service.footprint; + +import de.gsi.chart.renderer.spi.financial.service.footprint.NbColumnColorGroup; +import de.gsi.financial.samples.dos.PriceVolumeContainer; + +/** + * Calculate color group settings for each bid/ask volume in each level price + */ +public interface NbColumnColorGroupService { + /** + * Calculate color group settings for each bid/ask volume in each level price + * @param priceVolumeContainer which has to be painted + * @return the result with column color group data result + */ + NbColumnColorGroup calculate(PriceVolumeContainer priceVolumeContainer); +} diff --git a/docs/pics/FinancialRealtimeFootprintSample.png b/docs/pics/FinancialRealtimeFootprintSample.png new file mode 100644 index 000000000..0801d2068 Binary files /dev/null and b/docs/pics/FinancialRealtimeFootprintSample.png differ