diff --git a/README.md b/README.md index 380ebfdd7..c1d1361b2 100644 --- a/README.md +++ b/README.md @@ -248,11 +248,12 @@ mvn exec:java - - + + - + +
FinancialCandlestickSample
FinancialCandlestickSample.java
FinancialHiLowSample
FinancialHiLowSample.java
FinancialCandlestickSample
FinancialCandlestickSample.java (Several Themes Supported)
FinancialHiLowSample
FinancialHiLowSample.java (OHLC Renderer)
FinancialAdvancedCandlestickSample
FinancialAdvancedCandlestickSample.java
FinancialAdvancedCandlestickSample
FinancialAdvancedCandlestickSample.java (Advanced PaintBars and Extension Points)
FinancialAdvancedCandlestickSample
FinancialRealtimeCandlestickSample.java (OHLC Tick Replay Real-time processing)
diff --git a/chartfx-chart/src/main/java/de/gsi/chart/plugins/YWatchValueIndicator.java b/chartfx-chart/src/main/java/de/gsi/chart/plugins/YWatchValueIndicator.java new file mode 100644 index 000000000..2652d5a02 --- /dev/null +++ b/chartfx-chart/src/main/java/de/gsi/chart/plugins/YWatchValueIndicator.java @@ -0,0 +1,192 @@ +/** + * Copyright (c) 2017 European Organisation for Nuclear Research (CERN), All Rights Reserved. + */ + +package de.gsi.chart.plugins; + +import javafx.geometry.BoundingBox; +import javafx.geometry.Bounds; +import javafx.geometry.Point2D; +import javafx.scene.input.MouseEvent; + +import de.gsi.chart.axes.Axis; +import de.gsi.chart.ui.geometry.Side; +import de.gsi.dataset.event.EventSource; + +/** + * A horizontal line drawn on the plot area, indicating specified Y value with the {@link #textProperty() text + * label} describing the value inside the Y-Axis marker. + *

+ * Style Classes (from least to most specific): + *

+ * where {@code [index]} corresponds to the index (zero based) of this indicator instance added to the + * {@code XYChartPane}. For example class {@code y-value-indicator-label1} can be used to style label of the second + * instance of this indicator added to the chart pane. + * + * @author mhrabia + * @author afischer (modified) + */ +public class YWatchValueIndicator extends AbstractSingleValueIndicator implements EventSource, ValueIndicator { + protected static final String STYLE_CLASS_LABEL = "value-watch-indicator-label"; + protected static final String STYLE_CLASS_LINE = "value-watch-indicator-line"; + protected static final String STYLE_CLASS_MARKER = "value-watch-indicator-marker"; + + protected final String valueFormat; + protected String id; + + /** + * Creates a new instance indicating given Y value belonging to the specified {@code yAxis}. + * + * @param axis the axis this indicator is associated with + * @param valueFormat a value string format for marker visualization + * @param value a value to be marked + */ + public YWatchValueIndicator(final Axis axis, final String valueFormat, final double value) { + super(axis, value, String.format(valueFormat, value)); + this.valueFormat = valueFormat; + + // marker is visible always for this indicator + triangle.visibleProperty().unbind(); + triangle.visibleProperty().set(true); + + pickLine.setOnMouseDragged(this::handleDragMouseEvent); + triangle.setOnMouseDragged(this::handleDragMouseEvent); + label.setOnMouseDragged(this::handleDragMouseEvent); + } + + /** + * Creates a new instance for the specified {@code yAxis}. + * The Y value is updated by listeners. + * + * @param axis the axis this indicator is associated with + * @param valueFormat a value string format for marker visualization + */ + public YWatchValueIndicator(final Axis axis, final String valueFormat) { + this(axis, valueFormat, 0.0); + } + + /** + * Set the text and value for this indicator marker. + * + * @param value Update marker label and its Y Axis position by this double value. + */ + public void setMarkerValue(final double value) { + setText(String.format(valueFormat, value)); + setValue(value); + } + + /** + * Set visibility of the horizontal line + * + * @param lineVisible line visibility boolean + */ + public void setLineVisible(final boolean lineVisible) { + line.setVisible(lineVisible); + pickLine.setVisible(lineVisible); + } + + /** + * Unique identification of the indicator + * + * @return id unique ID + */ + public String getId() { + return id; + } + + /** + * Unique identification of the indicator + * + * @param id unique ID + */ + public void setId(String id) { + this.id = id; + } + + protected void handleDragMouseEvent(final MouseEvent mouseEvent) { + Point2D c = getChart().getPlotArea().sceneToLocal(mouseEvent.getSceneX(), mouseEvent.getSceneY()); + final double yPosData = getAxis().getValueForDisplay(c.getY() - dragDelta.y); + + if (getAxis().isValueOnAxis(yPosData)) { + setMarkerValue(yPosData); + } + + mouseEvent.consume(); + } + + @Override + public void layoutChildren() { + if (getChart() == null) { + return; + } + final Bounds plotAreaBounds = getChart().getCanvas().getBoundsInLocal(); + final double minX = plotAreaBounds.getMinX(); + final double maxX = plotAreaBounds.getMaxX(); + final double minY = plotAreaBounds.getMinY(); + final double maxY = plotAreaBounds.getMaxY(); + + final double yPos = minY + getAxis().getDisplayPosition(getValue()); + final double axisPos; + final boolean isRightSide = getAxis().getSide().equals(Side.RIGHT); + if (isRightSide) { + axisPos = getChart().getPlotForeground().sceneToLocal(getAxis().getCanvas().localToScene(0, 0)).getX() + 2; + triangle.getPoints().setAll(0.0, 0.0, 10.0, 10.0, 50.0, 10.0, 50.0, -10.0, 10.0, -10.0); + } else { + axisPos = getChart().getPlotForeground().sceneToLocal(getAxis().getCanvas().localToScene(getAxis().getWidth(), 0)).getX() - 2; + triangle.getPoints().setAll(0.0, 0.0, -10.0, 10.0, -50.0, 10.0, -50.0, -10.0, -10.0, -10.0); + } + final double yPosGlobal = getChart().getPlotForeground().sceneToLocal(getChart().getCanvas().localToScene(0, yPos)).getY(); + + if (yPos < minY || yPos > maxY) { + getChart().getPlotForeground().getChildren().remove(triangle); + getChart().getPlotForeground().getChildren().remove(label); + getChartChildren().remove(line); + getChartChildren().remove(pickLine); + } else { + layoutLine(minX, yPos, maxX, yPos); + layoutMarker(axisPos, yPosGlobal, minX, yPos); + layoutWatchLabel(new BoundingBox(minX, yPos, maxX - minX, 0), axisPos, isRightSide); + } + } + + @Override + protected void layoutLine(double startX, double startY, double endX, double endY) { + if (!line.isVisible()) { + return; + } + super.layoutLine(startX, startY, endX, endY); + } + + protected void layoutWatchLabel(final Bounds bounds, double axisPos, boolean isRightSide) { + if (label.getText() == null || label.getText().isEmpty()) { + getChartChildren().remove(label); + return; + } + + double xPos = bounds.getMinX(); + double yPos = bounds.getMinY(); + + final double width = label.prefWidth(-1); + final double height = label.prefHeight(width); + final double baseLine = label.getBaselineOffset(); + + double padding = isRightSide ? 0 : width + label.getPadding().getRight(); + label.resizeRelocate(xPos + axisPos - padding, yPos + baseLine, width, height); + label.toFront(); + + if (!getChart().getPlotForeground().getChildren().contains(label)) { + getChart().getPlotForeground().getChildren().add(label); + } + } + + @Override + public void updateStyleClass() { + setStyleClasses(label, getId() + "-", STYLE_CLASS_LABEL); + setStyleClasses(line, getId() + "-", STYLE_CLASS_LINE); + setStyleClasses(triangle, getId() + "-", STYLE_CLASS_MARKER); + } +} 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 f4641e254..b28575b77 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 @@ -1,5 +1,14 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ package de.gsi.chart.renderer.spi.financial; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.scene.canvas.GraphicsContext; import javafx.scene.paint.Color; import javafx.scene.paint.Paint; @@ -20,11 +29,42 @@ *
  • Shadows - specific fast shadow paintings without fx-effects
  • *
  • Extension-point before/after painting - extend specific renderers by your changes to add EP rules.
  • * + * + * @author afischer */ @SuppressWarnings({ "PMD.ExcessiveParameterList" }) public abstract class AbstractFinancialRenderer extends AbstractDataSetManagement implements Renderer { protected PaintBarMarker paintBarMarker; + private final BooleanProperty computeLocalYRange = new SimpleBooleanProperty(this, "computeLocalYRange", true); + + /** + * Indicates if the chart should compute the min/max y-Axis for the local (true) or global (false) visible range + * + * @return computeLocalRange property + */ + public BooleanProperty computeLocalRangeProperty() { + return computeLocalYRange; + } + + /** + * Returns the value of the {@link #computeLocalRangeProperty()}. + * + * @return {@code true} if the local range calculation is applied, {@code false} otherwise + */ + public boolean computeLocalRange() { + return computeLocalRangeProperty().get(); + } + + /** + * Sets the value of the {@link #computeLocalRangeProperty()}. + * + * @param value {@code true} if the local range calculation is applied, {@code false} otherwise + */ + public void setComputeLocalRange(final boolean value) { + computeLocalRangeProperty().set(value); + } + /** * Inject PaintBar Marker service * @@ -93,6 +133,31 @@ protected void paintVolume(GraphicsContext gc, DataSet ds, int index, Color volu gc.fillRect(x0 - barWidthHalf, min + zzVolume, barWidth, -zzVolume); } + /** + * Re-arrange y-axis by min/max of dataset + * + * @param ds DataSet domain object which contains volume data + * @param yAxis Y-Axis DO + * @param xmin actual minimal point of x-axis + * @param xmax acutal maximal point of x-axis + */ + protected void applyLocalYRange(DataSet ds, Axis yAxis, double xmin, double xmax) { + double minYRange = Double.MAX_VALUE; + double maxYRange = Double.MIN_VALUE; + for (int i = ds.getIndex(DataSet.DIM_X, xmin) + 1; i < Math.min(ds.getIndex(DataSet.DIM_X, xmax) + 1, ds.getDataCount()); i++) { + double low = ds.get(OhlcvDataSet.DIM_Y_LOW, i); + double high = ds.get(OhlcvDataSet.DIM_Y_HIGH, i); + if (minYRange > low) { + minYRange = low; + } + if (maxYRange < high) { + maxYRange = high; + } + } + double space = (maxYRange - minYRange) * 0.05; + yAxis.set(minYRange - space, maxYRange + space); + } + // services -------------------------------------------------------- @FunctionalInterface @@ -103,36 +168,68 @@ protected interface FindAreaDistances { protected static class XMinAreaDistances implements FindAreaDistances { @Override public double[] findAreaDistances(DataSet dataset, Axis xAxis, Axis yAxis, double xmin, double xmax) { - double minDistance = Integer.MAX_VALUE; - for (int i = dataset.getIndex(DataSet.DIM_X, xmin) + 1; i < Math.min(dataset.getIndex(DataSet.DIM_X, xmax) + 1, dataset.getDataCount()); i++) { + int imin = dataset.getIndex(DataSet.DIM_X, xmin) + 1; + int imax = Math.min(dataset.getIndex(DataSet.DIM_X, xmax) + 1, dataset.getDataCount()); + int diff = imax - imin; + int incr = diff > 30 ? (int) Math.round(Math.floor(diff / 30.0)) : 1; + List distances = new ArrayList<>(); + for (int i = imin; i < imax; i = i + incr) { final double param0 = xAxis.getDisplayPosition(dataset.get(DataSet.DIM_X, i - 1)); final double param1 = xAxis.getDisplayPosition(dataset.get(DataSet.DIM_X, i)); - if (param0 != param1) { - minDistance = Math.min(minDistance, Math.abs(param1 - param0)); + distances.add(Math.abs(param1 - param0)); } } - return new double[] { minDistance }; + double popularDistance = 0.0; + if (!distances.isEmpty()) { + Collections.sort(distances); + popularDistance = getMostPopularElement(distances); + } + return new double[] { popularDistance }; } } protected static class XMinVolumeMaxAreaDistances implements FindAreaDistances { @Override public double[] findAreaDistances(DataSet dataset, Axis xAxis, Axis yAxis, double xmin, double xmax) { - double minDistance = Integer.MAX_VALUE; - double maxVolume = Integer.MIN_VALUE; - for (int i = dataset.getIndex(DataSet.DIM_X, xmin) + 1; i < Math.min(dataset.getIndex(DataSet.DIM_X, xmax) + 1, dataset.getDataCount()); i++) { - final double param0 = xAxis.getDisplayPosition(dataset.get(DataSet.DIM_X, i - 1)); - final double param1 = xAxis.getDisplayPosition(dataset.get(DataSet.DIM_X, i)); + // get most popular are distance + double[] xminAreaDistances = new XMinAreaDistances().findAreaDistances(dataset, xAxis, yAxis, xmin, xmax); + // find max volume + double maxVolume = Double.MIN_VALUE; + int imin = dataset.getIndex(DataSet.DIM_X, xmin) + 1; + int imax = Math.min(dataset.getIndex(DataSet.DIM_X, xmax) + 1, dataset.getDataCount()); + for (int i = imin; i < imax; i++) { double volume = dataset.get(OhlcvDataSet.DIM_Y_VOLUME, i); if (maxVolume < volume) { maxVolume = volume; } - if (param0 != param1) { - minDistance = Math.min(minDistance, Math.abs(param1 - param0)); + } + return new double[] { xminAreaDistances[0], maxVolume }; + } + } + + protected static Double getMostPopularElement(List a) { + int counter = 0; + int maxcounter = -1; + Double curr; + Double maxvalue; + maxvalue = curr = a.get(0); + for (Double e : a) { + if (Math.abs(curr - e) < 1e-10) { + counter++; + } else { + if (counter > maxcounter) { + maxcounter = counter; + maxvalue = curr; } + counter = 0; + curr = e; } - return new double[] { minDistance, maxVolume }; } + if (counter > maxcounter) { + maxvalue = curr; + } + + return maxvalue; } } diff --git a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/CandleStickRenderer.java b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/CandleStickRenderer.java index c2abf4bda..ded3eefc1 100644 --- a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/CandleStickRenderer.java +++ b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/CandleStickRenderer.java @@ -1,3 +1,6 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ package de.gsi.chart.renderer.spi.financial; import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.*; @@ -47,7 +50,7 @@ * * @see Candlestick Investopedia * - * @author A.Fischer + * @author afischer */ @SuppressWarnings({ "PMD.ExcessiveMethodLength", "PMD.NPathComplexity", "PMD.ExcessiveParameterList" }) // designated purpose of this class @@ -252,6 +255,10 @@ public List render(final GraphicsContext gc, final Chart chart, final i } 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()) { diff --git a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/HighLowRenderer.java b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/HighLowRenderer.java index 25486e089..5f956db43 100644 --- a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/HighLowRenderer.java +++ b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/HighLowRenderer.java @@ -1,3 +1,6 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ package de.gsi.chart.renderer.spi.financial; import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.*; @@ -42,7 +45,7 @@ * * @see OHLC Chart Ivestopedia * - * @author A.Fischer + * @author afischer */ @SuppressWarnings({ "PMD.ExcessiveMethodLength", "PMD.NPathComplexity", "PMD.ExcessiveParameterList" }) // designated purpose of this class @@ -238,6 +241,10 @@ public List render(final GraphicsContext gc, final Chart chart, final i } 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()) { 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 edd2c33d6..55d4723ed 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 @@ -3,6 +3,8 @@ import static de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConstants.*; import static de.gsi.dataset.utils.StreamUtils.CLASSPATH_PREFIX; +import java.util.Locale; + import javafx.geometry.Insets; import javafx.scene.image.Image; import javafx.scene.layout.*; @@ -17,6 +19,9 @@ import de.gsi.dataset.utils.StreamUtils; public class FinancialColorSchemeConfig implements FinancialColorSchemeAware { + protected static final String CSS_STYLESHEET = "de/gsi/chart/financial/%s.css"; + protected static final String CSS_STYLESHEET_CHART = "chart"; + public void applySchemeToDataset(String theme, String customColorScheme, DataSet dataSet, Renderer renderer) { // customization if (customColorScheme != null) { @@ -93,6 +98,14 @@ public void applyTo(String theme, String customColorScheme, XYChart chart) throw applySchemeToDataset(theme, customColorScheme, dataset, renderer); } } + + // apply css styling by theme + String cssStyleSheet = String.format(CSS_STYLESHEET, CSS_STYLESHEET_CHART + "-" + theme.toLowerCase(Locale.ROOT)); + if (getClass().getClassLoader().getResource(cssStyleSheet) == null) { // fallback + cssStyleSheet = String.format(CSS_STYLESHEET, CSS_STYLESHEET_CHART); + } + chart.getStylesheets().add(cssStyleSheet); + // predefine axis, grid, an additional chart params switch (theme) { case CLEARLOOK: diff --git a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/RendererPaintAfterEP.java b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/RendererPaintAfterEP.java index 7a749b574..98455d169 100644 --- a/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/RendererPaintAfterEP.java +++ b/chartfx-chart/src/main/java/de/gsi/chart/renderer/spi/financial/service/RendererPaintAfterEP.java @@ -1,7 +1,5 @@ package de.gsi.chart.renderer.spi.financial.service; -import de.gsi.chart.renderer.spi.financial.service.OhlcvRendererEpData; - /** * Extension point service for Renderers * Placement: Paint After bar painting diff --git a/chartfx-chart/src/main/resources/de/gsi/chart/chart.css b/chartfx-chart/src/main/resources/de/gsi/chart/chart.css index e20bc0e28..5c773f140 100644 --- a/chartfx-chart/src/main/resources/de/gsi/chart/chart.css +++ b/chartfx-chart/src/main/resources/de/gsi/chart/chart.css @@ -93,6 +93,29 @@ -fx-fill: dodgerblue; } +.value-watch-indicator-label { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-border-radius: 0.0; + -fx-font-size: 11.0; + -fx-font-weight: bold; + -fx-text-fill: white; + -fx-text-alignment: center; + -fx-padding: 2.5 4.0 1.0 8.0; +} + +.value-watch-indicator-line { + -fx-stroke-width: 1; + -fx-stroke: black; + -fx-stroke-dash-array: 8; +} + +.value-watch-indicator-marker { + -fx-stroke-width: 0.5; + -fx-stroke: black; + -fx-fill: #416ef4ff; +} + .range-indicator-rect { -fx-stroke: transparent; -fx-fill: #416ef468; diff --git a/chartfx-chart/src/main/resources/de/gsi/chart/financial/chart-blackberry.css b/chartfx-chart/src/main/resources/de/gsi/chart/financial/chart-blackberry.css new file mode 100644 index 000000000..612040bc9 --- /dev/null +++ b/chartfx-chart/src/main/resources/de/gsi/chart/financial/chart-blackberry.css @@ -0,0 +1,49 @@ +.axis-label { + -fx-fill: #f5f5f5; + -fx-axis-label-alignment: center; +} + +.value-watch-indicator-label { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-border-radius: 0; + -fx-font-size: 11; + -fx-font-weight: bold; + -fx-text-fill: white; + -fx-text-alignment: center; + -fx-padding: 2 4 1 8; +} + +.value-watch-indicator-line { + -fx-stroke-width: 1; + -fx-stroke: #696969; + -fx-stroke-dash-array: 8; +} + +.value-watch-indicator-marker { + -fx-stroke-width: 0.5; + -fx-stroke: black; + -fx-fill: #ce0614; +} + +.price-value-watch-indicator-marker { + -fx-stroke-width: 0.5; + -fx-stroke: black; + -fx-fill: #3bdacd; +} + +.range-indicator-label { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-border-radius: 0; + -fx-font-size: 12; + -fx-font-weight: bold; + -fx-text-fill: white; + -fx-text-alignment: center; + -fx-padding: 1 2 1 2; +} + +.range-indicator-rect { + -fx-stroke: transparent; + -fx-fill: #9c1d1d70; +} diff --git a/chartfx-chart/src/main/resources/de/gsi/chart/financial/chart-dark.css b/chartfx-chart/src/main/resources/de/gsi/chart/financial/chart-dark.css new file mode 100644 index 000000000..20fc97d74 --- /dev/null +++ b/chartfx-chart/src/main/resources/de/gsi/chart/financial/chart-dark.css @@ -0,0 +1,49 @@ +.axis-label { + -fx-fill: #c2c2c2ff; + -fx-axis-label-alignment: center; +} + +.value-watch-indicator-label { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-border-radius: 0; + -fx-font-size: 11; + -fx-font-weight: bold; + -fx-text-fill: white; + -fx-text-alignment: center; + -fx-padding: 2 4 1 8; +} + +.value-watch-indicator-line { + -fx-stroke-width: 1; + -fx-stroke: #a8a8a8; + -fx-stroke-dash-array: 8; +} + +.value-watch-indicator-marker { + -fx-stroke-width: 0.5; + -fx-stroke: black; + -fx-fill: #ce0614; +} + +.price-value-watch-indicator-marker { + -fx-stroke-width: 0.5; + -fx-stroke: black; + -fx-fill: #3bdacd; +} + +.range-indicator-label { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-border-radius: 0; + -fx-font-size: 12; + -fx-font-weight: bold; + -fx-text-fill: white; + -fx-text-alignment: center; + -fx-padding: 1 2 1 2; +} + +.range-indicator-rect { + -fx-stroke: transparent; + -fx-fill: #9c1d1d70; +} diff --git a/chartfx-chart/src/main/resources/de/gsi/chart/financial/chart.css b/chartfx-chart/src/main/resources/de/gsi/chart/financial/chart.css new file mode 100644 index 000000000..0b8f7da3a --- /dev/null +++ b/chartfx-chart/src/main/resources/de/gsi/chart/financial/chart.css @@ -0,0 +1,44 @@ +.value-watch-indicator-label { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-border-radius: 0; + -fx-font-size: 11; + -fx-font-weight: bold; + -fx-text-fill: white; + -fx-text-alignment: center; + -fx-padding: 2.5 4 1 8; +} + +.value-watch-indicator-line { + -fx-stroke-width: 1; + -fx-stroke: black; + -fx-stroke-dash-array: 8; +} + +.value-watch-indicator-marker { + -fx-stroke-width: 0.5; + -fx-stroke: black; + -fx-fill: #ce0614; +} + +.price-value-watch-indicator-marker { + -fx-stroke-width: 0.5; + -fx-stroke: black; + -fx-fill: #78015b; +} + +.range-indicator-label { + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-border-radius: 0; + -fx-font-size: 12; + -fx-font-weight: bold; + -fx-text-fill: white; + -fx-text-alignment: center; + -fx-padding: 1 2 1 2; +} + +.range-indicator-rect { + -fx-stroke: transparent; + -fx-fill: #9c1d1d70; +} diff --git a/chartfx-chart/src/test/java/de/gsi/chart/plugins/YWatchValueIndicatorTest.java b/chartfx-chart/src/test/java/de/gsi/chart/plugins/YWatchValueIndicatorTest.java new file mode 100644 index 000000000..5623aac52 --- /dev/null +++ b/chartfx-chart/src/test/java/de/gsi/chart/plugins/YWatchValueIndicatorTest.java @@ -0,0 +1,114 @@ +package de.gsi.chart.plugins; + +import static org.junit.jupiter.api.Assertions.*; + +import static de.gsi.chart.renderer.spi.financial.utils.FinancialTestUtils.generateCosData; + +import javafx.scene.Scene; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.testfx.framework.junit5.ApplicationExtension; +import org.testfx.framework.junit5.Start; + +import de.gsi.chart.XYChart; +import de.gsi.chart.axes.spi.DefaultNumericAxis; +import de.gsi.chart.ui.geometry.Side; +import de.gsi.chart.ui.utils.JavaFXInterceptorUtils.SelectiveJavaFxInterceptor; +import de.gsi.chart.ui.utils.TestFx; +import de.gsi.dataset.spi.DefaultErrorDataSet; + +@ExtendWith(ApplicationExtension.class) +@ExtendWith(SelectiveJavaFxInterceptor.class) +class YWatchValueIndicatorTest { + private YWatchValueIndicator valueWatchIndicatorTested; + private XYChart chart; + private DefaultNumericAxis yAxis; + + @Start + public void start(Stage stage) throws Exception { + // check flow in the category too + final DefaultNumericAxis xAxis = new DefaultNumericAxis("time", "iso"); + xAxis.setTimeAxis(true); + yAxis = new DefaultNumericAxis("price", "points"); + + valueWatchIndicatorTested = new YWatchValueIndicator(yAxis, "%1.2f"); // auto + valueWatchIndicatorTested = new YWatchValueIndicator(yAxis, "%1.2f", 50.12); + + final DefaultErrorDataSet dataSet = new DefaultErrorDataSet("TestData"); + generateCosData(dataSet); + + // prepare chart structure + chart = new XYChart(xAxis, yAxis); + chart.getDatasets().add(dataSet); + stage.setScene(new Scene(chart)); + stage.show(); + } + + @TestFx + void leftSide() { + chart.getPlugins().add(valueWatchIndicatorTested); + } + + @TestFx + void rightSide() { + yAxis.setSide(Side.RIGHT); + chart.layoutChildren(); + chart.getPlugins().add(valueWatchIndicatorTested); + + // change to unseen position + yAxis.setAutoRanging(false); + yAxis.set(100.0, 150.0); + valueWatchIndicatorTested.layoutChildren(); + + // test drag mouse on it + valueWatchIndicatorTested.handleDragMouseEvent(new MyMouseEvent(15, 25, MouseButton.PRIMARY, 1)); + } + + @TestFx + void setMarkerValue() { + chart.getPlugins().add(valueWatchIndicatorTested); + valueWatchIndicatorTested.setMarkerValue(35.15); + assertEquals("35.15", valueWatchIndicatorTested.getText()); + assertEquals(35.15, valueWatchIndicatorTested.getValue(), 1e-2); + } + + @TestFx + void setLineVisible() { + chart.getPlugins().add(valueWatchIndicatorTested); + valueWatchIndicatorTested.setLineVisible(true); + valueWatchIndicatorTested.setLineVisible(false); + } + + @Test + void setId() { + valueWatchIndicatorTested.setId("price"); + assertEquals("price", valueWatchIndicatorTested.getId()); + } + + private static class MyMouseEvent extends MouseEvent { + private static final long serialVersionUID = 0L; + + MyMouseEvent(final double x, final double y, final MouseButton mouseButton, final int clickCount) { + super(MouseEvent.MOUSE_MOVED, x, y, // + x, y, // screen coordinates + mouseButton, // mouse button + clickCount, // clickCount + false, // shiftDown + false, // controlDown + false, // altDown + false, // metaDown + MouseButton.PRIMARY.equals(mouseButton), // primaryButtonDown + MouseButton.MIDDLE.equals(mouseButton), // middleButtonDown + MouseButton.SECONDARY.equals(mouseButton), // secondaryButtonDown + true, // synthesised + false, // popupTrigger + true, // stillSincePress + null // pickResult + ); + } + } +} 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 bc63a2851..0a48f0612 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 @@ -4,6 +4,9 @@ import static de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConstants.SAND; +import java.security.InvalidParameterException; +import java.util.Calendar; + import javafx.scene.Scene; import javafx.scene.paint.Color; import javafx.stage.Stage; @@ -19,36 +22,51 @@ 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.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.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 CandleStickRendererTest { private CandleStickRenderer rendererTested; private XYChart chart; + private OhlcvDataSet ohlcvDataSet; @Start public void start(Stage stage) throws Exception { - OhlcvDataSet ohlcvDataSet = new OhlcvDataSet("ohlc1"); + ProcessingProfiler.setDebugState(true); + ohlcvDataSet = new OhlcvDataSet("ohlc1"); ohlcvDataSet.setData(FinancialTestUtils.createTestOhlcv()); rendererTested = new CandleStickRenderer(true); + rendererTested.setComputeLocalRange(false); + rendererTested.setComputeLocalRange(true); - // check flow in the category too - final CategoryAxis xAxis = new CategoryAxis("time [iso]"); - xAxis.setTickLabelRotation(90); - xAxis.setOverlapPolicy(AxisLabelOverlapPolicy.SKIP_ALT); + assertNull(rendererTested.getPaintBarColor(null)); + + 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 @@ -59,10 +77,21 @@ public void start(Stage stage) throws Exception { new FinancialColorSchemeConfig().applyTo(SAND, chart); - stage.setScene(new Scene(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(); @@ -93,7 +122,7 @@ public DataSet set(DataSet other, boolean copy) { } @Test - public void testVolumeContructor() { + public void testVolumeConstructor() { CandleStickRenderer candleStickRenderer = new CandleStickRenderer(true); assertTrue(candleStickRenderer.isPaintVolume()); candleStickRenderer = new CandleStickRenderer(false); @@ -102,6 +131,11 @@ public void testVolumeContructor() { assertFalse(candleStickRenderer.isPaintVolume()); } + @Test + public void noXyChartInstance() { + assertThrows(InvalidParameterException.class, () -> rendererTested.render(null, new TestChart(), 0, null)); + } + @Test void getThis() { assertEquals(CandleStickRenderer.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 c27c3cbdd..600a789cb 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 @@ -4,6 +4,9 @@ import static de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConstants.SAND; +import java.security.InvalidParameterException; +import java.util.Calendar; + import javafx.scene.Scene; import javafx.scene.paint.Color; import javafx.stage.Stage; @@ -19,36 +22,51 @@ 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.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.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 HighLowRendererTest { private HighLowRenderer rendererTested; private XYChart chart; + private OhlcvDataSet ohlcvDataSet; @Start public void start(Stage stage) throws Exception { - OhlcvDataSet ohlcvDataSet = new OhlcvDataSet("ohlc1"); + ProcessingProfiler.setDebugState(true); + ohlcvDataSet = new OhlcvDataSet("ohlc1"); ohlcvDataSet.setData(FinancialTestUtils.createTestOhlcv()); - rendererTested = new HighLowRenderer(); + rendererTested = new HighLowRenderer(true); + rendererTested.setComputeLocalRange(false); + rendererTested.setComputeLocalRange(true); - // check flow in the category too - final CategoryAxis xAxis = new CategoryAxis("time [iso]"); - xAxis.setTickLabelRotation(90); - xAxis.setOverlapPolicy(AxisLabelOverlapPolicy.SKIP_ALT); + assertNull(rendererTested.getPaintBarColor(null)); + + 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 @@ -59,10 +77,21 @@ public void start(Stage stage) throws Exception { new FinancialColorSchemeConfig().applyTo(SAND, chart); - stage.setScene(new Scene(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(); @@ -102,6 +131,11 @@ public void testVolumeContructor() { assertFalse(highLowRenderer.isPaintVolume()); } + @Test + public void noXyChartInstance() { + assertThrows(InvalidParameterException.class, () -> rendererTested.render(null, new TestChart(), 0, null)); + } + @Test void getThis() { assertEquals(HighLowRenderer.class, rendererTested.getThis().getClass()); diff --git a/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/utils/CalendarUtils.java b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/utils/CalendarUtils.java new file mode 100644 index 000000000..6f9c728b4 --- /dev/null +++ b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/utils/CalendarUtils.java @@ -0,0 +1,117 @@ +package de.gsi.chart.renderer.spi.financial.utils; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +public class CalendarUtils { + /** + * Create the calendar interval instance by date interval pattern: + * yyyy/MM/dd-yyyy/MM/dd + * for example: 2017/12/01-2017/12/22 + * + * @param dateIntervalPattern String + * @return calendar interval instance + * @throws ParseException parsing fails + */ + public static Interval createByDateInterval(String dateIntervalPattern) throws ParseException { + if (dateIntervalPattern == null) { + throw new ParseException("The resource date interval pattern is null", -1); + } + String[] parts = dateIntervalPattern.split("-"); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); + List calendarList = new ArrayList<>(); + for (String time : parts) { + Calendar cal = Calendar.getInstance(); + cal.setTime(sdf.parse(time)); + cal.set(Calendar.HOUR_OF_DAY, 0); + cal.set(Calendar.MINUTE, 0); + cal.set(Calendar.SECOND, 0); + cal.set(Calendar.MILLISECOND, 0); + calendarList.add(cal); + } + + return new Interval<>(calendarList.get(0), calendarList.get(1)); + } + + /** + * Create the calendar interval instance by datetime interval pattern: + * yyyy/MM/dd HH:mm-yyyy/MM/dd HH:mm + * for example: 2017/12/01 15:30-2017/12/22 22:15 + * + * @param datetimeIntervalPattern String + * @return calendar interval instance + * @throws ParseException parsing fails + */ + public static Interval createByDateTimeInterval(String datetimeIntervalPattern) throws ParseException { + if (datetimeIntervalPattern == null) { + throw new ParseException("The resource datetime interval pattern is null", -1); + } + String[] parts = datetimeIntervalPattern.split("-"); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm"); + List calendarList = new ArrayList<>(); + for (String time : parts) { + Date fromTotime = sdf.parse(time); + Calendar cal = Calendar.getInstance(); + cal.setTime(fromTotime); + cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DATE), + cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), 0); + calendarList.add(cal); + } + + return new Interval<>(calendarList.get(0), calendarList.get(1)); + } + + /** + * Create the calendar interval instance by time interval pattern: + * HH:mm-HH:mm + * for example: 15:30-22:15 + * + * @param timeIntervalPattern String + * @return calendar interval instance + * @throws ParseException parsing fails + */ + public static Interval createByTimeInterval(String timeIntervalPattern) throws ParseException { + if (timeIntervalPattern == null) { + throw new ParseException("The resource time interval pattern is null", -1); + } + String[] parts = timeIntervalPattern.split("-"); + SimpleDateFormat sdf = new SimpleDateFormat("HH:mm"); + List calendarList = new ArrayList<>(); + for (String time : parts) { + Date fromTotime = sdf.parse(time); + Calendar cal = Calendar.getInstance(); + cal.setTime(fromTotime); + cal.set(1900, Calendar.JANUARY, 1, cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), 0); + calendarList.add(cal); + } + + return new Interval<>(calendarList.get(0), calendarList.get(1)); + } + + /** + * Create the calendar instance by datetime pattern: + * yyyy/MM/dd HH:mm + * for example: 2017/12/01 15:30 + * + * @param datetimePattern String + * @return calendar interval instance + * @throws ParseException parsing fails + */ + public static Calendar createByDateTime(String datetimePattern) throws ParseException { + if (datetimePattern == null) { + throw new ParseException("The resource datetime pattern is null", -1); + } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm"); + Date fromTotime = sdf.parse(datetimePattern); + Calendar cal = Calendar.getInstance(); + cal.setTime(fromTotime); + cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DATE), + cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), 0); + + return cal; + } +} diff --git a/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/utils/FinancialTestUtils.java b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/utils/FinancialTestUtils.java index df23afb94..2414d0fec 100644 --- a/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/utils/FinancialTestUtils.java +++ b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/utils/FinancialTestUtils.java @@ -4,20 +4,72 @@ import java.time.ZoneId; import java.util.Date; +import javafx.application.Platform; +import javafx.collections.ListChangeListener; +import javafx.scene.control.Label; + +import de.gsi.chart.Chart; +import de.gsi.chart.axes.Axis; +import de.gsi.dataset.spi.DefaultErrorDataSet; import de.gsi.dataset.spi.financial.api.ohlcv.IOhlcv; +import de.gsi.dataset.utils.ProcessingProfiler; public class FinancialTestUtils { + private static final int N_SAMPLES = 10_000; // default: 10000 + public static IOhlcv createTestOhlcv() { LocalDate date = LocalDate.parse("2020-11-19"); return new Ohlcv() .addOhlcvItem(new OhlcvItem(toDate(date.plusDays(1)), 3001.0, 3005.0, 3000.10, 3002.84, 15001.0, 18007.0)) - .addOhlcvItem(new OhlcvItem(toDate(date.plusDays(1)), 3002.0, 3007.0, 3001.35, 3005.64, 16005.0, 19002.0)) - .addOhlcvItem(new OhlcvItem(toDate(date.plusDays(1)), 3003.0, 3009.15, 3002.50, 3008.75, 14004.0, 20005.0)) - .addOhlcvItem(new OhlcvItem(toDate(date.plusDays(1)), 2999.0, 3000.75, 2997.15, 2998.10, 100085.0, 35001.0)) - .addOhlcvItem(new OhlcvItem(toDate(date.plusDays(1)), 2996.0, 2998.0, 2994.10, 2993.50, 135001.0, 64010.0)); + .addOhlcvItem(new OhlcvItem(toDate(date.plusDays(2)), 3002.0, 3007.0, 3001.35, 3005.64, 16005.0, 19002.0)) + .addOhlcvItem(new OhlcvItem(toDate(date.plusDays(3)), 3003.0, 3009.15, 3002.50, 3008.75, 14004.0, 20005.0)) + .addOhlcvItem(new OhlcvItem(toDate(date.plusDays(4)), 2999.0, 3000.75, 2997.15, 2998.10, 100085.0, 35001.0)) + .addOhlcvItem(new OhlcvItem(toDate(date.plusDays(5)), 2996.0, 2998.0, 2990.10, 2992.50, 135001.0, 64010.0)); + } + + public static void generateCosData(final DefaultErrorDataSet dataSet) { + final long startTime = ProcessingProfiler.getTimeStamp(); + + dataSet.autoNotification().set(false); + dataSet.clearData(); + final double now = System.currentTimeMillis() / 1000.0 + 1; // N.B. '+1' + for (int n = 0; n < N_SAMPLES; n++) { + double t = now + n * 10; + t *= +1; + final double y = 100 * Math.cos(Math.PI * t * 0.0005) + 0 * 0.001 * (t - now) + 0 * 1e4; + final double ex = 0.1; + final double ey = 10; + dataSet.add(t, y, ex, ey); + } + dataSet.autoNotification().set(true); + + Platform.runLater(() -> dataSet.fireInvalidated(null)); + ProcessingProfiler.getTimeDiff(startTime, "adding data into DataSet"); } public static Date toDate(LocalDate ldate) { return Date.from(ldate.atStartOfDay(ZoneId.systemDefault()).toInstant()); } + + @SuppressWarnings({ "PMD.UncommentedEmptyMethodBody" }) + public static class TestChart extends Chart { + @Override + public void updateAxisRange() { + // use for test only + } + + @Override + protected void axesChanged(ListChangeListener.Change change) { + // use for test only + } + + @Override + protected void redrawCanvas() { + // use for test only + } + + public Label getTitlePaint() { + return titleLabel; + } + } } diff --git a/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/utils/Interval.java b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/utils/Interval.java new file mode 100644 index 000000000..1f3609bc5 --- /dev/null +++ b/chartfx-chart/src/test/java/de/gsi/chart/renderer/spi/financial/utils/Interval.java @@ -0,0 +1,45 @@ +package de.gsi.chart.renderer.spi.financial.utils; + +public class Interval { + public T from; + public T to; + + public Interval(T from, T to) { + this.from = from; + this.to = to; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((from == null) ? 0 : from.hashCode()); + result = prime * result + ((to == null) ? 0 : to.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Interval other = (Interval) obj; + if (from == null) { + if (other.from != null) + return false; + } else if (!from.equals(other.from)) + return false; + if (to == null) { + return other.to == null; + } else + return to.equals(other.to); + } + + @Override + public String toString() { + return "Interval [from=" + from + ", to=" + to + "]"; + } +} 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 1ee6bf503..a95f8a64d 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 @@ -19,6 +19,7 @@ import de.gsi.chart.samples.financial.FinancialAdvancedCandlestickSample; import de.gsi.chart.samples.financial.FinancialCandlestickSample; import de.gsi.chart.samples.financial.FinancialHiLowSample; +import de.gsi.chart.samples.financial.FinancialRealtimeCandlestickSample; import de.gsi.chart.utils.PeriodicScreenCapture; /** @@ -60,6 +61,7 @@ public void start(final Stage primaryStage) { buttons.getChildren().add(new MyButton("FinancialCandlestickSample", new FinancialCandlestickSample())); buttons.getChildren().add(new MyButton("FinancialHiLowSample", new FinancialHiLowSample())); buttons.getChildren().add(new MyButton("FinancialAdvancedCandlestickSample", new FinancialAdvancedCandlestickSample())); + buttons.getChildren().add(new MyButton("FinancialRealtimeCandlestickSample", new FinancialRealtimeCandlestickSample())); 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/chart/samples/financial/AbstractBasicFinancialApplication.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/AbstractBasicFinancialApplication.java index e174e5a11..701daf55a 100644 --- a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/AbstractBasicFinancialApplication.java +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/AbstractBasicFinancialApplication.java @@ -1,9 +1,15 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ + package de.gsi.chart.samples.financial; import static de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConstants.getDefaultColorSchemes; +import static de.gsi.chart.ui.ProfilerInfoBox.DebugLevel.VERSION; import java.io.IOException; import java.text.ParseException; +import java.time.ZoneOffset; import java.util.Arrays; import java.util.Calendar; @@ -11,23 +17,38 @@ import javafx.application.Platform; import javafx.geometry.Pos; import javafx.scene.Scene; +import javafx.scene.control.*; import javafx.scene.layout.FlowPane; +import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; +import javafx.scene.layout.Priority; import javafx.stage.Stage; +import javafx.stage.WindowEvent; import org.apache.commons.math3.stat.descriptive.DescriptiveStatistics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import de.gsi.chart.Chart; import de.gsi.chart.XYChart; import de.gsi.chart.axes.AxisLabelOverlapPolicy; +import de.gsi.chart.axes.AxisMode; import de.gsi.chart.axes.spi.DefaultNumericAxis; +import de.gsi.chart.axes.spi.format.DefaultTimeFormatter; +import de.gsi.chart.plugins.ChartPlugin; import de.gsi.chart.plugins.DataPointTooltip; import de.gsi.chart.plugins.EditAxis; import de.gsi.chart.plugins.Zoomer; +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.chart.samples.financial.dos.Interval; import de.gsi.chart.samples.financial.service.CalendarUtils; import de.gsi.chart.samples.financial.service.SimpleOhlcvDailyParser; +import de.gsi.chart.samples.financial.service.SimpleOhlcvReplayDataSet; +import de.gsi.chart.samples.financial.service.SimpleOhlcvReplayDataSet.DataInput; +import de.gsi.chart.samples.financial.service.period.IntradayPeriod; +import de.gsi.chart.ui.ProfilerInfoBox; import de.gsi.chart.ui.geometry.Side; import de.gsi.dataset.spi.DefaultDataSet; import de.gsi.dataset.spi.financial.OhlcvDataSet; @@ -35,18 +56,39 @@ import de.gsi.dataset.spi.financial.api.ohlcv.IOhlcvItem; import de.gsi.dataset.utils.ProcessingProfiler; +/** + * Base class for demonstration of financial charts. + * This abstract class assemblies and configures important chart components and elements for financial charts. + * Any part can be overridden and modified for final Sample test. + * + * @author afischer + */ public abstract class AbstractBasicFinancialApplication extends Application { - protected static final int prefChartWidth = 640; //1024 - protected static final int prefChartHeight = 480; //768 - protected static final int prefSceneWidth = 1920; - protected static final int prefSceneHeight = 1080; + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractBasicFinancialApplication.class); + + protected int prefChartWidth = 640; //1024 + protected int prefChartHeight = 480; //768 + protected int prefSceneWidth = 1920; + protected int prefSceneHeight = 1080; + + private final double UPDATE_PERIOD = 10.0; // replay multiple + protected int DEBUG_UPDATE_RATE = 500; - protected static String resource = "@ES-[TF1D]"; - protected static String timeRange = "2020/08/24-2020/11/12"; + 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; // injection private 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"); + + private boolean timerActivated = false; + @Override public void start(final Stage primaryStage) { ProcessingProfiler.setVerboseOutputState(true); @@ -57,44 +99,114 @@ public void start(final Stage primaryStage) { ProcessingProfiler.getTimeDiff(startTime, "adding data to chart"); startTime = ProcessingProfiler.getTimeStamp(); - // create and prepare chart to the root - Pane root = prepareCharts(); - - final Scene scene = new Scene(root, prefSceneWidth, prefSceneHeight); + Scene scene = prepareScene(); ProcessingProfiler.getTimeDiff(startTime, "adding chart into StackPane"); startTime = ProcessingProfiler.getTimeStamp(); primaryStage.setTitle(this.getClass().getSimpleName()); primaryStage.setScene(scene); - primaryStage.setOnCloseRequest(evt -> Platform.exit()); + primaryStage.setOnCloseRequest(this::closeDemo); primaryStage.show(); ProcessingProfiler.getTimeDiff(startTime, "for showing"); + + // ensure correct state after restart demo + stopTimer(); + } + + protected void closeDemo(final WindowEvent evt) { + if (evt.getEventType().equals(WindowEvent.WINDOW_CLOSE_REQUEST) && LOGGER.isInfoEnabled()) { + LOGGER.atInfo().log("requested demo to shut down"); + } + stopTimer(); + Platform.exit(); + } + + protected ToolBar getTestToolBar(Chart chart, AbstractFinancialRenderer renderer, boolean replaySupport) { + ToolBar testVariableToolBar = new ToolBar(); + localRange.setSelected(renderer.computeLocalRange()); + localRange.setTooltip(new Tooltip("select for auto-adjusting min/max the y-axis (prices)")); + localRange.selectedProperty().bindBidirectional(renderer.computeLocalRangeProperty()); + localRange.selectedProperty().addListener((ch, old, selection) -> { + for (ChartPlugin plugin : chart.getPlugins()) { + if (plugin instanceof Zoomer) { + ((Zoomer) plugin).setAxisMode(selection ? AxisMode.X : AxisMode.XY); + } + } + chart.requestLayout(); + }); + + Button periodicTimer = null; + if (replaySupport) { + // repetitively generate new data + periodicTimer = new Button("replay"); + periodicTimer.setTooltip(new Tooltip("replay instrument data in realtime")); + periodicTimer.setOnAction(evt -> pauseResumeTimer()); + + updatePeriod.valueProperty().addListener((ch, o, n) -> updateTimer()); + updatePeriod.setEditable(true); + updatePeriod.setPrefWidth(80); + } + + final ProfilerInfoBox profilerInfoBox = new ProfilerInfoBox(DEBUG_UPDATE_RATE); + profilerInfoBox.setDebugLevel(VERSION); + + final Pane spacer = new Pane(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + if (replaySupport) { + testVariableToolBar.getItems().addAll(localRange, periodicTimer, updatePeriod, new Label("[multiply]"), spacer, profilerInfoBox); + } else { + testVariableToolBar.getItems().addAll(localRange, spacer, profilerInfoBox); + } + + return testVariableToolBar; } /** * Prepare charts to the root. + * + * @return prepared scene for sample app */ - protected Pane prepareCharts() { + protected Scene prepareScene() { // show all default financial color schemes final FlowPane root = new FlowPane(); root.setAlignment(Pos.CENTER); Chart[] charts = Arrays.stream(getDefaultColorSchemes()).map(this::getDefaultFinancialTestChart).toArray(Chart[] ::new); root.getChildren().addAll(charts); - return root; + return new Scene(root, prefSceneWidth, prefSceneHeight); } /** * Default financial chart configuration + * + * @param theme defines theme which has to be used for sample app */ protected Chart getDefaultFinancialTestChart(final String theme) { // load datasets - final OhlcvDataSet ohlcvDataSet = new OhlcvDataSet(resource); - final DefaultDataSet indiSet = new DefaultDataSet("MA(24)"); - try { - loadTestData(resource, ohlcvDataSet, indiSet); - } catch (IOException e) { - throw new IllegalArgumentException(e.getMessage(), e); + DefaultDataSet indiSet = null; + if (resource.startsWith("REALTIME")) { + try { + Interval timeRangeInt = CalendarUtils.createByDateTimeInterval(timeRange); + Interval ttInt = CalendarUtils.createByTimeInterval(tt); + Calendar replayFromCal = CalendarUtils.createByDateTime(replayFrom); + ohlcvDataSet = new SimpleOhlcvReplayDataSet( + DataInput.valueOf(resource.substring("REALTIME-".length())), + period, + timeRangeInt, + ttInt, + replayFromCal); + } catch (ParseException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + } else { + ohlcvDataSet = new OhlcvDataSet(resource); + indiSet = new DefaultDataSet("MA(24)"); + try { + loadTestData(resource, ohlcvDataSet, indiSet); + } catch (IOException e) { + throw new IllegalArgumentException(e.getMessage(), e); + } } // prepare axis @@ -103,6 +215,12 @@ protected Chart getDefaultFinancialTestChart(final String theme) { xAxis1.setAutoRangeRounding(false); xAxis1.setTimeAxis(true); + // set localised time offset + if (xAxis1.isTimeAxis() && xAxis1.getAxisLabelFormatter() instanceof DefaultTimeFormatter) { + final DefaultTimeFormatter axisFormatter = (DefaultTimeFormatter) xAxis1.getAxisLabelFormatter(); + axisFormatter.setTimeZoneOffset(ZoneOffset.ofHoursMinutes(2, 0)); + } + // category axis support tests //final CategoryAxis xAxis = new CategoryAxis("time [iso]"); //xAxis.setTickLabelRotation(90); @@ -119,7 +237,7 @@ protected Chart getDefaultFinancialTestChart(final String theme) { chart.setAnimated(false); // prepare plugins - chart.getPlugins().add(new Zoomer()); + chart.getPlugins().add(new Zoomer(AxisMode.X)); chart.getPlugins().add(new EditAxis()); chart.getPlugins().add(new DataPointTooltip()); @@ -139,20 +257,27 @@ protected Chart getDefaultFinancialTestChart(final String theme) { } // zoom to specific time range - showPredefinedTimeRange(timeRange, ohlcvDataSet, xAxis1, yAxis1); + if (timeRange != null) { + showPredefinedTimeRange(timeRange, ohlcvDataSet, xAxis1, yAxis1); + } return chart; } /** * Show required part of the OHLC resource + * + * @param dateIntervalPattern from to pattern for time range + * @param ohlcvDataSet domain object with filled ohlcv data + * @param xaxis X-axis for settings + * @param yaxis Y-axis for settings */ protected void showPredefinedTimeRange(String dateIntervalPattern, OhlcvDataSet ohlcvDataSet, DefaultNumericAxis xaxis, DefaultNumericAxis yaxis) { try { - Calendar[] fromTo = CalendarUtils.createByDateInterval(dateIntervalPattern); - double fromTime = fromTo[0].getTime().getTime() / 1000.0; - double toTime = fromTo[1].getTime().getTime() / 1000.0; + Interval fromTo = CalendarUtils.createByDateTimeInterval(dateIntervalPattern); + double fromTime = fromTo.from.getTime().getTime() / 1000.0; + double toTime = fromTo.to.getTime().getTime() / 1000.0; int fromIdx = ohlcvDataSet.getXIndex(fromTime); int toIdx = ohlcvDataSet.getXIndex(toTime); @@ -167,13 +292,11 @@ protected void showPredefinedTimeRange(String dateIntervalPattern, OhlcvDataSet min = ohlcvItem.getLow(); } } - xaxis.setAutoRanging(false); xaxis.set(fromTime, toTime); - yaxis.setAutoRanging(false); yaxis.set(min, max); - xaxis.forceRedraw(); - yaxis.forceRedraw(); + xaxis.setAutoRanging(false); + yaxis.setAutoRanging(false); } catch (ParseException e) { throw new IllegalArgumentException(e.getMessage(), e); @@ -183,6 +306,9 @@ protected void showPredefinedTimeRange(String dateIntervalPattern, OhlcvDataSet /** * Load OHLC structures and indi calc * + * @param data required data + * @param dataSet dataset which will be filled by this data + * @param indiSet example of indicator calculation * @throws IOException if loading fails */ protected void loadTestData(String data, final OhlcvDataSet dataSet, DefaultDataSet indiSet) throws IOException { @@ -212,10 +338,42 @@ protected void loadTestData(String data, final OhlcvDataSet dataSet, DefaultData */ protected abstract void prepareRenderers(XYChart chart, OhlcvDataSet ohlcvDataSet, DefaultDataSet indiSet); + //--------- replay support --------- + + private void pauseResumeTimer() { + if (!timerActivated) { + startTimer(); + } else if (ohlcvDataSet instanceof SimpleOhlcvReplayDataSet) { + ((SimpleOhlcvReplayDataSet) ohlcvDataSet).pauseResume(); + } + } + + private void updateTimer() { + if (timerActivated) { + startTimer(); + } + } + + private void startTimer() { + if (ohlcvDataSet instanceof SimpleOhlcvReplayDataSet) { + SimpleOhlcvReplayDataSet realtimeDataSet = (SimpleOhlcvReplayDataSet) ohlcvDataSet; + realtimeDataSet.setUpdatePeriod(updatePeriod.getValue()); + timerActivated = true; + } + } + + private void stopTimer() { + if (timerActivated && ohlcvDataSet instanceof SimpleOhlcvReplayDataSet) { + timerActivated = false; + SimpleOhlcvReplayDataSet realtimeDataSet = (SimpleOhlcvReplayDataSet) ohlcvDataSet; + realtimeDataSet.stop(); + } + } + /** * @param args the command line arguments */ public static void main(final String[] args) { Application.launch(args); } -} \ No newline at end of file +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialAdvancedCandlestickSample.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialAdvancedCandlestickSample.java index 5ecb1a085..75be2aa16 100644 --- a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialAdvancedCandlestickSample.java +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialAdvancedCandlestickSample.java @@ -1,15 +1,22 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ package de.gsi.chart.samples.financial; import java.util.Calendar; import javafx.application.Application; -import javafx.scene.layout.BorderPane; -import javafx.scene.layout.Pane; +import javafx.scene.Scene; +import javafx.scene.control.ToolBar; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; import javafx.scene.paint.Color; +import de.gsi.chart.Chart; 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.AbstractFinancialRenderer; import de.gsi.chart.renderer.spi.financial.CandleStickRenderer; import de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConstants; import de.gsi.dataset.spi.DefaultDataSet; @@ -17,19 +24,31 @@ import de.gsi.dataset.spi.financial.api.attrs.AttributeKey; import de.gsi.dataset.spi.financial.api.ohlcv.IOhlcvItem; +/** + * Advanced configuration of Candlestick renderer. Support for PaintBars and extension points. + * + * @author afischer + */ public class FinancialAdvancedCandlestickSample extends AbstractBasicFinancialApplication { public static final AttributeKey MARK_BAR = AttributeKey.create(Boolean.class, "MARK_BAR"); /** * Prepare charts to the root. */ - protected Pane prepareCharts() { - timeRange = "2020/06/24-2020/11/12"; + protected Scene prepareScene() { + timeRange = "2020/06/24 0:00-2020/11/12 0:00"; - final BorderPane root = new BorderPane(); - root.setCenter(getDefaultFinancialTestChart(FinancialColorSchemeConstants.SAND)); + final Chart chart = getDefaultFinancialTestChart(FinancialColorSchemeConstants.SAND); + final AbstractFinancialRenderer renderer = (AbstractFinancialRenderer) chart.getRenderers().get(0); - return root; + // prepare top financial toolbar + ToolBar testVariableToolBar = getTestToolBar(chart, renderer, false); + + VBox root = new VBox(); + VBox.setVgrow(chart, Priority.SOMETIMES); + root.getChildren().addAll(testVariableToolBar, chart); + + return new Scene(root, prefSceneWidth, prefSceneHeight); } protected void prepareRenderers(XYChart chart, OhlcvDataSet ohlcvDataSet, DefaultDataSet indiSet) { diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialCandlestickSample.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialCandlestickSample.java index 1a11cde8f..ad85f1ee1 100644 --- a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialCandlestickSample.java +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialCandlestickSample.java @@ -1,3 +1,6 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ package de.gsi.chart.samples.financial; import javafx.application.Application; @@ -9,6 +12,11 @@ import de.gsi.dataset.spi.DefaultDataSet; import de.gsi.dataset.spi.financial.OhlcvDataSet; +/** + * Candlestick Renderer Sample + * + * @author afischer + */ public class FinancialCandlestickSample extends AbstractBasicFinancialApplication { protected void prepareRenderers(XYChart chart, OhlcvDataSet ohlcvDataSet, DefaultDataSet indiSet) { // create and apply renderers diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialHiLowSample.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialHiLowSample.java index 94d97789d..25bccbc30 100644 --- a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialHiLowSample.java +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialHiLowSample.java @@ -1,3 +1,6 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ package de.gsi.chart.samples.financial; import javafx.application.Application; @@ -9,6 +12,11 @@ import de.gsi.dataset.spi.DefaultDataSet; import de.gsi.dataset.spi.financial.OhlcvDataSet; +/** + * OHLC (HiLo) Renderer Sample + * + * @author afischer + */ public class FinancialHiLowSample extends AbstractBasicFinancialApplication { protected void prepareRenderers(XYChart chart, OhlcvDataSet ohlcvDataSet, DefaultDataSet indiSet) { // create and apply renderers diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialRealtimeCandlestickSample.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialRealtimeCandlestickSample.java new file mode 100644 index 000000000..4bc87cb99 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialRealtimeCandlestickSample.java @@ -0,0 +1,106 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ +package de.gsi.chart.samples.financial; + +import static de.gsi.chart.samples.financial.service.period.IntradayPeriod.IntradayPeriodEnum.M; + +import javafx.application.Application; +import javafx.geometry.HPos; +import javafx.scene.Scene; +import javafx.scene.control.ToolBar; +import javafx.scene.layout.Priority; +import javafx.scene.layout.VBox; + +import de.gsi.chart.Chart; +import de.gsi.chart.XYChart; +import de.gsi.chart.axes.Axis; +import de.gsi.chart.plugins.YRangeIndicator; +import de.gsi.chart.plugins.YWatchValueIndicator; +import de.gsi.chart.renderer.spi.financial.AbstractFinancialRenderer; +import de.gsi.chart.renderer.spi.financial.CandleStickRenderer; +import de.gsi.chart.renderer.spi.financial.css.FinancialColorSchemeConstants; +import de.gsi.chart.samples.financial.service.SimpleOhlcvReplayDataSet; +import de.gsi.chart.samples.financial.service.period.IntradayPeriod; +import de.gsi.chart.utils.FXUtils; +import de.gsi.dataset.spi.DefaultDataSet; +import de.gsi.dataset.spi.financial.OhlcvDataSet; + +/** + * Tick OHLC/V 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 FinancialRealtimeCandlestickSample extends AbstractBasicFinancialApplication { + /** + * Prepare charts to the root. + */ + protected Scene prepareScene() { + String title = "Replay OHLC/V Tick Data in real-time (press 'replay' button)"; + String priceFormat = "%1.1f"; + 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); + + final Chart chart = getDefaultFinancialTestChart(FinancialColorSchemeConstants.SAND); + final AbstractFinancialRenderer renderer = (AbstractFinancialRenderer) chart.getRenderers().get(0); + + chart.setTitle(title); + + // prepare top financial toolbar with replay support + ToolBar testVariableToolBar = getTestToolBar(chart, renderer, true); + + // prepare financial y-value indicator + Axis yAxis = chart.getAxes().get(1); + if (ohlcvDataSet instanceof SimpleOhlcvReplayDataSet) { + final YWatchValueIndicator closeIndicator = new YWatchValueIndicator(yAxis, priceFormat); + closeIndicator.setId("price"); + closeIndicator.setLineVisible(false); + closeIndicator.setEditable(false); + chart.getPlugins().add(closeIndicator); + ((SimpleOhlcvReplayDataSet) ohlcvDataSet).addOhlcvChangeListener(ohlcvItem -> FXUtils.runFX(() -> closeIndicator.setMarkerValue(ohlcvItem.getClose()))); + } + + // manual levels + chart.getPlugins().add(new YWatchValueIndicator(yAxis, priceFormat, 4727.5)); + chart.getPlugins().add(new YWatchValueIndicator(yAxis, priceFormat, 4715.0)); + + // simple S/R ranges + chart.getPlugins().add(createRsLevel(yAxis, 4710, 4711, "Daily Support")); + chart.getPlugins().add(createRsLevel(yAxis, 4731, 4733, "Daily Resistance")); + + VBox root = new VBox(); + VBox.setVgrow(chart, Priority.SOMETIMES); + root.getChildren().addAll(testVariableToolBar, chart); + + return new Scene(root, prefSceneWidth, prefSceneHeight); + } + + protected YRangeIndicator createRsLevel(Axis yAxis, double lowerBound, double upperBound, String description) { + final YRangeIndicator rangeIndi = new YRangeIndicator(yAxis, lowerBound, upperBound, description); + rangeIndi.setLabelHorizontalAnchor(HPos.LEFT); + rangeIndi.setLabelHorizontalPosition(0.01); + + return rangeIndi; + } + + protected void prepareRenderers(XYChart chart, OhlcvDataSet ohlcvDataSet, DefaultDataSet indiSet) { + // create and apply renderers + CandleStickRenderer candleStickRenderer = new CandleStickRenderer(true); + candleStickRenderer.getDatasets().addAll(ohlcvDataSet); + + chart.getRenderers().clear(); + chart.getRenderers().add(candleStickRenderer); + } + + /** + * @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/chart/samples/financial/dos/AbsorptionClusterDO.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/AbsorptionClusterDO.java new file mode 100644 index 000000000..da2a92934 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/AbsorptionClusterDO.java @@ -0,0 +1,27 @@ +package de.gsi.chart.samples.financial.dos; + +import java.util.LinkedHashSet; +import java.util.Set; + +public class AbsorptionClusterDO { + // ordered from bottom to top + private final Set> bidClusters = new LinkedHashSet<>(); + // ordered from top to bottom + private final Set> askClusters = new LinkedHashSet<>(); + + public void addBidCluster(Interval cluster) { + bidClusters.add(cluster); + } + + public void addAskCluster(Interval cluster) { + askClusters.add(cluster); + } + + public Set> getBidClusters() { + return bidClusters; + } + + public Set> getAskClusters() { + return askClusters; + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/DefaultOHLCV.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/DefaultOHLCV.java index 3da901d0d..6aff5a376 100644 --- a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/DefaultOHLCV.java +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/DefaultOHLCV.java @@ -2,6 +2,10 @@ import java.util.*; +import org.apache.commons.lang3.time.DateUtils; + +import de.gsi.chart.samples.financial.service.period.EodPeriod; +import de.gsi.chart.samples.financial.service.period.Period; import de.gsi.dataset.spi.financial.api.attrs.AttributeModel; import de.gsi.dataset.spi.financial.api.ohlcv.IOhlcv; @@ -20,6 +24,7 @@ public class DefaultOHLCV implements IOhlcv { private String assetName; private String symbol; private String studyCategory; + private Period period = new EodPeriod(); // DAILY private OHLCVStateAttributes stateAttributes = new OHLCVStateAttributes(); private AttributeModel addon; @@ -76,6 +81,10 @@ public String getSymbol() { return symbol; } + public Period getPeriod() { + return period; + } + @Override public OHLCVItem getOhlcvItem(int sampleId) { return ohlcvItems[sampleId + stateAttributes.lowerBound]; @@ -183,6 +192,10 @@ public void setName(String name) { this.name = name; } + public void setPeriod(Period period) { + this.period = period; + } + public void setDescription(String description) { this.description = description; } @@ -372,7 +385,7 @@ protected void datasetChanged() { private OhlcvTimestampComparator ohlcvTimestampComparator = null; - private static class OhlcvTimestampComparator implements Comparator { + private class OhlcvTimestampComparator implements Comparator { public int field; public OhlcvTimestampComparator(int field) { @@ -381,7 +394,7 @@ public OhlcvTimestampComparator(int field) { @Override public int compare(OHLCVItem o1, OHLCVItem o2) { - return o1.getTimeStamp().compareTo(o2.getTimeStamp()); + return DateUtils.truncatedCompareTo(o1.getTimeStamp(), o2.getTimeStamp(), field); } } @@ -391,6 +404,7 @@ public int hashCode() { int result = 1; result = prime * result + ((assetName == null) ? 0 : assetName.hashCode()); result = prime * result + ((id == null) ? 0 : id.hashCode()); + result = prime * result + ((period == null) ? 0 : period.hashCode()); result = prime * result + ((symbol == null) ? 0 : symbol.hashCode()); return result; } @@ -414,6 +428,11 @@ public boolean equals(Object obj) { return false; } else if (!id.equals(other.id)) return false; + if (period == null) { + if (other.period != null) + return false; + } else if (!period.equals(other.period)) + return false; if (symbol == null) { if (other.symbol != null) return false; @@ -424,6 +443,6 @@ public boolean equals(Object obj) { @Override public String toString() { - return "OHLCV [id=" + id + ", name=" + name + ", title=" + title + ", assetName=" + assetName + ", symbol=" + symbol + "]"; + return "OHLCV [id=" + id + ", name=" + name + ", title=" + title + ", assetName=" + assetName + ", symbol=" + symbol + ", period=" + period + "]"; } } diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/Interval.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/Interval.java new file mode 100644 index 000000000..fb1d3ff9e --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/Interval.java @@ -0,0 +1,45 @@ +package de.gsi.chart.samples.financial.dos; + +public class Interval { + public T from; + public T to; + + public Interval(T from, T to) { + this.from = from; + this.to = to; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((from == null) ? 0 : from.hashCode()); + result = prime * result + ((to == null) ? 0 : to.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Interval other = (Interval) obj; + if (from == null) { + if (other.from != null) + return false; + } else if (!from.equals(other.from)) + return false; + if (to == null) { + return other.to == null; + } else + return to.equals(other.to); + } + + @Override + public String toString() { + return "Interval [from=" + from + ", to=" + to + "]"; + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/OHLCVItem.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/OHLCVItem.java index 0502afbc7..005bcd9cc 100644 --- a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/OHLCVItem.java +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/OHLCVItem.java @@ -22,6 +22,7 @@ public class OHLCVItem implements IOhlcvItem { private double volumeDown; // bid volume private final double openInterest; private boolean gap; + private OHLCVItemExtended extended; private AttributeModel addon; public OHLCVItem(Date timeStamp, double open, double high, double low, double close, double volume, double openInterest) { @@ -63,6 +64,14 @@ public AttributeModel getAddonOrCreate() { return addon; } + public OHLCVItemExtended getExtended() { + return extended; + } + + public void setExtended(OHLCVItemExtended extended) { + this.extended = extended; + } + public Date getTimeStamp() { return timeStamp; } diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/OHLCVItemExtended.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/OHLCVItemExtended.java new file mode 100644 index 000000000..3eedf2ec9 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/OHLCVItemExtended.java @@ -0,0 +1,56 @@ +package de.gsi.chart.samples.financial.dos; + +import java.util.Date; + +/** + * Extended data for Volume/Market Profiles, Numbers Bars etc. + */ +public class OHLCVItemExtended { + public final Object lock = new Object(); + + private Date timestamp; // unique identifier of the item addon + private PriceVolumeContainer priceVolumeMap = new PriceVolumeContainer(); + private OHLCVItem pullbackOhlcvItem; + private OHLCVItem lastIncrementItem; + + private AbsorptionClusterDO absorptionClusterDO = null; // absorption of volume levels for fims + + public PriceVolumeContainer getPriceVolumeContainer() { + return priceVolumeMap; + } + + public void setPriceVolumeMap(PriceVolumeContainer priceVolumeMap) { + this.priceVolumeMap = priceVolumeMap; + } + + public void setAbsorptionClusterDO(AbsorptionClusterDO absorptionClusterDO) { + this.absorptionClusterDO = absorptionClusterDO; + } + + public AbsorptionClusterDO getAbsorptionClusterDO() { + return absorptionClusterDO; + } + + public OHLCVItem getPullbackOhlcvItem() { + return pullbackOhlcvItem; + } + + public void setPullbackOhlcvItem(OHLCVItem pullbackOhlcvItem) { + this.pullbackOhlcvItem = pullbackOhlcvItem; + } + + public Date getTimestamp() { + return timestamp; + } + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + public void setLastIncrementItem(OHLCVItem lastIncrementItem) { + this.lastIncrementItem = lastIncrementItem; + } + + public OHLCVItem getLastIncrementItem() { + return lastIncrementItem; + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/PriceVolume.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/PriceVolume.java new file mode 100644 index 000000000..73f812174 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/PriceVolume.java @@ -0,0 +1,18 @@ +package de.gsi.chart.samples.financial.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/chart/samples/financial/dos/PriceVolumeContainer.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/PriceVolumeContainer.java new file mode 100644 index 000000000..b20b7609e --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/dos/PriceVolumeContainer.java @@ -0,0 +1,78 @@ +package de.gsi.chart.samples.financial.dos; + +import java.util.Collection; +import java.util.TreeMap; + +public class PriceVolumeContainer { + private final TreeMap priceVolumeMap = new TreeMap<>(); + private double pocPrice; + private double pocVolume = -Double.MAX_VALUE; + + /** + * Add volume up and down to specific price + * @param price value + * @param volumeDown tick down volume + * @param volumeUp tick up volume + */ + public void addPriceVolume(double price, double volumeDown, double volumeUp) { + PriceVolume priceVolume = priceVolumeMap.get(price); + if (priceVolume == null) { + priceVolume = new PriceVolume(price, volumeDown, volumeUp); + priceVolumeMap.put(price, priceVolume); + + } else { + priceVolume.volumeUp += volumeUp; + priceVolume.volumeDown += volumeDown; + } + double totalVolume = priceVolume.volumeUp + priceVolume.volumeDown; + if (totalVolume > pocVolume) { + pocVolume = totalVolume; + pocPrice = price; + } + } + + /** + * @param price return DO price volume by required price level + * @return provides volume information for specific price + */ + public PriceVolume getPriceVolumeBy(double price) { + return priceVolumeMap.get(price); + } + + /** + * @return provides price volume tree map + */ + public TreeMap getCompletedPriceVolumeTreeMap() { + return priceVolumeMap; + } + + /** + * @return provides price volume collection for actual bar + */ + public Collection getCompletedPriceVolume() { + return priceVolumeMap.values(); + } + + /** + * Reset PriceVolume instance + */ + public void clear() { + priceVolumeMap.clear(); + pocVolume = -Double.MAX_VALUE; + pocPrice = 0.0d; + } + + /** + * @return provides calculated POC price + */ + public double getPocPrice() { + return pocPrice; + } + + /** + * @return provides total volume of POS price + */ + public double getPocVolume() { + return pocVolume; + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/CalendarUtils.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/CalendarUtils.java index 197a43b3c..16b5df0a6 100644 --- a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/CalendarUtils.java +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/CalendarUtils.java @@ -4,10 +4,13 @@ import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; +import java.util.Date; import java.util.List; import org.apache.commons.lang3.time.DateUtils; +import de.gsi.chart.samples.financial.dos.Interval; + public class CalendarUtils { /** * Create the calendar interval instance by date interval pattern: @@ -18,7 +21,7 @@ public class CalendarUtils { * @return calendar interval instance * @throws ParseException parsing fails */ - public static Calendar[] createByDateInterval(String dateIntervalPattern) throws ParseException { + public static Interval createByDateInterval(String dateIntervalPattern) throws ParseException { if (dateIntervalPattern == null) { throw new ParseException("The resource date interval pattern is null", -1); } @@ -31,6 +34,84 @@ public static Calendar[] createByDateInterval(String dateIntervalPattern) throws calendarList.add(DateUtils.truncate(cal, Calendar.DATE)); } - return new Calendar[] { calendarList.get(0), calendarList.get(1) }; + return new Interval<>(calendarList.get(0), calendarList.get(1)); + } + + /** + * Create the calendar interval instance by datetime interval pattern: + * yyyy/MM/dd HH:mm-yyyy/MM/dd HH:mm + * for example: 2017/12/01 15:30-2017/12/22 22:15 + * + * @param datetimeIntervalPattern String + * @return calendar interval instance + * @throws ParseException parsing fails + */ + public static Interval createByDateTimeInterval(String datetimeIntervalPattern) throws ParseException { + if (datetimeIntervalPattern == null) { + throw new ParseException("The resource datetime interval pattern is null", -1); + } + String[] parts = datetimeIntervalPattern.split("-"); + SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm"); + List calendarList = new ArrayList<>(); + for (String time : parts) { + Date fromTotime = sdf.parse(time); + Calendar cal = Calendar.getInstance(); + cal.setTime(fromTotime); + cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DATE), + cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), 0); + calendarList.add(cal); + } + + return new Interval<>(calendarList.get(0), calendarList.get(1)); + } + + /** + * Create the calendar interval instance by time interval pattern: + * HH:mm-HH:mm + * for example: 15:30-22:15 + * + * @param timeIntervalPattern String + * @return calendar interval instance + * @throws ParseException parsing fails + */ + public static Interval createByTimeInterval(String timeIntervalPattern) throws ParseException { + if (timeIntervalPattern == null) { + throw new ParseException("The resource time interval pattern is null", -1); + } + String[] parts = timeIntervalPattern.split("-"); + SimpleDateFormat sdf = new SimpleDateFormat("HH:mm"); + List calendarList = new ArrayList<>(); + for (String time : parts) { + Date fromTotime = sdf.parse(time); + Calendar cal = Calendar.getInstance(); + cal.setTime(fromTotime); + cal.set(1900, Calendar.JANUARY, 1, cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), 0); + calendarList.add(cal); + } + + return new Interval<>(calendarList.get(0), calendarList.get(1)); + } + + /** + * Create the calendar instance by datetime pattern: + * yyyy/MM/dd HH:mm + * for example: 2017/12/01 15:30 + * + * @param datetimePattern String + * @return calendar interval instance + * @throws ParseException parsing fails + */ + public static Calendar createByDateTime(String datetimePattern) throws ParseException { + if (datetimePattern == null) { + throw new ParseException("The resource datetime pattern is null", -1); + } + SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd HH:mm"); + Date fromTotime = sdf.parse(datetimePattern); + Calendar cal = Calendar.getInstance(); + cal.setTime(fromTotime); + cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DATE), + cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE), 0); + + return cal; } } diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/OhlcvChangeListener.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/OhlcvChangeListener.java new file mode 100644 index 000000000..4ef27456b --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/OhlcvChangeListener.java @@ -0,0 +1,15 @@ +package de.gsi.chart.samples.financial.service; + +import de.gsi.dataset.spi.financial.api.ohlcv.IOhlcvItem; + +/** + * OHLCV Listener about structure changes. + */ +public interface OhlcvChangeListener { + /** + * Notification event about new ohlcv item changed + * @param ohlcvItem new or changed ohlcv item + * @exception Exception if the processing failed + */ + void tickEvent(IOhlcvItem ohlcvItem) throws Exception; +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/SCIDByNio.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/SCIDByNio.java new file mode 100644 index 000000000..5d5b7b32e --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/SCIDByNio.java @@ -0,0 +1,307 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ +package de.gsi.chart.samples.financial.service; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.channels.FileChannel; +import java.util.Calendar; +import java.util.Date; + +import javafx.beans.property.DoubleProperty; + +import de.gsi.chart.samples.financial.dos.Interval; +import de.gsi.chart.samples.financial.dos.OHLCVItem; + +/** + * Create OHLCV from Sierra Chart SCID files (intraday tick format). + * + * @author afischer + */ +public class SCIDByNio { + private FileChannel fileChannel; + private ByteBuffer bufferHeader; + private ByteBuffer bufferRecordDouble; + private ByteBuffer bufferRecordFloat; + private ByteBuffer bufferRecordULong; + private final Calendar cal = Calendar.getInstance(); + private int timeZone; + + private String title; + private String symbol; + + @SuppressWarnings({ "lgtm[java/output-resource-leak", "resource" }) + public void openNewChannel(String resource) throws IOException { + title = new File(resource).getName(); + symbol = title.replaceFirst("[.][^.]+$", ""); + + timeZone = cal.get(Calendar.ZONE_OFFSET); + + File f = new File(resource); + FileInputStream fis = new FileInputStream(f); // lgtm[java/output-resource-leak] + + //---------------------------------- + fileChannel = fis.getChannel(); + + bufferHeader = ByteBuffer.allocate(4); + bufferHeader.order(ByteOrder.LITTLE_ENDIAN); + + bufferRecordDouble = ByteBuffer.allocate(8); + bufferRecordDouble.order(ByteOrder.LITTLE_ENDIAN); + + bufferRecordFloat = ByteBuffer.allocate(4); + bufferRecordFloat.order(ByteOrder.LITTLE_ENDIAN); + + bufferRecordULong = ByteBuffer.allocate(4); + bufferRecordULong.order(ByteOrder.LITTLE_ENDIAN); + + fileChannel.position(56); + } + + public void closeActualChannel() throws IOException { + if (fileChannel.isOpen()) { + fileChannel.close(); + } + } + + /** + * Find position which if FIRST or equaled after you inserted timestamp. + * Check if the position is negative. If the position is negative it is first position + * after your required timestamp. Beware if the position is higher that maximal position. + * Usage of Binary Search algorithm + * + * @param timestamp Date + * @return file position of timestamp of record + * @throws IOException if reading of file failed + */ + public long findPositionByTimestamp(Date timestamp) throws IOException { + // usage of binary search + long lo = 56; + long hi = fileChannel.size() - 40; + long mid; + while (lo <= hi) { + // Key is in a[lo..hi] or not present. + mid = lo + (hi - lo) / 2; + mid = ((mid - 56) / 40) * 40 + 56; // recalculate for nearest timestamp + Date midTimestamp = loadTimestamp(mid); + if (timestamp.before(midTimestamp)) { + hi = mid - 40; + } else if (timestamp.after(midTimestamp)) { + lo = mid + 40; + } else + return mid; + } + return -lo; + } + + /** + * Return first or equaled position for required position result + * + * @param timestamp Date + * @return modified position long + * @throws IOException if reading of file failed + */ + public long ensureNearestTimestampPosition(Date timestamp) throws IOException { + long position = findPositionByTimestamp(timestamp); + + if (position > 0) { + return position; + } + position = Math.abs(position); + + long positionEnd = fileChannel.size() - 40; + if (position > positionEnd) { + return positionEnd; + } + + return position; + } + + /** + * Create instance of tick ohlcv data provider for replay stream + * + * @param requiredTimestamps [from, to] interval + * @param replayStarTime Date - point of replay timing start + * @param replaySpeed multiply of replay simulation (with real timing!) + * @return tick data provider + * @throws IOException if reading of file failed + */ + public TickOhlcvDataProvider createTickDataReplayStream(final Interval requiredTimestamps, + final Date replayStarTime, DoubleProperty replaySpeed) throws IOException { + // define boundaries of loaded data + final long positionStart = ensureNearestTimestampPosition(requiredTimestamps.from.getTime()); + final long positionEnd = ensureNearestTimestampPosition(requiredTimestamps.to.getTime()); + final long ohlcvReplayStartIndex = ensureNearestTimestampPosition(replayStarTime); + + // initialization settings + fileChannel.position(positionStart); + + return new TickOhlcvDataProvider() { + private OHLCVItem prevItem = null; + private OHLCVItem item = null; + private final Object lock = new Object(); + + @Override + public OHLCVItem get() throws TickDataFinishedException, IOException { + long position = fileChannel.position(); + if (positionEnd != -1 && position >= positionEnd) { + throw new TickDataFinishedException("The replay finished."); + } + if (position >= ohlcvReplayStartIndex) { + long prevTime = prevItem != null ? prevItem.getTimeStamp().getTime() : 0; + long time = item != null ? item.getTimeStamp().getTime() : 0; + long waitingTime = Math.round((time - prevTime) / replaySpeed.get()); + waitingTime = waitingTime < 1 ? 1 : waitingTime; + try { + // waiting to send next sample - simulation of replay processing + Thread.sleep(waitingTime); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + prevItem = item; + item = loadOhlcvItemRealtime(); + + return item; + } + }; + } + + /** + * Base method for reading of interval ohlcv item in the realtime mode + * + * @return domain object + * @throws IOException if reading of file failed + */ + private OHLCVItem loadOhlcvItemRealtime() throws IOException { + double dt; + float open; + float high; + float low; + float close; + + long numTrades; + long totalVolume; + long bidVolume; + long askVolume; + + int bytesRead; + do { + bytesRead = fileChannel.read(bufferRecordDouble); + if (bytesRead == -1) { + // wait for new realtime data + synchronized (this) { + try { + wait(25); // default is 25ms + } catch (InterruptedException ignored) { + } + } + } + } while (bytesRead == -1); + + // timestamp + bufferRecordDouble.flip(); + dt = bufferRecordDouble.getDouble(); + bufferRecordDouble.clear(); + + // open + // In Sierra Chart version 1150 and higher, in the case where the data + // record holds 1 tick/trade of data, the Open will be equal to 0. + fileChannel.read(bufferRecordFloat); + // bufferRecordFloat.flip(); + // open = bufferRecordFloat.getFloat(); + bufferRecordFloat.clear(); + + // high + fileChannel.read(bufferRecordFloat); + bufferRecordFloat.flip(); + high = bufferRecordFloat.getFloat(); + bufferRecordFloat.clear(); + + // low + fileChannel.read(bufferRecordFloat); + bufferRecordFloat.flip(); + low = bufferRecordFloat.getFloat(); + bufferRecordFloat.clear(); + + // close + fileChannel.read(bufferRecordFloat); + bufferRecordFloat.flip(); + close = bufferRecordFloat.getFloat(); + open = close; // tick data only! + bufferRecordFloat.clear(); + + // number of trades + fileChannel.read(bufferRecordULong); + bufferRecordULong.flip(); + numTrades = bufferRecordULong.getInt(); + bufferRecordULong.clear(); + + // total volume + fileChannel.read(bufferRecordULong); + bufferRecordULong.flip(); + totalVolume = bufferRecordULong.getInt(); + bufferRecordULong.clear(); + + // bid volume + fileChannel.read(bufferRecordULong); + bufferRecordULong.flip(); + bidVolume = bufferRecordULong.getInt(); + bufferRecordULong.clear(); + + // ask volume + fileChannel.read(bufferRecordULong); + bufferRecordULong.flip(); + askVolume = bufferRecordULong.getInt(); + bufferRecordULong.clear(); + + // timestamp conversion to date structure + Date timestamp = new Date(convertWindowsTimeToMilliseconds(dt)); + + // assembly one ohlcv item domain object + return new OHLCVItem(timestamp, open, high, low, close, totalVolume, 0, askVolume, bidVolume); + } + + /** + * Load timestamp of the required position + * + * @param position in file + * @return timestamp + * @throws IOException if reading of file failed + */ + private Date loadTimestamp(long position) throws IOException { + double dt; + + fileChannel.position(position); + int bytesRead = fileChannel.read(bufferRecordDouble); + if (bytesRead == -1) { + return null; + } + bufferRecordDouble.flip(); + dt = bufferRecordDouble.getDouble(); + bufferRecordDouble.clear(); + + return new Date(convertWindowsTimeToMilliseconds(dt)); + } + + /** + * Thanks to @see + * http://svn.codehaus.org/groovy/modules/scriptom/branches/SCRIPTOM + * -1.5.4-ANT/src/com/jacob/com/DateUtilities.java + * + * @param comTime time in windows time for convert to java format + * @return java format of windows format with usage of specific timezone + */ + public long convertWindowsTimeToMilliseconds(double comTime) { + comTime = comTime - 25569D; + long result = Math.round(86400000L * comTime) - timeZone; + cal.setTime(new Date(result)); + result -= cal.get(Calendar.DST_OFFSET); + + return result; + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/SimpleOhlcvDailyParser.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/SimpleOhlcvDailyParser.java index b1a0a5a8e..d08ff11b6 100644 --- a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/SimpleOhlcvDailyParser.java +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/SimpleOhlcvDailyParser.java @@ -1,3 +1,6 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ package de.gsi.chart.samples.financial.service; import java.io.BufferedReader; @@ -16,6 +19,11 @@ import de.gsi.dataset.spi.financial.api.ohlcv.IOhlcv; import de.gsi.dataset.utils.StreamUtils; +/** + * Simple Tradestation OHLC data parser. + * + * @author afischer + */ public class SimpleOhlcvDailyParser { private static final String CHART_SAMPLE_PATH = StreamUtils.CLASSPATH_PREFIX + "de/gsi/chart/samples/financial/%s.csv"; diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/SimpleOhlcvReplayDataSet.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/SimpleOhlcvReplayDataSet.java new file mode 100644 index 000000000..aefe61c5b --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/SimpleOhlcvReplayDataSet.java @@ -0,0 +1,215 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ +package de.gsi.chart.samples.financial.service; + +import java.io.IOException; +import java.util.Calendar; +import java.util.LinkedHashSet; +import java.util.Set; + +import javafx.beans.property.DoubleProperty; +import javafx.beans.property.SimpleDoubleProperty; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.gsi.chart.samples.financial.dos.DefaultOHLCV; +import de.gsi.chart.samples.financial.dos.Interval; +import de.gsi.chart.samples.financial.dos.OHLCVItem; +import de.gsi.chart.samples.financial.service.consolidate.IncrementalOhlcvConsolidation; +import de.gsi.chart.samples.financial.service.consolidate.OhlcvTimeframeConsolidation; +import de.gsi.chart.samples.financial.service.period.IntradayPeriod; +import de.gsi.dataset.event.AddedDataEvent; +import de.gsi.dataset.spi.financial.OhlcvDataSet; +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; + +/** + * Very simple financial OHLC replay data set. + * The service is used just for simple testing of OHLC chart changes and performance. + * + * @author afischer + */ +public class SimpleOhlcvReplayDataSet extends OhlcvDataSet implements Iterable, IOhlcvItemAware, AttributeModelAware { + private static final Logger LOGGER = LoggerFactory.getLogger(SimpleOhlcvReplayDataSet.class); + + private static final String DATA_SOURCE_OHLC_TICK = "NQ-201609-GLOBEX"; + + private static final String DATA_SOURCE_PATH = "chartfx-samples/target/classes/de/gsi/chart/samples/financial/%s.scid"; + + private final DoubleProperty replayMultiply = new SimpleDoubleProperty(this, "replayMultiply", 1.0); + + private DataInput inputSource = DataInput.OHLC_TICK; + protected DefaultOHLCV ohlcv; + + protected volatile boolean running = false; + protected volatile boolean paused = false; + protected transient final Object pause = new Object(); + + protected transient SCIDByNio scid; + protected transient TickOhlcvDataProvider tickOhlcvDataProvider; + protected transient IncrementalOhlcvConsolidation consolidation; + + protected Set ohlcvChangeListeners = new LinkedHashSet<>(); + + protected int maxXIndex = 0; + + public enum DataInput { + OHLC_TICK + } + + public SimpleOhlcvReplayDataSet(DataInput dataInput, IntradayPeriod period, Interval timeRange, Interval tt, Calendar replayFrom) { + super(dataInput.name()); + setInputSource(dataInput); + fillTestData(period, timeRange, tt, replayFrom); // NOPMD + if (LOGGER.isDebugEnabled()) { + LOGGER.atDebug().addArgument(SimpleOhlcvReplayDataSet.class.getSimpleName()).log("started '{}'"); + } + } + + public void addOhlcvChangeListener(OhlcvChangeListener ohlcvChangeListener) { + ohlcvChangeListeners.add(ohlcvChangeListener); + } + + public void fillTestData(IntradayPeriod period, Interval timeRange, Interval tt, Calendar replayFrom) { + lock().writeLockGuard( + () -> { + try { + // create services + scid = new SCIDByNio(); + scid.openNewChannel(String.format(DATA_SOURCE_PATH, DATA_SOURCE_OHLC_TICK)); + tickOhlcvDataProvider = scid.createTickDataReplayStream(timeRange, replayFrom.getTime(), replayMultiply); + + ohlcv = new DefaultOHLCV(); + ohlcv.setTitle(DATA_SOURCE_OHLC_TICK); + + consolidation = OhlcvTimeframeConsolidation.createConsolidation(period, tt, null); + + autoNotification().set(false); + setData(ohlcv); + // try first tick in the fill part + tick(); + autoNotification().set(true); + + } catch (TickDataFinishedException e) { + LOGGER.info(e.getMessage()); + + } catch (Exception e) { + throw new IllegalArgumentException(e.getMessage(), e); + } + }); + } + + protected void tick() throws Exception { + OHLCVItem increment = tickOhlcvDataProvider.get(); + //lock().writeLockGuard(() -> { // not write lock blinking + consolidation.consolidate(ohlcv, increment); + // recalculate limits + if (maxXIndex < ohlcv.size()) { + maxXIndex = ohlcv.size(); + // best performance solution + getAxisDescription(DIM_X).set(get(DIM_X, 0), get(DIM_X, maxXIndex - 1)); + } + // notify last tick listeners + fireOhlcvTickEvent(increment); + //}); + } + + protected void fireOhlcvTickEvent(IOhlcvItem ohlcvItem) throws Exception { + for (OhlcvChangeListener listener : ohlcvChangeListeners) { + listener.tickEvent(ohlcvItem); + } + } + + public DataInput getInputSource() { + return inputSource; + } + + public void setInputSource(DataInput inputSource) { + this.inputSource = inputSource; + } + + /** + * pause/resume play back of the data source via the sound card + */ + public void pauseResume() { + if (paused) { + paused = false; + synchronized (pause) { + pause.notify(); + } + } else { + paused = true; + } + } + + /** + * Update replay interval + * @param updatePeriod replay multiple 1.0-N + */ + public void setUpdatePeriod(final double updatePeriod) { + replayMultiply.set(updatePeriod); + if (!running) { + start(); + } + } + + /** + * starts play back of the data source via the sound card + */ + public void start() { + paused = false; + running = true; + new Thread(getDataUpdateTask()).start(); + } + + public void step() { + getDataUpdateTask().run(); + } + + /** + * stops and resets play back of the data source via the sound card + */ + public void stop() { + if (running) { + running = false; + if (paused) { + pauseResume(); + } + try { + if (scid != null) { + scid.closeActualChannel(); + } + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + } + } + + protected Runnable getDataUpdateTask() { + return () -> { + while (running) { + try { + tick(); + fireInvalidated(new AddedDataEvent(SimpleOhlcvReplayDataSet.this, "tick")); + // pause simple support + if (paused) { + try { + synchronized (pause) { + pause.wait(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + } catch (TickDataFinishedException e) { + stop(); + } catch (Exception e) { + throw new IllegalArgumentException(e); + } + } + }; + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/TickDataFinishedException.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/TickDataFinishedException.java new file mode 100644 index 000000000..bcd04050c --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/TickDataFinishedException.java @@ -0,0 +1,9 @@ +package de.gsi.chart.samples.financial.service; + +public class TickDataFinishedException extends Exception { + private static final long serialVersionUID = 5241232871349317846L; + + public TickDataFinishedException(String message) { + super(message); + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/TickOhlcvDataProvider.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/TickOhlcvDataProvider.java new file mode 100644 index 000000000..5c1b4b35a --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/TickOhlcvDataProvider.java @@ -0,0 +1,19 @@ +package de.gsi.chart.samples.financial.service; + +import java.io.IOException; + +import de.gsi.chart.samples.financial.dos.OHLCVItem; + +/** + * Provides actual tick data + */ +public interface TickOhlcvDataProvider { + /** + * Every get() returns tick ohlcv item. If it is replay mode - the boundary is reached the TickDataFinishedException is thrown. + * If the realtime mode is used - never-end loop is used. The thread waits to next data. + * @return provides tick ohlcv data + * @throws TickDataFinishedException if the data are reached the boundary + * @throws IOException - the data are not reachable + */ + OHLCVItem get() throws TickDataFinishedException, IOException; +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/AbstractIncrementalOhlcvConsolidation.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/AbstractIncrementalOhlcvConsolidation.java new file mode 100644 index 000000000..e2180ee37 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/AbstractIncrementalOhlcvConsolidation.java @@ -0,0 +1,117 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ +package de.gsi.chart.samples.financial.service.consolidate; + +import java.util.Calendar; + +import de.gsi.chart.samples.financial.dos.DefaultOHLCV; +import de.gsi.chart.samples.financial.dos.Interval; +import de.gsi.chart.samples.financial.dos.OHLCVItem; +import de.gsi.chart.samples.financial.service.consolidate.OhlcvTimeframeConsolidation.OhlcvConsolidationComputation; +import de.gsi.chart.samples.financial.service.consolidate.OhlcvTimeframeConsolidation.StandardOhlcvConsolidationComputation; +import de.gsi.chart.samples.financial.service.period.IntradayPeriod; + +/** + * Incremental consolidation based class for OHLCV structures. + * It can be used for time period, range bars and volume graphs. + */ +public abstract class AbstractIncrementalOhlcvConsolidation implements IncrementalOhlcvConsolidation { + protected IntradayPeriod period; + protected OhlcvConsolidationComputation consolidationComputation; + protected OhlcvConsolidationAddon[] ohlcvConsolidationAddons; + protected Interval tt; + + private OHLCVItem lastItem; + + public AbstractIncrementalOhlcvConsolidation(OhlcvConsolidationComputation consolidationComputation, + IntradayPeriod period, Interval tt, + OhlcvConsolidationAddon[] ohlcvConsolidationAddons) { + this.period = period; + this.consolidationComputation = consolidationComputation == null ? new StandardOhlcvConsolidationComputation() : consolidationComputation; + this.ohlcvConsolidationAddons = ohlcvConsolidationAddons; + this.tt = tt; + } + + @Override + public IntradayPeriod getPeriod() { + return period; + } + + @Override + public DefaultOHLCV consolidate(DefaultOHLCV ohlcv, OHLCVItem incrementItem) { + if (lastItem == null) { + lastItem = incrementItem; + } + + if (checkConsolidationCondition(lastItem, incrementItem)) { + OHLCVItem finalItem = processConsolidation(lastItem, incrementItem); + ohlcv.updateOhlcvItem(ohlcv.size() - 1, finalItem); + lastItem = finalItem; + defineConsolidationConditionAfterUpdate(lastItem); + processConsolidationAddonsInUpdate(ohlcv, incrementItem); + + } else { + ohlcv.addOhlcvItem(incrementItem); + lastItem = incrementItem; + defineConsolidationConditionAfterAddition(lastItem); + processConsolidationAddonsInAddition(ohlcv, incrementItem); + } + + return ohlcv; + } + + protected void processConsolidationAddonsInUpdate(DefaultOHLCV ohlcv, OHLCVItem incrementItem) { + if (ohlcvConsolidationAddons != null) { + for (int i = 0; i < ohlcvConsolidationAddons.length; i++) { + if (ohlcvConsolidationAddons[i].isDynamic()) { + ohlcvConsolidationAddons[i].consolidationUpdateAddon(ohlcv, incrementItem); + } + } + } + } + + protected void processConsolidationAddonsInAddition(DefaultOHLCV ohlcv, OHLCVItem incrementItem) { + if (ohlcvConsolidationAddons != null) { + for (int i = 0; i < ohlcvConsolidationAddons.length; i++) { + ohlcvConsolidationAddons[i].consolidationAdditionAddon(ohlcv, incrementItem); + } + } + } + + /** + * Define consolidation condition after addition for next checking of performConsolidation method + * + * @param finalItem for definition consolidation condition + */ + protected abstract void defineConsolidationConditionAfterAddition(OHLCVItem finalItem); + + /** + * Define consolidation condition after update for next checking of performConsolidation method + * + * @param finalItem for definition consolidation condition + */ + protected abstract void defineConsolidationConditionAfterUpdate(OHLCVItem finalItem); + + /** + * Different test for consolidation defined by IntradayPeriod instance + * + * @param lastItem of consolidated structure + * @param incrementItem tick which will be increased to the consolidation structure + * @return true = consolidation process has to be performed + */ + protected abstract boolean checkConsolidationCondition(OHLCVItem lastItem, OHLCVItem incrementItem); + + /** + * Process consolidation process with actual increment. + * Standard or extended (footpring) consolidation processing of OHLCV structure + * + * @param lastItem of consolidated structure + * @param incrementItem tick which will be increased to the consolidation structure + * @return consolidated ohlcv item + */ + protected OHLCVItem processConsolidation(OHLCVItem lastItem, OHLCVItem incrementItem) { + // use servant for this processing + return consolidationComputation.consolidate(lastItem.getTimeStamp(), lastItem, incrementItem); + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/IncrementalOhlcvConsolidation.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/IncrementalOhlcvConsolidation.java new file mode 100644 index 000000000..28515520a --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/IncrementalOhlcvConsolidation.java @@ -0,0 +1,20 @@ +package de.gsi.chart.samples.financial.service.consolidate; + +import de.gsi.chart.samples.financial.dos.DefaultOHLCV; +import de.gsi.chart.samples.financial.dos.OHLCVItem; +import de.gsi.chart.samples.financial.service.period.IntradayPeriod; + +public interface IncrementalOhlcvConsolidation { + /** + * Base method for incremental consolidation process + * @param ohlcv existed consolidated ohlcv structure + * @param incrementItem tick actual ohlcv item + * @return consolidated signal + */ + DefaultOHLCV consolidate(DefaultOHLCV ohlcv, OHLCVItem incrementItem); + + /** + * @return provides information about consolidation settings period + */ + IntradayPeriod getPeriod(); +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/OhlcvConsolidationAddon.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/OhlcvConsolidationAddon.java new file mode 100644 index 000000000..caeb80d21 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/OhlcvConsolidationAddon.java @@ -0,0 +1,29 @@ +package de.gsi.chart.samples.financial.service.consolidate; + +import de.gsi.chart.samples.financial.dos.DefaultOHLCV; +import de.gsi.chart.samples.financial.dos.OHLCVItem; + +public interface OhlcvConsolidationAddon { + /** + * Base method for addon calculation process + * @param ohlcv existed ohlcv structure + * @param incrementItem incremental ohlc item + * @return enhanced signal + */ + DefaultOHLCV consolidationUpdateAddon(DefaultOHLCV ohlcv, OHLCVItem incrementItem); + + /** + * Base method for addon calculation process + * @param ohlcv existed ohlcv structure + * @param incrementItem incremental ohlc item + * @return enhanced signal + */ + DefaultOHLCV consolidationAdditionAddon(DefaultOHLCV ohlcv, OHLCVItem incrementItem); + + /** + * @return true = addon needs recalculation per tick in the consolidation process, + * false = the computation is processing by new tick which create new bar. It means + * in the end of previous closed bar - on close of bar. + */ + boolean isDynamic(); +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/OhlcvTimeframeConsolidation.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/OhlcvTimeframeConsolidation.java new file mode 100644 index 000000000..da6aed056 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/OhlcvTimeframeConsolidation.java @@ -0,0 +1,229 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ +package de.gsi.chart.samples.financial.service.consolidate; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; +import java.util.Map; + +import de.gsi.chart.samples.financial.dos.Interval; +import de.gsi.chart.samples.financial.dos.OHLCVItem; +import de.gsi.chart.samples.financial.dos.OHLCVItemExtended; +import de.gsi.chart.samples.financial.dos.PriceVolumeContainer; +import de.gsi.chart.samples.financial.service.period.IntradayPeriod; + +public class OhlcvTimeframeConsolidation { + private OhlcvTimeframeConsolidation() { + } + + /** + * Create incremental consolidation (re-sampling) of OHLC/V. + * + * @param intradayPeriod required re-sampled period. + * @param tt time template range. Necessary for perfect consolidation times are calculated from beginning of the TT and ending of TT. + * If missing (null), the "00:00" time is selected as beginning for consolidation algorithm. + * @param addons which can extend re-sampling process about additional services to add more specific behaviours. + * @return prepared service for incremental consolidation. + */ + public static IncrementalOhlcvConsolidation createConsolidation(IntradayPeriod intradayPeriod, Interval tt, Map addons) { + IncrementalOhlcvConsolidation consolidation = null; + OhlcvConsolidationComputation ohlcvConsolidationComputation = intradayPeriod.isExtendedCalculation() ? new ExtendedOhlcvConsolidationComputation() : new StandardOhlcvConsolidationComputation(); + + OhlcvConsolidationAddon[] _ohlcvConsolidationAddons = null; + if (intradayPeriod.getCalculationAddonServicesType() != null && addons != null) { + _ohlcvConsolidationAddons = addons.get(intradayPeriod.getCalculationAddonServicesType()); + } + + switch (intradayPeriod.getPeriod()) { + case RB: // Example of range bars length 4 and one tick is 0.25 of point + consolidation = new RangeBarsIncrementalOhlcvConsolidation( + ohlcvConsolidationComputation, intradayPeriod, intradayPeriod.getMinimalMoveSymbol(), tt, _ohlcvConsolidationAddons); + break; + case V: + consolidation = new VolumeIncrementalOhlcvConsolidation(ohlcvConsolidationComputation, intradayPeriod, tt, _ohlcvConsolidationAddons); + break; + case T: // no consolidation + break; + case H: + throw new IllegalArgumentException("HOUR timeframe is not allowed for tick data provider."); + default: //M, S + consolidation = new TimePeriodIncrementalOhlcvConsolidation(ohlcvConsolidationComputation, intradayPeriod, tt, _ohlcvConsolidationAddons); + break; + } + return consolidation; + } + + public interface OhlcvConsolidationComputation { + OHLCVItem consolidate(List ohlcvItems); + + OHLCVItem consolidate(Date timestamp, List ohlcvItems); + + OHLCVItem consolidate(Date timeStamp, OHLCVItem lastConsolidatedItem, OHLCVItem incrementItem); + } + + public static class StandardOhlcvConsolidationComputation implements OhlcvConsolidationComputation { + @Override + public OHLCVItem consolidate(Date timeStamp, OHLCVItem lastConsolidatedItem, OHLCVItem incrementItem) { + double oi = 0.0d; + double volume = lastConsolidatedItem.getVolume(); + double low = lastConsolidatedItem.getLow(); + double high = lastConsolidatedItem.getHigh(); + + if (incrementItem.getLow() < low) { + low = incrementItem.getLow(); + } + if (incrementItem.getHigh() > high) { + high = incrementItem.getHigh(); + } + volume += incrementItem.getVolume(); + + return new OHLCVItem( + timeStamp, + lastConsolidatedItem.getOpen(), + high, + low, + incrementItem.getClose(), + volume, + oi); + } + + @Override + public OHLCVItem consolidate(List ohlcvItems) { + return consolidate(null, ohlcvItems); + } + + @Override + public OHLCVItem consolidate(Date timestamp, List ohlcvItems) { + if (timestamp == null) { + timestamp = ohlcvItems.get(0).getTimeStamp(); + } + double volume = 0.0d; + double oi = 0.0d; + double open = ohlcvItems.get(0).getOpen(); + double close = ohlcvItems.get(ohlcvItems.size() - 1).getClose(); + double low = Double.MAX_VALUE; + double high = -1 * Double.MAX_VALUE; + + for (OHLCVItem ohlcvItem : ohlcvItems) { + if (ohlcvItem.getLow() < low) { + low = ohlcvItem.getLow(); + } + if (ohlcvItem.getHigh() > high) { + high = ohlcvItem.getHigh(); + } + volume += ohlcvItem.getVolume(); + } + + return new OHLCVItem(timestamp, open, high, low, close, volume, oi); + } + } + + public static class ExtendedOhlcvConsolidationComputation implements OhlcvConsolidationComputation { + @Override + public OHLCVItem consolidate(Date timeStamp, OHLCVItem lastConsolidatedItem, OHLCVItem incrementItem) { + return consolidate(timeStamp, lastConsolidatedItem, incrementItem, true); + } + + public OHLCVItem consolidate(Date timestamp, OHLCVItem lastConsolidatedItem, OHLCVItem incrementItem, boolean computePullbackColumn) { + double oi = 0.0d; + double volume = lastConsolidatedItem.getVolume(); + double volumeUp = lastConsolidatedItem.getVolumeUpAsk(); + double volumeDown = lastConsolidatedItem.getVolumeDownBid(); + double open = lastConsolidatedItem.getOpen(); + double close = incrementItem.getClose(); + double low = lastConsolidatedItem.getLow(); + double high = lastConsolidatedItem.getHigh(); + + OHLCVItemExtended ohlcvItemExtended = lastConsolidatedItem.getExtended(); + if (ohlcvItemExtended == null) { + ohlcvItemExtended = new OHLCVItemExtended(); + } + + synchronized (ohlcvItemExtended.lock) { + ohlcvItemExtended.setLastIncrementItem(incrementItem); + + PriceVolumeContainer priceVolumeContainer = ohlcvItemExtended.getPriceVolumeContainer(); + OHLCVItem pullbackOhlcvItem = ohlcvItemExtended.getPullbackOhlcvItem(); + + if (incrementItem.getLow() < low) { + low = incrementItem.getLow(); + if (computePullbackColumn) { + // create low pullback column + pullbackOhlcvItem = new OHLCVItem(timestamp, low, low, low, low, 0, 0, 0.0d, 0.0d); + ohlcvItemExtended.setPullbackOhlcvItem(pullbackOhlcvItem); + } + } + if (incrementItem.getHigh() > high) { + high = incrementItem.getHigh(); + if (computePullbackColumn) { + // create high pullback column + pullbackOhlcvItem = new OHLCVItem(timestamp, high, high, high, high, 0, 0, 0.0d, 0.0d); + ohlcvItemExtended.setPullbackOhlcvItem(pullbackOhlcvItem); + } + } + if (pullbackOhlcvItem != null) { + OHLCVItem pullbackOhlcvItemUpdated = consolidate(timestamp, pullbackOhlcvItem, incrementItem, false); + ohlcvItemExtended.setPullbackOhlcvItem(pullbackOhlcvItemUpdated); + } + + volume += incrementItem.getVolume(); + volumeUp += incrementItem.getVolumeUpAsk(); + volumeDown += incrementItem.getVolumeDownBid(); + + priceVolumeContainer.addPriceVolume(incrementItem.getClose(), incrementItem.getVolumeDownBid(), incrementItem.getVolumeUpAsk()); + } + + OHLCVItem ohlcvItem = new OHLCVItem(timestamp, open, high, low, close, volume, oi, volumeUp, volumeDown); + ohlcvItemExtended.setTimestamp(timestamp); // unique identifier + ohlcvItem.setExtended(ohlcvItemExtended); + + return ohlcvItem; + } + + @Override + public OHLCVItem consolidate(List ohlcvItems) { + return consolidate(null, ohlcvItems); + } + + @Override + public OHLCVItem consolidate(Date timestamp, List ohlcvItems) { + if (timestamp == null) { + timestamp = ohlcvItems.get(0).getTimeStamp(); + } + double volume = 0.0d; + double volumeUp = 0.0d; + double volumeDown = 0.0d; + double oi = 0.0d; + double open = ohlcvItems.get(0).getOpen(); + double close = ohlcvItems.get(ohlcvItems.size() - 1).getClose(); + double low = Double.MAX_VALUE; + double high = -1 * Double.MAX_VALUE; + + OHLCVItemExtended ohlcvItemExtended = new OHLCVItemExtended(); + PriceVolumeContainer priceVolumeContainer = ohlcvItemExtended.getPriceVolumeContainer(); + + for (OHLCVItem ohlcvItem : ohlcvItems) { + if (ohlcvItem.getLow() < low) { + low = ohlcvItem.getLow(); + } + if (ohlcvItem.getHigh() > high) { + high = ohlcvItem.getHigh(); + } + volume += ohlcvItem.getVolume(); + volumeUp += ohlcvItem.getVolumeUpAsk(); + volumeDown += ohlcvItem.getVolumeDownBid(); + + priceVolumeContainer.addPriceVolume( + ohlcvItem.getClose(), ohlcvItem.getVolumeDownBid(), ohlcvItem.getVolumeUpAsk()); + } + + OHLCVItem ohlcvItem = new OHLCVItem(timestamp, open, high, low, close, volume, oi, volumeUp, volumeDown); + ohlcvItemExtended.setTimestamp(timestamp); // unique identifier + ohlcvItem.setExtended(ohlcvItemExtended); + + return ohlcvItem; + } + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/RangeBarsIncrementalOhlcvConsolidation.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/RangeBarsIncrementalOhlcvConsolidation.java new file mode 100644 index 000000000..50641907f --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/RangeBarsIncrementalOhlcvConsolidation.java @@ -0,0 +1,52 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ +package de.gsi.chart.samples.financial.service.consolidate; + +import java.util.Calendar; + +import de.gsi.chart.samples.financial.dos.Interval; +import de.gsi.chart.samples.financial.dos.OHLCVItem; +import de.gsi.chart.samples.financial.service.consolidate.OhlcvTimeframeConsolidation.OhlcvConsolidationComputation; +import de.gsi.chart.samples.financial.service.period.IntradayPeriod; + +/** + * Range-Bars based financial charts + * + * @author afischer + */ + +public class RangeBarsIncrementalOhlcvConsolidation extends AbstractIncrementalOhlcvConsolidation { + private final double rangeBarsLength; + private boolean hasLength = true; // declare first bar + private double low; + private double high; + + public RangeBarsIncrementalOhlcvConsolidation(OhlcvConsolidationComputation consolidationComputation, + IntradayPeriod period, double minMoveTick, Interval tt, + OhlcvConsolidationAddon[] ohlcvConsolidationAddons) { + super(consolidationComputation, period, tt, ohlcvConsolidationAddons); + this.rangeBarsLength = period.getPeriodValue() * minMoveTick; + } + + @Override + protected void defineConsolidationConditionAfterAddition(OHLCVItem finalItem) { + defineConsolidationCondition(finalItem); + } + + @Override + protected void defineConsolidationConditionAfterUpdate(OHLCVItem finalItem) { + defineConsolidationCondition(finalItem); + } + + @Override + protected boolean checkConsolidationCondition(OHLCVItem lastItem, OHLCVItem incrementItem) { + return !hasLength || incrementItem.getLow() >= low && incrementItem.getHigh() <= high; + } + + private void defineConsolidationCondition(OHLCVItem finalItem) { + low = finalItem.getLow(); + high = finalItem.getHigh(); + hasLength = rangeBarsLength <= (high - low); + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/TimePeriodIncrementalOhlcvConsolidation.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/TimePeriodIncrementalOhlcvConsolidation.java new file mode 100644 index 000000000..324003528 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/TimePeriodIncrementalOhlcvConsolidation.java @@ -0,0 +1,113 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ +package de.gsi.chart.samples.financial.service.consolidate; + +import java.util.Calendar; +import java.util.Date; + +import de.gsi.chart.samples.financial.dos.Interval; +import de.gsi.chart.samples.financial.dos.OHLCVItem; +import de.gsi.chart.samples.financial.service.consolidate.OhlcvTimeframeConsolidation.OhlcvConsolidationComputation; +import de.gsi.chart.samples.financial.service.period.IntradayPeriod; + +/** + * Time based financial charts + * + * @author afischer + */ + +public class TimePeriodIncrementalOhlcvConsolidation extends AbstractIncrementalOhlcvConsolidation { + private final Calendar calFrom = Calendar.getInstance(); // FROM + private final Calendar calTo = Calendar.getInstance(); // TO + + public TimePeriodIncrementalOhlcvConsolidation(OhlcvConsolidationComputation consolidationComputation, + IntradayPeriod period, Interval tt, + OhlcvConsolidationAddon[] _ohlcvConsolidationAddons) { + super(consolidationComputation, period, tt, _ohlcvConsolidationAddons); + calFrom.setFirstDayOfWeek(Calendar.SUNDAY); // US style + calTo.setFirstDayOfWeek(Calendar.SUNDAY); // US style + } + + @Override + protected void defineConsolidationConditionAfterAddition(OHLCVItem finalItem) { + defineTimeInterval(calFrom, calTo, finalItem.getTimeStamp(), period); + } + + @Override + protected void defineConsolidationConditionAfterUpdate(OHLCVItem finalItem) { + // nothing to do + } + + @Override + protected boolean checkConsolidationCondition(OHLCVItem lastItem, OHLCVItem incrementItem) { + // if the increment is inside in the interval - it has to be consolidated with lastItem + Date incrementTimestamp = incrementItem.getTimeStamp(); + return incrementTimestamp.getTime() <= calTo.getTimeInMillis() && incrementTimestamp.getTime() > calFrom.getTimeInMillis(); + } + + @Override + protected OHLCVItem processConsolidation(OHLCVItem lastItem, OHLCVItem incrementItem) { + // for final item used the end timestamp of the re-sample interval (calTo timestamp) + return consolidationComputation.consolidate(calTo.getTime(), lastItem, incrementItem); + } + + /** + * Defines consolidation condition interval + */ + private void defineTimeInterval(Calendar calFrom, Calendar calTo, Date aDate, IntradayPeriod period) { + double periodValue = period.getPeriodValue(); + IntradayPeriod.IntradayPeriodEnum intradayPeriodEnum = period.getPeriod(); + double toTime; + switch (intradayPeriodEnum) { + case S: + toTime = 1000; + break; + case M: + toTime = 60000; + break; + case H: + toTime = 3600000; + break; + default: + throw new IllegalArgumentException("This type of Intraday period is not supported!"); + } + Calendar ttStart = Calendar.getInstance(); + //note: there is possibility to defined more complex time template which starts day before. + //In this case, there is necessary to ttStart calculate by formula and decrease the day from aDate. + ttStart.setTime(aDate); + ttStart.set(Calendar.HOUR_OF_DAY, tt == null ? 0 : tt.from.get(Calendar.HOUR_OF_DAY)); + ttStart.set(Calendar.MINUTE, tt == null ? 0 : tt.from.get(Calendar.MINUTE)); + ttStart.set(Calendar.SECOND, 0); + ttStart.set(Calendar.MILLISECOND, 0); + + double ttEndTime = -1.0; + if (tt != null) { + Calendar ttEnd = Calendar.getInstance(); + ttEnd.setTime(aDate); + ttEnd.set(Calendar.HOUR_OF_DAY, tt.to.get(Calendar.HOUR_OF_DAY)); + ttEnd.set(Calendar.MINUTE, tt.to.get(Calendar.MINUTE)); + ttEnd.set(Calendar.SECOND, 59); + ttEnd.set(Calendar.MILLISECOND, 999999999); + + ttEndTime = ttEnd.getTime().getTime() / toTime; + } + + double aDateTime = aDate.getTime() / toTime; + double ttStartTime = ttStart.getTime().getTime() / toTime; + + double diff = aDateTime - ttStartTime; + double n = Math.floor(diff / periodValue); + + double resampleFrom; + double resampleTo; + resampleFrom = ttStartTime + periodValue * (diff % periodValue == 0 ? n - 1 : n); + resampleTo = resampleFrom + periodValue; + if (tt != null && resampleTo >= ttEndTime) { + resampleTo = ttEndTime; + } + + calFrom.setTime(new Date(Math.round(resampleFrom * toTime))); + calTo.setTime(new Date(Math.round(resampleTo * toTime))); + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/VolumeIncrementalOhlcvConsolidation.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/VolumeIncrementalOhlcvConsolidation.java new file mode 100644 index 000000000..9ecf32cdc --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/consolidate/VolumeIncrementalOhlcvConsolidation.java @@ -0,0 +1,47 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ +package de.gsi.chart.samples.financial.service.consolidate; + +import java.util.Calendar; + +import de.gsi.chart.samples.financial.dos.Interval; +import de.gsi.chart.samples.financial.dos.OHLCVItem; +import de.gsi.chart.samples.financial.service.consolidate.OhlcvTimeframeConsolidation.OhlcvConsolidationComputation; +import de.gsi.chart.samples.financial.service.period.IntradayPeriod; + +/** + * Volume based financial charts + * + * @author afischer + */ +public class VolumeIncrementalOhlcvConsolidation extends AbstractIncrementalOhlcvConsolidation { + private double volumeDiff; + private final double volumePeriod; + + public VolumeIncrementalOhlcvConsolidation(OhlcvConsolidationComputation consolidationComputation, + IntradayPeriod period, Interval tt, + OhlcvConsolidationAddon[] _ohlcvConsolidationAddons) { + super(consolidationComputation, period, tt, _ohlcvConsolidationAddons); + this.volumePeriod = period.getPeriodValue(); + } + + @Override + protected void defineConsolidationConditionAfterAddition(OHLCVItem finalItem) { + defineConsolidationCondition(finalItem); + } + + @Override + protected void defineConsolidationConditionAfterUpdate(OHLCVItem finalItem) { + defineConsolidationCondition(finalItem); + } + + @Override + protected boolean checkConsolidationCondition(OHLCVItem lastItem, OHLCVItem incrementItem) { + return incrementItem.getVolume() <= volumeDiff; + } + + private void defineConsolidationCondition(OHLCVItem finalItem) { + volumeDiff = volumePeriod - finalItem.getVolume(); + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/period/EodPeriod.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/period/EodPeriod.java new file mode 100644 index 000000000..d236fdca7 --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/period/EodPeriod.java @@ -0,0 +1,52 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ +package de.gsi.chart.samples.financial.service.period; + +/** + * End-of-Day Periods Domain object + * + * @author afischer + */ +public class EodPeriod extends Period { + public static final EodPeriod DAILY = new EodPeriod(); + + public enum PeriodEnum { + DAILY, + WEEKLY, + MONTHLY + } + + private final PeriodEnum period; + + public EodPeriod() { + this(PeriodEnum.DAILY); + } + + public EodPeriod(PeriodEnum period) { + this.period = period; + } + + public PeriodEnum getPeriod() { + return period; + } + + @Override + public long getMillis() { + switch (period) { + case DAILY: + return 24 * 60 * 60 * 1000; + case WEEKLY: + return 7 * 24 * 60 * 60 * 1000; + case MONTHLY: + return 2592000000L; + default: + throw new IllegalArgumentException("The method getMillis() is not supported for this type of period: " + this); + } + } + + @Override + public String toString() { + return period.toString(); + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/period/IntradayPeriod.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/period/IntradayPeriod.java new file mode 100644 index 000000000..2a45d2c8b --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/period/IntradayPeriod.java @@ -0,0 +1,132 @@ +/** + * LGPL-3.0, 2020/21, GSI-CS-CO/Chart-fx, BTA HF OpenSource Java-FX Branch, Financial Charts + */ +package de.gsi.chart.samples.financial.service.period; + +/** + * Intraday Periods Domain object + * + * @author afischer + */ +public class IntradayPeriod extends Period { + public enum IntradayPeriodEnum { + T, // ticks + S, // seconds + M, // minutes + H, // hours + RB, // range bars + V // volume + } + + private final IntradayPeriodEnum period; + private final double periodValue; + private final Double minimalMoveSymbol; + private final boolean extendedCalculation; + private final String calculationAddonServicesType; + + public IntradayPeriod(IntradayPeriodEnum period, double periodValue) { + super(PeriodType.INTRA); + this.period = period; + this.periodValue = periodValue; + this.minimalMoveSymbol = null; + this.extendedCalculation = false; + this.calculationAddonServicesType = null; // not used + } + + public IntradayPeriod(IntradayPeriodEnum period, double periodValue, Double minimalMoveSymbol, + boolean extendedCalculation, String calculationAddonServicesType) { + super(PeriodType.INTRA); + this.period = period; + this.periodValue = periodValue; + this.minimalMoveSymbol = minimalMoveSymbol; + this.extendedCalculation = extendedCalculation; + this.calculationAddonServicesType = calculationAddonServicesType; + } + + public IntradayPeriodEnum getPeriod() { + return period; + } + + public double getPeriodValue() { + return periodValue; + } + + /** + * @return provides type of ADDONs for OHLC calculation services + */ + public String getCalculationAddonServicesType() { + return calculationAddonServicesType; + } + + /** + * @return minimal move of market is necessary for range bars + */ + public Double getMinimalMoveSymbol() { + return minimalMoveSymbol; + } + + /** + * @return defines calculation of extended bid ask volumes for order flow + */ + public boolean isExtendedCalculation() { + return extendedCalculation; + } + + @Override + public long getMillis() { + switch (period) { + case S: + return 1000 * Math.round(periodValue); + case M: + return 60 * 1000 * Math.round(periodValue); + case H: + return 60 * 60 * 1000 * Math.round(periodValue); + + default: + return 60 * 1000; + //throw new IllegalArgumentException("The method getMillis() is not supported for this type of period: " + this); + } + } + + @Override + public String toString() { + return periodValue + period.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = super.hashCode(); + result = prime * result + ((period == null) ? 0 : period.hashCode()); + long temp; + temp = Double.doubleToLongBits(periodValue); + result = prime * result + (int) (temp ^ (temp >>> 32)); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (!super.equals(obj)) + return false; + if (getClass() != obj.getClass()) + return false; + IntradayPeriod other = (IntradayPeriod) obj; + if (period != other.period) + return false; + if (Double.doubleToLongBits(periodValue) != Double.doubleToLongBits(other.periodValue)) + return false; + return true; + } + + public static IntradayPeriod convert(String periodString) { + if (periodString == null || "".equals(periodString)) { + return new IntradayPeriod(IntradayPeriodEnum.T, 1); + } + String periodSymbol = periodString.substring(periodString.length() - 1); + double periodValue = Double.parseDouble(periodString.substring(0, periodString.length() - 1)); + + return new IntradayPeriod(IntradayPeriodEnum.valueOf(periodSymbol), periodValue); + } +} diff --git a/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/period/Period.java b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/period/Period.java new file mode 100644 index 000000000..347b6283a --- /dev/null +++ b/chartfx-samples/src/main/java/de/gsi/chart/samples/financial/service/period/Period.java @@ -0,0 +1,50 @@ +package de.gsi.chart.samples.financial.service.period; + +public abstract class Period { + public enum PeriodType { + EOD, + INTRA + } + + private final PeriodType type; + + public Period(PeriodType type) { + this.type = type; + } + + public Period() { + this(PeriodType.EOD); + } + + /** + * @return common type of the time period + */ + public PeriodType getType() { + return type; + } + + /** + * @return get period in millis + */ + public abstract long getMillis(); + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Period other = (Period) obj; + return type == other.type; + } +} diff --git a/chartfx-samples/src/main/resources/de/gsi/chart/samples/financial/NQ-201609-GLOBEX.scid b/chartfx-samples/src/main/resources/de/gsi/chart/samples/financial/NQ-201609-GLOBEX.scid new file mode 100644 index 000000000..c014e67ef Binary files /dev/null and b/chartfx-samples/src/main/resources/de/gsi/chart/samples/financial/NQ-201609-GLOBEX.scid differ diff --git a/docs/pics/FinancialRealtimeCandlestickSample.png b/docs/pics/FinancialRealtimeCandlestickSample.png new file mode 100644 index 000000000..9e3d60141 Binary files /dev/null and b/docs/pics/FinancialRealtimeCandlestickSample.png differ