Skip to content

Instantly share code, notes, and snippets.

@ColinEberhardt
Last active October 14, 2023 11:48
Show Gist options
  • Star 8 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ColinEberhardt/0391a200d09c05883f8181f8093268f2 to your computer and use it in GitHub Desktop.
Save ColinEberhardt/0391a200d09c05883f8181f8093268f2 to your computer and use it in GitHub Desktop.
Market Profile Chart
license: mit
const createMarketProfile = (data, priceBuckets) => {
// find the price bucket size
const priceStep = priceBuckets[1] - priceBuckets[0];
// determine whether a datapoint is within a bucket
const inBucket = (datum, priceBucket) =>
datum.low < priceBucket && datum.high > (priceBucket - priceStep);
// the volume contribution for this range
const volumeInBucket = (datum, priceBucket) =>
inBucket(datum, priceBucket) ? datum.volume / Math.ceil((datum.high - datum.low) / priceStep) : 0;
// map each point in our time series, to construct the market profile
const marketProfile = data.map(
(datum, index) => priceBuckets.map(priceBucket => {
// determine how many points to the left are also within this time bucket
const base = d3.sum(data.slice(0, index)
.map(d => volumeInBucket(d, priceBucket)));
return {
base,
value: base + volumeInBucket(datum, priceBucket),
price: priceBucket
};
})
);
// similar to d3-stack - cache the underlying data
marketProfile.data = data;
return marketProfile;
};
const seriesMarketProfile = () => {
let xScale, yScale;
let bandwidth = 20;
const join = fc.dataJoin('g', 'profile');
const barSeries = fc.autoBandwidth(fc.seriesSvgBar())
.orient('horizontal')
.crossValue(d => d.price)
.mainValue(d => d.value)
.baseValue(d => d.base);
const colorScale = d3.scaleSequential(d3.interpolateSpectral);
const repeatSeries = fc.seriesSvgRepeat()
.series(barSeries)
.orient('horizontal')
.decorate((selection) => {
selection.enter()
.each((data, index, group) => {
d3.select(group[index])
.selectAll('g.bar')
.attr('fill', () => colorScale(index));
});
});
const series = (selection) => {
selection.each((data, index, group) => {
const xDomain = d3.extent(_.flattenDeep(data).map(d => d.value));
colorScale.domain([0, data.length]);
join(d3.select(group[index]), data)
.each((marketProfile, index, group) => {
// create a composite scale that applies the required offset
const leftEdge = xScale(marketProfile.data[0].date);
const offset = d3.scaleLinear()
.domain(xDomain)
.range([leftEdge, leftEdge + bandwidth]);
repeatSeries.yScale(yScale)
.xScale(offset);
d3.select(group[index])
.call(repeatSeries);
});
})
};
series.xScale = (...args) => {
if (!args.length) {
return xScale;
}
xScale = args[0];
return series;
};
series.bandwidth = (...args) => {
if (!args.length) {
return bandwidth;
}
bandwidth = args[0];
return series;
};
series.yScale = (...args) => {
if (!args.length) {
return yScale;
}
yScale = args[0];
return series;
};
return series;
}
const pointOfControl = (marketProfile) =>
_.maxBy(_.flatten(marketProfile), d => d.value).price;
// create some random financial data
const generator = fc.randomFinancial()
.interval(d3.timeMinute)
const timeSeries = generator(12 * 8);
// determine the price range
const extent = fc.extentLinear()
.accessors([d => d.high, d => d.low]);
const priceRange = extent(timeSeries);
// use a d3 scale to create a set of price buckets
const priceScale = d3.scaleLinear()
.domain(priceRange);
const priceBuckets = priceScale.ticks(40);
const series = _.chunk(timeSeries, 12)
.map((data) => createMarketProfile(data, priceBuckets));
const marketProfileSeries = fc.autoBandwidth(seriesMarketProfile());
const pocSeries = fc.autoBandwidth(fc.seriesSvgErrorBar())
.crossValue(d => d.date)
.lowValue(d => d.value)
.highValue(d => d.value)
.align('left');
const multiSeries = fc.seriesSvgMulti()
.series([marketProfileSeries, pocSeries])
.mapping((data, index, series) => {
switch(series[index]) {
case pocSeries:
return data.map(d => ({
date: d.data[0].date,
value: pointOfControl(d)
}));
case marketProfileSeries:
return data;
}
});
const xExtent = fc.extentDate()
.accessors([d => d.data[0].date]);
const profileChart = fc.chartSvgCartesian(
d3.scaleBand(),
d3.scaleBand()
)
.xDomain(series.map(s => s.data[0].date))
.yDomain(priceBuckets)
.yTickValues(priceBuckets.filter((d, i) => i % 4 == 0))
.xTickFormat(d3.timeFormat('%H:%M'))
.yOrient('left')
.xPadding(0.3)
.plotArea(multiSeries);
d3.select('#chart')
.datum(series)
.call(profileChart);
<!DOCTYPE html>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/d3fc@13.0.1"></script>
<script src="https://unpkg.com/lodash@4.17.4"></script>
<script src="https://unpkg.com/d3-scale-chromatic@1.1.1"></script>
<style>
g.profile g.multi {
opacity: 0.8;
}
g.profile g.multi:hover {
opacity: 1.0;
}
</style>
<div id='chart' style='height: 500px'></div>
<script src='chart.js'></script>
@cadentic
Copy link

hey

can u create an order flow analysis chart and tape reading panel ? and market profile for any zoomed or focused bar? actually it is quite interesting with d3js .

thanks
sayantan

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment