Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
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>
@menrichky

This comment has been minimized.

Copy link

commented Mar 14, 2018

Hi,
How to use CVS data like below instead of Random data?

Date Open High Low Close Volume
9-Jun-14 62.40 63.34 61.79 62.88 37617413
6-Jun-14 63.37 63.48 62.15 62.50 42442096
5-Jun-14 63.66 64.36 62.82 63.19 47352368
4-Jun-14 62.45 63.59 62.07 63.34 36513991
3-Jun-14 62.62 63.42 62.32 62.87 32216707

@kurawadiprasanna

This comment has been minimized.

Copy link

commented Mar 26, 2019

Hi Colin, This is an excellent solution for presenting the market profile. You are really doing great. Was trying to understand how it works with custom data from CSV file.

But, it is adding all the special characters to the chart. Any suggestions..?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.