Skip to content

Commit

Permalink
Financial Charts - realtime processing, re-sample live demos, improve…
Browse files Browse the repository at this point in the history
…d renderers, navigation etc. (#326)

* Financial Charts - real-time processing, re-sample live demos, improved renderers.
* Value and range indicator testing for financial charts.
  • Loading branch information
raven2cz authored Dec 8, 2020
1 parent 20ae707 commit c0ea477
Show file tree
Hide file tree
Showing 49 changed files with 3,003 additions and 79 deletions.
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -248,11 +248,12 @@ mvn exec:java

<table>
<tr>
<td><figure><img src="docs/pics/FinancialCandlestickSample.png" alt="FinancialCandlestickSample" width=600/><figcaption><a href="chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialCandlestickSample.java">FinancialCandlestickSample.java</a></figcaption></figure></td>
<td><figure><img src="docs/pics/FinancialHiLowSample.png" alt="FinancialHiLowSample" width=600/><figcaption><a href="chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialHiLowSample.java">FinancialHiLowSample.java</a></figcaption></figure></td>
<td><figure><img src="docs/pics/FinancialCandlestickSample.png" alt="FinancialCandlestickSample" width=600/><figcaption><a href="chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialCandlestickSample.java">FinancialCandlestickSample.java (Several Themes Supported)</a></figcaption></figure></td>
<td><figure><img src="docs/pics/FinancialHiLowSample.png" alt="FinancialHiLowSample" width=600/><figcaption><a href="chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialHiLowSample.java">FinancialHiLowSample.java (OHLC Renderer)</a></figcaption></figure></td>
</tr>
<tr>
<td><figure><img src="docs/pics/FinancialAdvancedCandlestickSample.png" alt="FinancialAdvancedCandlestickSample" width=600/><figcaption><a href="chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialAdvancedCandlestickSample.java">FinancialAdvancedCandlestickSample.java</a></figcaption></figure></td>
<td><figure><img src="docs/pics/FinancialAdvancedCandlestickSample.png" alt="FinancialAdvancedCandlestickSample" width=600/><figcaption><a href="chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialAdvancedCandlestickSample.java">FinancialAdvancedCandlestickSample.java (Advanced PaintBars and Extension Points)</a></figcaption></figure></td>
<td><figure><img src="docs/pics/FinancialRealtimeCandlestickSample.png" alt="FinancialAdvancedCandlestickSample" width=600/><figcaption><a href="chartfx-samples/src/main/java/de/gsi/chart/samples/financial/FinancialRealtimeCandlestickSample.png">FinancialRealtimeCandlestickSample.java (OHLC Tick Replay Real-time processing)</a></figcaption></figure></td>
</tr>
</table>

Expand Down
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* Style Classes (from least to most specific):
* <ul>
* <li><b>Marker:</b> {@code value-watch-indicator-marker, {id}-value-watch-indicator-marker, {id}-value-watch-indicator-marker[index]}</li>
* <li><b>Label:</b> {@code value-watch-indicator-label, {id}-value-watch-indicator-label, {id}-value-watch-indicator-label[index]}</li>
* <li><b>Line:</b> {@code value-watch-indicator-line, {id}-value-watch-indicator-line, {id}-value-watch-indicator-line[index]}</li>
* </ul>
* 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -20,11 +29,42 @@
* <li>Shadows - specific fast shadow paintings without fx-effects</li>
* <li>Extension-point before/after painting - extend specific renderers by your changes to add EP rules.</li>
* </ul>
*
* @author afischer
*/
@SuppressWarnings({ "PMD.ExcessiveParameterList" })
public abstract class AbstractFinancialRenderer<R extends Renderer> extends AbstractDataSetManagement<R> 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
*
Expand Down Expand Up @@ -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
Expand All @@ -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<Double> 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<Double> 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;
}
}
Loading

0 comments on commit c0ea477

Please sign in to comment.