DEV Community

Cover image for How to create a Volume Profile in a JavaScript Financial Chart
Andrew Bt
Andrew Bt

Posted on • Originally published at scichart.com

How to create a Volume Profile in a JavaScript Financial Chart

In financial stock charts, a candlestick chart is used to visualise open, high, low, close prices of an asset over a specific timeframe. The volume (amount traded) in each specific candle is significant for traders. Sometimes areas of high or low volume can act as support/resistance (difficult areas for the price to pass). Visualising the price levels associated with high volume can be done with a Volume Profile chart.

This post covers how to create the following elements on a JavaScript Chart:

  • Candles showing price data in open, high, low, close format from an exchange
  • Volume bars coloured red/green according to buy or sell
  • A rotated** volume profile histogram** with calculated volume distribution docked to the right of the chart

For the purpose of the blog, we will be using SciChart.js - a commercial JavaScript Chart Library with a free community edition that can be used in non-commercial, educational or personal projects.

At the end of the blog post you will be able to create this in JavaScript. Full source-code is shared at the end of the post.

JavaScript Volume Profile Chart

Initializing the Chart Layout

First of all we need a layout which meets the specification.

Rotated Chart docked inside a Chart

In SciChart.js this can be achieved by creating a chart with an X,Y axis, but then adding a second X,Y axis with xAxis.axisAlignment = EAxisAlignment.Left and yAxis.axisAlignment = EAxisAlignment.Top respectively. An axis may then be hidden by setting axis.isVisible = false.

When the alignments of axis are switched in SciChart.js, you create a vertical chart, and with this library it's possible to have two sets of axis on the same chart which are transposed.

Here’s how to setup a transposed axis on the same chart as a normal X,Y axis in JavaScript using SciChart.js. This technique allows you to rotate a specific series on the chart.

const { 
  SciChartSurface,
  NumericAxis,
  SciChartJSLightTheme,
  EAxisAlignment,
  Thickness,
  FastLineRenderableSeries,
  XyDataSeries,
  NumberRange,
  ZoomPanModifier,
  MouseWheelZoomModifier,
  ZoomExtentsModifier
} = SciChart;

async function initSciChart(divElementId) {
  const { sciChartSurface, wasmContext } = await SciChartSurface.create(divElementId, { 
    title: "Custom layout JS Chart with Transposed Axis", 
    titleStyle: { fontSize: 28, fontWeight: "bold" },
    theme: new SciChartJSLightTheme()
  });

  // Add the primary x,y axis
  sciChartSurface.xAxes.add(new NumericAxis(wasmContext, {
    axisTitle: "X Axis",
    backgroundColor: "#0066ff33"
  }));
  sciChartSurface.yAxes.add(new NumericAxis(wasmContext, {
    axisTitle: "Y Axis",
    backgroundColor: "#0066ff33"
  }));

  // Add the secondary y,x axis transposed
  sciChartSurface.xAxes.add(new NumericAxis(wasmContext, {
    id: "SecondaryXAxis",
    axisAlignment: EAxisAlignment.Right,    
    axisTitle: "X Axis Transposed",
    backgroundColor: "#ff660033"
  }));
  sciChartSurface.yAxes.add(new NumericAxis(wasmContext, {
    id: "SecondaryYAxis",
    axisAlignment: EAxisAlignment.Bottom,
    axisTitle: "Y Axis Transposed",
    backgroundColor: "#ff660033",
    // growBy sets padding and yMax at 66% (2:1 ratio)
    growBy: new NumberRange(0, 2)
  }));

  // Add some series to make it clear which axis is which
  const xValues = Array.from(Array(100).keys());
  const yValues = xValues.map(x => Math.sin(x*0.3) * (150-x));

  // Blue series on primary X,Y axis
  sciChartSurface.renderableSeries.add(new FastLineRenderableSeries(wasmContext, {
    dataSeries: new XyDataSeries(wasmContext, { xValues, yValues }),
    stroke: "0066ff77", 
    strokeThickness: 5
  }));

  // Orange series on secondary, transposed, X,Y axis
  sciChartSurface.renderableSeries.add(new FastLineRenderableSeries(wasmContext, {
    yAxisId: "SecondaryYAxis",
    xAxisId: "SecondaryXAxis",
    dataSeries: new XyDataSeries(wasmContext, { xValues, yValues }),
    stroke: "ff660099", 
    strokeThickness: 5
  }));

  // Add panning, zooming interaction 
  sciChartSurface.chartModifiers.add(
    new ZoomPanModifier(),
    new MouseWheelZoomModifier(),
    new ZoomExtentsModifier()
  );
}

