+ * 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