de.gsi.chart.renderer.spi.financial.HighLowRenderer Maven / Gradle / Ivy
package de.gsi.chart.renderer.spi.financial;
import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.DATASET_CANDLESTICK_VOLUME_LONG_COLOR;
import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.DATASET_CANDLESTICK_VOLUME_SHORT_COLOR;
import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.DATASET_HILOW_BAR_WIDTH_PERCENTAGE;
import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.DATASET_HILOW_BODY_LINEWIDTH;
import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.DATASET_HILOW_BODY_LONG_COLOR;
import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.DATASET_HILOW_BODY_SHORT_COLOR;
import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.DATASET_HILOW_SHADOW_COLOR;
import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.DATASET_HILOW_TICK_LINEWIDTH;
import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.DATASET_HILOW_TICK_LONG_COLOR;
import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.DATASET_HILOW_TICK_SHORT_COLOR;
import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.DATASET_SHADOW_LINE_WIDTH;
import static de.gsi.chart.renderer.spi.financial.css.FinancialCss.DATASET_SHADOW_TRANSPOSITION_PERCENT;
import static de.gsi.dataset.DataSet.DIM_X;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import javafx.collections.ObservableList;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import de.gsi.chart.Chart;
import de.gsi.chart.XYChart;
import de.gsi.chart.axes.Axis;
import de.gsi.chart.axes.spi.CategoryAxis;
import de.gsi.chart.renderer.Renderer;
import de.gsi.chart.renderer.spi.financial.service.OhlcvRendererEpData;
import de.gsi.chart.renderer.spi.financial.service.RendererPaintAfterEP;
import de.gsi.chart.renderer.spi.financial.service.RendererPaintAfterEPAware;
import de.gsi.chart.renderer.spi.utils.DefaultRenderColorScheme;
import de.gsi.chart.utils.StyleParser;
import de.gsi.dataset.DataSet;
import de.gsi.dataset.spi.financial.OhlcvDataSet;
import de.gsi.dataset.spi.financial.api.attrs.AttributeModelAware;
import de.gsi.dataset.spi.financial.api.ohlcv.IOhlcvItemAware;
import de.gsi.dataset.utils.ProcessingProfiler;
/**
* High-Low renderer (OHLC-V/OI Chart)
*
* An open-high-low-close chart (also OHLC) is a type of chart typically used to illustrate movements in the price of a financial instrument over time.
* Each vertical line on the chart shows the price range (the highest and lowest prices) over one unit of time, e.g., one day or one hour.
* Tick marks project from each side of the line indicating the opening price (e.g., for a daily bar chart this would be the starting price for that day) on the left,
* and the closing price for that time period on the right. The bars may be shown in different hues depending on whether prices rose or fell in that period.
*
* The OHLC bars do not require color or fill pattern to show the Open and Close levels, and they do not create confusion in cases when,
* for example, the Open price is lower than the Close price (a bullish sign), but the Close price for the studied bar is lower
* than the Close price for the previous bar, i.e. the bar to the left on the same chart (a bearish sign).
*
* @see OHLC Chart Ivestopedia
*
* @author afischer
*/
@SuppressWarnings({ "PMD.ExcessiveMethodLength", "PMD.NPathComplexity", "PMD.ExcessiveParameterList" })
// designated purpose of this class
public class HighLowRenderer extends AbstractFinancialRenderer implements Renderer, RendererPaintAfterEPAware {
private final boolean paintVolume;
private final FindAreaDistances findAreaDistances;
protected List paintAfterEPS = new ArrayList<>();
public HighLowRenderer(boolean paintVolume) {
this.paintVolume = paintVolume;
this.findAreaDistances = paintVolume ? new XMinVolumeMaxAreaDistances() : new XMinAreaDistances();
}
public HighLowRenderer() {
this(false);
}
public boolean isPaintVolume() {
return paintVolume;
}
@Override
public Canvas drawLegendSymbol(DataSet dataSet, int dsIndex, int width, int height) {
final Canvas canvas = new Canvas(width, height);
final GraphicsContext gc = canvas.getGraphicsContext2D();
final String style = dataSet.getStyle();
gc.save();
Color longBodyColor = StyleParser.getColorPropertyValue(style, DATASET_HILOW_BODY_LONG_COLOR, Color.GREEN);
Color shortBodyColor = StyleParser.getColorPropertyValue(style, DATASET_HILOW_BODY_SHORT_COLOR, Color.RED);
gc.setStroke(shortBodyColor);
double x = width / 4.0;
gc.strokeLine(2, 3, x, 3);
gc.strokeLine(x, height - 4.0, width / 2.0 - 2.0, height - 4.0);
gc.strokeLine(x, 1, x, height - 2.0);
gc.setStroke(longBodyColor);
x = 3.0 * width / 4.0;
gc.strokeLine(x - 3.0, height - 8.0, x, height - 8.0);
gc.strokeLine(x, 5.0, x + 3.0, 5.0);
gc.strokeLine(x, 2, x, height - 2.0);
gc.restore();
return canvas;
}
@Override
protected HighLowRenderer getThis() {
return this;
}
@Override
public List render(final GraphicsContext gc, final Chart chart, final int dataSetOffset,
final ObservableList datasets) {
if (!(chart instanceof XYChart)) {
throw new InvalidParameterException(
"must be derivative of XYChart for renderer - " + this.getClass().getSimpleName());
}
final XYChart xyChart = (XYChart) chart;
// make local copy and add renderer specific data sets
final List localDataSetList = new ArrayList<>(datasets);
localDataSetList.addAll(super.getDatasets());
long start = 0;
if (ProcessingProfiler.getDebugState()) {
start = ProcessingProfiler.getTimeStamp();
}
final Axis xAxis = xyChart.getXAxis();
final Axis yAxis = xyChart.getYAxis();
final double xAxisWidth = xAxis.getWidth();
final double xmin = xAxis.getValueForDisplay(0);
final double xmax = xAxis.getValueForDisplay(xAxisWidth);
int index = 0;
for (final DataSet ds : localDataSetList) {
if (!ds.isVisible() || ds.getDimension() < 7)
continue;
final int lindex = index;
ds.lock().readLockGuardOptimistic(() -> {
// update categories in case of category axes for the first (index == '0') indexed data set
if (lindex == 0 && xyChart.getXAxis() instanceof CategoryAxis) {
final CategoryAxis axis = (CategoryAxis) xyChart.getXAxis();
axis.updateCategories(ds);
}
AttributeModelAware attrs = null;
if (ds instanceof AttributeModelAware) {
attrs = (AttributeModelAware) ds;
}
IOhlcvItemAware itemAware = null;
if (ds instanceof IOhlcvItemAware) {
itemAware = (IOhlcvItemAware) ds;
}
boolean isEpAvailable = !paintAfterEPS.isEmpty() || paintBarMarker != null;
gc.save();
// default styling level
String style = ds.getStyle();
DefaultRenderColorScheme.setLineScheme(gc, style, lindex);
DefaultRenderColorScheme.setGraphicsContextAttributes(gc, style);
// financial styling level
Color longBodyColor = StyleParser.getColorPropertyValue(style, DATASET_HILOW_BODY_LONG_COLOR, Color.GREEN);
Color shortBodyColor = StyleParser.getColorPropertyValue(style, DATASET_HILOW_BODY_SHORT_COLOR, Color.RED);
Color longTickColor = StyleParser.getColorPropertyValue(style, DATASET_HILOW_TICK_LONG_COLOR, Color.GREEN);
Color shortTickColor = StyleParser.getColorPropertyValue(style, DATASET_HILOW_TICK_SHORT_COLOR, Color.RED);
Color hiLowShadowColor = StyleParser.getColorPropertyValue(style, DATASET_HILOW_SHADOW_COLOR, null);
Color candleVolumeLongColor = StyleParser.getColorPropertyValue(style, DATASET_CANDLESTICK_VOLUME_LONG_COLOR, Color.rgb(139, 199, 194, 0.2));
Color candleVolumeShortColor = StyleParser.getColorPropertyValue(style, DATASET_CANDLESTICK_VOLUME_SHORT_COLOR, Color.rgb(235, 160, 159, 0.2));
double bodyLineWidth = StyleParser.getFloatingDecimalPropertyValue(style, DATASET_HILOW_BODY_LINEWIDTH, 1.2d);
double tickLineWidth = StyleParser.getFloatingDecimalPropertyValue(style, DATASET_HILOW_TICK_LINEWIDTH, 1.2d);
double barWidthPercent = StyleParser.getFloatingDecimalPropertyValue(style, DATASET_HILOW_BAR_WIDTH_PERCENTAGE, 0.6d);
double shadowLineWidth = StyleParser.getFloatingDecimalPropertyValue(style, DATASET_SHADOW_LINE_WIDTH, 2.5d);
double shadowTransPercent = StyleParser.getFloatingDecimalPropertyValue(style, DATASET_SHADOW_TRANSPOSITION_PERCENT, 0.5d);
if (ds.getDataCount() > 0) {
int iMin = ds.getIndex(DIM_X, xmin);
if (iMin < 0)
iMin = 0;
int iMax = Math.min(ds.getIndex(DIM_X, xmax) + 1, ds.getDataCount());
double[] distances = null;
double minRequiredWidth = 0.0;
if (lindex == 0) {
distances = findAreaDistances(findAreaDistances, ds, xAxis, yAxis, xmin, xmax);
minRequiredWidth = distances[0];
}
double localBarWidth = minRequiredWidth * barWidthPercent;
double barWidthHalf = localBarWidth / 2.0;
for (int i = iMin; i < iMax; i++) {
double x0 = xAxis.getDisplayPosition(ds.get(DIM_X, i));
double yOpen = yAxis.getDisplayPosition(ds.get(OhlcvDataSet.DIM_Y_OPEN, i));
double yHigh = yAxis.getDisplayPosition(ds.get(OhlcvDataSet.DIM_Y_HIGH, i));
double yLow = yAxis.getDisplayPosition(ds.get(OhlcvDataSet.DIM_Y_LOW, i));
double yClose = yAxis.getDisplayPosition(ds.get(OhlcvDataSet.DIM_Y_CLOSE, i));
// prepare extension point data (if EPs available)
OhlcvRendererEpData data = null;
if (isEpAvailable) {
data = new OhlcvRendererEpData();
data.gc = gc;
data.ds = ds;
data.attrs = attrs;
data.ohlcvItemAware = itemAware;
data.ohlcvItem = itemAware != null ? itemAware.getItem(i) : null;
data.index = i;
data.minIndex = iMin;
data.maxIndex = iMax;
data.barWidth = localBarWidth;
data.barWidthHalf = barWidthHalf;
data.xCenter = x0;
data.yOpen = yOpen;
data.yHigh = yHigh;
data.yLow = yLow;
data.yClose = yClose;
}
// paint volume
if (paintVolume) {
assert distances != null;
paintVolume(gc, ds, i, candleVolumeLongColor, candleVolumeShortColor, yAxis, distances, localBarWidth, barWidthHalf, x0);
}
// paint shadow
if (hiLowShadowColor != null) {
double lineWidth = gc.getLineWidth();
paintHiLowShadow(gc, hiLowShadowColor, shadowLineWidth, shadowTransPercent, barWidthHalf, x0, yOpen, yClose, yLow, yHigh);
gc.setLineWidth(lineWidth);
}
// choose color of the bar
Paint barPaint = data == null ? null : getPaintBarColor(data);
// the ohlc body
gc.setStroke(Objects.requireNonNullElse(barPaint, yOpen > yClose ? longBodyColor : shortBodyColor));
gc.setLineWidth(bodyLineWidth);
gc.strokeLine(x0, yLow, x0, yHigh);
// paint open/close tick
gc.setStroke(Objects.requireNonNullElse(barPaint, yOpen > yClose ? longTickColor : shortTickColor));
gc.setLineWidth(tickLineWidth);
gc.strokeLine(x0 - barWidthHalf, yOpen, x0, yOpen);
gc.strokeLine(x0, yClose, x0 + barWidthHalf, yClose);
// extension point - paint after painting of bar
if (!paintAfterEPS.isEmpty()) {
paintAfter(data);
}
}
}
gc.restore();
});
// possibility to re-arrange y-axis by min/max of dataset (after paint)
if (computeLocalRange()) {
applyLocalYRange(ds, yAxis, xmin, xmax);
}
index++;
}
if (ProcessingProfiler.getDebugState()) {
ProcessingProfiler.getTimeDiff(start);
}
return localDataSetList;
}
/**
* Handle extension point PaintAfter
*
* @param data filled domain object which is provided to external extension points.
*/
protected void paintAfter(OhlcvRendererEpData data) {
for (RendererPaintAfterEP paintAfterEP : paintAfterEPS) {
paintAfterEP.paintAfter(data);
}
}
/**
* Simple support for HiLow OHLC shadows painting. Without effects - performance problems.
* The shadow has to be activated by parameter configuration hiLowShadowColor in css.
*
* @param gc GraphicsContext
* @param shadowColor color for shadow
* @param shadowLineWidth line width for painting shadow
* @param shadowTransPercent object transposition for painting shadow in percentage
* @param barWidthHalf half width of bar
* @param x0 the center of the bar for X coordination
* @param yOpen coordination of Open price
* @param yClose coordination of Close price
* @param yLow coordination of Low price
* @param yHigh coordination of High price
*/
protected void paintHiLowShadow(GraphicsContext gc, Color shadowColor, double shadowLineWidth, double shadowTransPercent, double barWidthHalf,
double x0, double yOpen, double yClose, double yLow, double yHigh) {
double trans = shadowTransPercent * barWidthHalf;
gc.setLineWidth(shadowLineWidth);
gc.setStroke(shadowColor);
gc.strokeLine(x0 + trans, yLow + trans, x0 + trans, yHigh + trans);
gc.strokeLine(x0 - barWidthHalf + trans, yOpen + trans, x0 + trans, yOpen + trans);
gc.strokeLine(x0 + trans, yClose + trans, x0 + barWidthHalf + trans, yClose + trans);
}
//-------------- injections --------------------------------------------
@Override
public void addPaintAfterEp(RendererPaintAfterEP paintAfterEP) {
paintAfterEPS.add(paintAfterEP);
}
@Override
public List getPaintAfterEps() {
return paintAfterEPS;
}
}