initSciChart("scichart-root");
Enter fullscreen mode Exit fullscreen mode

This results in the following output:

Building the Volume Profile Chart

Now that we’ve covered the basics of how to achieve the layout of a docked, rotated JavaScript chart inside the main chart, we can proceed to build a volume profile.

Setting up a JavaScript Candlestick Chart with Market Data

With a chart setup as above with transposed X,Y axis, it's now possible to extend it as follows to draw real market data, volume bars, and a rotated volume profile chart.

To fetch the market data we're using a simple REST client for Binance which can be found on Github here. To fetch candle data, call this code. The return values are arrays of dates, open, high, low, close and volume values.

// Data format is { dateValues[], openValues[], highValues[], lowValues[], closeValues[] }
const { dateValues, openValues, highValues, lowValues, closeValues, volumeValues }
     = await getCandles("BTCUSDT", "1h", 100);
Enter fullscreen mode Exit fullscreen mode

Open, High, Low, Close values can be plotted on the chart as a candlestick series, while volume values can be plotted as a Column series. There are some extra tricks in SciChart.js to ensure the columns are docked bottom, and colored according to red/green bars in the main candle series.

// Plotting the market data as candles
// 

const candleDataSeries = new OhlcDataSeries(wasmContext, {
    xValues: dateValues,
    openValues,
    highValues,
    lowValues,
    closeValues,
});

// Create and add the Candlestick series
const candlestickSeries = new FastCandlestickRenderableSeries(wasmContext, {
    strokeThickness: 1,
    dataSeries: candleDataSeries,
    dataPointWidth: 0.7,
    brushUp: "#33ff3377",
    brushDown: "#ff333377",
    strokeUp: "#77ff77",
    strokeDown: "#ff7777",
});
sciChartSurface.renderableSeries.add(candlestickSeries);

// Plotting the volume data as columns
// 

// Helper class to colour column series according to price up or down
class VolumePaletteProvider extends DefaultPaletteProvider {    

    constructor(masterData, upColor, downColor) {
        super();
        this.upColorArgb = parseColorToUIntArgb(upColor);
        this.downColorArgb = parseColorToUIntArgb(downColor);
        this.ohlcDataSeries = masterData;
    }    

    // Return up or down color for the volume bars depending on Ohlc data
    overrideFillArgb(xValue, yValue, index, opacity, metadata) {
        const isUpCandle =
            this.ohlcDataSeries.getNativeOpenValues().get(index) >=
            this.ohlcDataSeries.getNativeCloseValues().get(index);
        return isUpCandle ? this.upColorArgb : this.downColorArgb;
    }
}

// Ensure the main chart has a hidden yAxis for volume bars 
const volumeYAxis = new NumericAxis(wasmContext, {
    id: "volumeAxisId",
    isVisible: false,
    growBy: new NumberRange(0, 4)    
});
sciChartSurface.yAxes.add(volumeYAxis);

// Add a volume series docked to bottom of the chart
const volumeSeries = new FastColumnRenderableSeries(wasmContext, {
    dataPointWidth: 0.7,
    strokeThickness: 0,
    dataSeries: new XyDataSeries(wasmContext, { xValues: dateValues, yValues: volumeValues }),
    yAxisId: "volumeAxisId",
    paletteProvider: new VolumePaletteProvider(
      candleDataSeries,
      "#33ff3377",
      "#ff333377"
    ),
});
sciChartSurface.renderableSeries.add(volumeSeries);
Enter fullscreen mode Exit fullscreen mode

Calculating the Volume Profile distribution

For this bit I used GPT-4 to save myself some time doing maths :D

Here’s my prompt:

Prompt: I’d like to calculate an approximation of a volume profile from candlestick data in JavaScript Given arrays of data in JavaScript containing open, high, low, close, volume data respectively.

const dateValues = []; const openValues = []; const highValues = []; const lowValues = []; const closeValues = []; const volumeValues = [];

Assume that the volume profile price is in bins. You can specify the bin size as a function parameter. The output should be two arrays: xValues[] should be prices and yValues[] should be computed volume profile values in bins.

