A market profile chart, using d3 and d3fc, as described in this blog post.
Built with blockbuilder.org
license: mit |
A market profile chart, using d3 and d3fc, as described in this blog post.
Built with blockbuilder.org
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/[email protected]"></script> | |
<script src="https://unpkg.com/[email protected]"></script> | |
<script src="https://unpkg.com/[email protected]"></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> |
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
good stuff but should have an option to show letters. don't u think so ?