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) ? 1 : 0; | |
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 timePeriods = 40; | |
// create some random financial data | |
const generator = fc.randomFinancial() | |
.interval(d3.timeMinute) | |
const timeSeries = generator(timePeriods); | |
// 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(20); | |
const marketProfile = createMarketProfile(timeSeries, priceBuckets); | |
const colorScale = d3.scaleSequential(d3.interpolateSpectral) | |
.domain([0, timePeriods]); | |
const barSeries = fc.autoBandwidth(fc.seriesSvgBar()) | |
.orient('horizontal') | |
.align('left') | |
.crossValue(d => d.price) | |
.mainValue(d => d.value) | |
.baseValue(d => d.base); | |
const repeat = 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 xExtent = fc.extentLinear() | |
.accessors([d => d.value]) | |
.include([0]) | |
const profileChart = fc.chartCartesian( | |
d3.scaleLinear(), | |
d3.scaleBand() | |
) | |
.xDomain(xExtent(_.flattenDeep(marketProfile))) | |
.yDomain(priceBuckets) | |
.yTickValues(priceBuckets.filter((d, i) => i % 4 == 0)) | |
.svgPlotArea(repeat); | |
d3.select('#chart') | |
.datum(marketProfile) | |
.call(profileChart); |
<!DOCTYPE html> | |
<!-- include polyfills for custom event, Symbol and Custom Elements --> | |
<script src="//unpkg.com/babel-polyfill@6.26.0/dist/polyfill.js"></script> | |
<script src="//unpkg.com/custom-event-polyfill@0.3.0/custom-event-polyfill.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/document-register-element/1.8.0/document-register-element.js"></script> | |
<!-- use babel so that we can use arrow functions and other goodness in this block! --> | |
<script src="//unpkg.com/babel-standalone@6/babel.min.js"></script> | |
<script src="//unpkg.com/d3@5.5.0"></script> | |
<script src="//unpkg.com/d3fc@14.0.41"></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.multi { | |
opacity: 0.8; | |
} | |
g.multi:hover { | |
opacity: 1.0; | |
} | |
</style> | |
<div id='chart' style='height: 500px'></div> | |
<script src='chart.js' type='text/babel'></script> |