The answer it provided was relatively decent, with a code sample on how to compute a volume distribution given arrays of Open, High, Low, Close and Volume values. Here’s the code to calculate the volume distribution based on all candle data. Adjusting this and recalculating for a specific range of candle data should be quite easy:

const binSize = 25.0; // Define your bin size

// Function to calculate the bin for a given price
function getBin(price, binSize) {
    return Math.floor(price / binSize) * binSize;
}

// Initialize volume profile
const volumeProfile = {};

// Function to distribute volume across bins
function distributeVolume(high, low, volume, binSize) {
    const startBin = getBin(low, binSize);
    const endBin = getBin(high, binSize);

    let totalBins = (endBin - startBin) / binSize + 1;
    const volumePerBin = volume / totalBins;

    for (let bin = startBin; bin <= endBin; bin += binSize) {
        if (volumeProfile[bin]) {
            volumeProfile[bin] += volumePerBin;
        } else {
            volumeProfile[bin] = volumePerBin;
        }
    }
}

// Process each candlestick
for (let i = 0; i < highValues.length; i++) {
    distributeVolume(highValues[i], lowValues[i], volumeValues[i], binSize);
}

const xVolValues = [];
const yVolValues = [];

// Extract bins (prices) and corresponding volumes from volumeProfile
for (const [price, volume] of Object.entries(volumeProfile)) {
  xVolValues.push(parseFloat(price)); // Convert string key back to number
  yVolValues.push(volume);
}
Enter fullscreen mode Exit fullscreen mode

The computed Volume Profile data can be plotted in SciChart on a transposed X,Y axis docked to the right of the chart as follows:

// Create the transposed volume X-axis 
const volXAxis = new NumericAxis(wasmContext, {
    id: "VolX", 
    axisAlignment: EAxisAlignment.Right,
    flippedCoordinates: true,
    isVisible: false,
});
sciChartSurface.xAxes.add(volXAxis);

// Create the transposed volume Y-axis
sciChartSurface.yAxes.add(new NumericAxis(wasmContext, {
    id: "VolY",
    axisAlignment: EAxisAlignment.Bottom,
    isVisible: false,
    growBy: new NumberRange(0, 3)
}));

// When the main chart price yaxis changes, we want to update the range of the volume xAxis
priceYAxis.visibleRangeChanged.subscribe(args => {
    volXAxis.visibleRange = new NumberRange(args.visibleRange.min, args.visibleRange.max)
});

// ...

// Render the volume profile series on transposed Y, X axis. This could also be a 
// mountain series, stacked mountain series, and adding Point of Control (POC) is possible
// via Line Annotations
const volumeProfileSeries = new FastColumnRenderableSeries(wasmContext, {
  dataSeries: new XyDataSeries(wasmContext, { xValues: xVolValues, yValues: yVolValues }),
  dataPointWidth: 0.5,
  opacity: 0.33,
  fill: "White",
  strokeThickness: 0,
  xAxisId: "VolX",
  yAxisId: "VolY"
});
sciChartSurface.renderableSeries.add(volumeProfileSeries);
Enter fullscreen mode Exit fullscreen mode

Here’s the final result, also with all the code present: the fetching of candlestick data from Binance US, plotting a JavaScript Candlestick chart, plotting coloured volume bars docked to the bottom of the chart, plotting a Volume Profile docked to the right of the chart, with computed volume distribution from live market data, and with zooming, panning and mousewheel zoom behaviour.

Enjoy!

Future work & further enhancements

This sample could be expanded a lot further for use in real JS financial & trading applications.

  • The Volume Profile could be rendered as a Stacked Mountain, Stacked Column or Mountain (Area) series
  • The Bin size was hard coded to $25 in this example. In reality you'd probably want to vary this dynamically as the chart zooms.
  • The volume profile distribution was calculated from all OHLC data, in practice you'd want to calculate this from visible data only, or a day-range
  • You could add a Point of Control (POC) to the volume profile, using a HorizontalLineAnnotation at the price of maximum volume
  • Using the SubCharts API in SciChart.js, you could place multiple charts inside the main chart to show the Volume Profile for specific ranges.
  • Also using the SubCharts API, it could be placed on the axis rather than docked inside the main chart.

Top comments (1)

Collapse
 
scichart profile image
SciChart

Great post, thanks!