<div id="scichart-root" ></div>
body { margin: 0; }
#scichart-root { width: 100%; height: 100vh; }
const {
SciChartDefaults,
SciChartSurface,
CategoryAxis,
NumericAxis,
FastCandlestickRenderableSeries,
OhlcDataSeries,
SciChartJsNavyTheme,
makeIncArray,
ECoordinateMode,
EAxisAlignment,
FastColumnRenderableSeries,
XyDataSeries,
ENumericFormat,
NumberRange,
MouseWheelZoomModifier,
ZoomPanModifier,
ZoomExtentsModifier,
IFillPaletteProvider,
DefaultPaletteProvider,
parseColorToUIntArgb,
EAutoRange,
EXyDirection
} = SciChart;
// 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;
}
overrideStrokeArgb(xValue, yValue, index, opacity, metadata) {
return this.overrideFillArgb(xValue, yValue, index, opacity, metadata);
}
}
// Helper function to fetch candlestick data from Binance via Rest API
const getCandles = async (
symbol,
interval,
limit = 300
) => {
let url = `https://api.binance.us/api/v3/klines?symbol=${symbol}&interval=${interval}`;
if (limit) {
url += `&limit=${limit}`;
}
try {
console.log(`SimpleBinanceClient: Fetching ${limit} candles of ${symbol} ${interval}`);
const response = await fetch(url);
// Returned data format is [ { date, open, high, low, close, volume }, ... ]
const data = await response.json();
// Map to { dateValues[], openValues[], highValues[], lowValues[], closeValues[] } expected by scichart.js
const dateValues = [];
const openValues = [];
const highValues = [];
const lowValues = [];
const closeValues = [];
const volumeValues = [];
data.forEach(candle => {
const [timestamp, open, high, low, close, volume] = candle;
dateValues.push(timestamp / 1000); // SciChart expects Unix Timestamp / 1000
openValues.push(parseFloat(open));
highValues.push(parseFloat(high));
lowValues.push(parseFloat(low));
closeValues.push(parseFloat(close));
volumeValues.push(parseFloat(volume));
});
return { dateValues, openValues, highValues, lowValues, closeValues, volumeValues };
} catch (err) {
console.error(err);
return [];
}
};
// Initializes a volume profile chart with SciChart.js
async function volumeProfile(divElementId) {
SciChartDefaults.performanceWarnings = false;
const { wasmContext, sciChartSurface } = await SciChartSurface.create(divElementId, {
theme: new SciChartJsNavyTheme()
});
sciChartSurface.xAxes.add(new NumericAxis(wasmContext, {
labelFormat: ENumericFormat.Date_HHMMSS
}));
const priceYAxis = new NumericAxis(wasmContext, {
labelPrefix: "$",
labelPrecision: 2
});
sciChartSurface.yAxes.add(priceYAxis);
const volumeYAxis = new NumericAxis(wasmContext, {
id: "volumeAxisId",
isVisible: false,
growBy: new NumberRange(0, 4)
});
sciChartSurface.yAxes.add(volumeYAxis);
// Data format is { dateValues[], openValues[], highValues[], lowValues[], closeValues[] }
const { dateValues, openValues, highValues, lowValues, closeValues, volumeValues }
= await getCandles("BTCUSDT", "1h", 1000);
// Create a OhlcDataSeries with open, high, low, close values
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);
// 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);
// 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 => {
console.log(`${args.visibleRange.min}, ${args.visibleRange.max}`);
volXAxis.visibleRange = new NumberRange(args.visibleRange.min, args.visibleRange.max)
});
sciChartSurface.zoomExtents();
// This code provides faked normally distributed data for a volume profile
// const volWidth = 10;
// const numbars = Math.round(priceYAxis.visibleRange.diff / volWidth);
// const xVolValues = makeIncArray(numbars, volWidth, n => n + priceYAxis.visibleRange.min);
// const yVolValues = makeIncArray(
// numbars,
// 1,
// v => Math.exp(-Math.pow(v - numbars / 2, 2) / 20) + Math.random() * 0.5
// );
// Whereas this code actually calculates the volume profile
//
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);
}
// 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);
// add interactivity for the example
// Keep the volume profile pinned to the right, by not modifying the volume y axis
sciChartSurface.chartModifiers.add(new MouseWheelZoomModifier({ excludedYAxisIds: ["volumeAxisId", "VolY"], xyDirection: EXyDirection.XDirection }));
sciChartSurface.chartModifiers.add(new ZoomPanModifier({ excludedYAxisIds: ["volumeAxisId", "VolY"]}));
sciChartSurface.chartModifiers.add(new ZoomExtentsModifier());
};
volumeProfile("scichart-root");
This Pen doesn't use any external CSS resources.