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>
@tienqnguyen
Copy link

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
Copy link

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..?

@cadentic
Copy link

cadentic commented Mar 9, 2020

good stuff but should have an option to show letters. don't u think so ?

@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