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
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):
+ *
+ * - Marker: {@code value-watch-indicator-marker, {id}-value-watch-indicator-marker, {id}-value-watch-indicator-marker[index]}
+ * - Label: {@code value-watch-indicator-label, {id}-value-watch-indicator-label, {id}-value-watch-indicator-label[index]}
+ * - Line: {@code value-watch-indicator-line, {id}-value-watch-indicator-line, {id}-value-watch-indicator-line[index]}
+ *
+ * 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 extends Axis> 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