|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<style> |
|
|
|
body { |
|
font: 10px sans-serif; |
|
} |
|
|
|
text { |
|
fill: #000; |
|
} |
|
|
|
text.symbol { |
|
fill: #BBBBBB; |
|
} |
|
|
|
path { |
|
fill: none; |
|
stroke-width: 1; |
|
} |
|
|
|
path.candle { |
|
stroke: #000000; |
|
} |
|
|
|
path.candle.body { |
|
stroke-width: 0; |
|
} |
|
|
|
path.candle.up { |
|
fill: #00AA00; |
|
stroke: #00AA00; |
|
} |
|
|
|
path.candle.down { |
|
fill: #FF0000; |
|
stroke: #FF0000; |
|
} |
|
|
|
.close.annotation.up path { |
|
fill: #00AA00; |
|
} |
|
|
|
path.volume { |
|
fill: #DDDDDD; |
|
} |
|
|
|
.indicator-plot path.line { |
|
fill: none; |
|
stroke-width: 1; |
|
} |
|
|
|
.ma-0 path.line { |
|
stroke: #1f77b4; |
|
} |
|
|
|
.ma-1 path.line { |
|
stroke: #aec7e8; |
|
} |
|
|
|
.ma-2 path.line { |
|
stroke: #ff7f0e; |
|
} |
|
|
|
button { |
|
position: absolute; |
|
right: 110px; |
|
top: 25px; |
|
} |
|
|
|
path.macd { |
|
stroke: #0000AA; |
|
} |
|
|
|
path.signal { |
|
stroke: #FF9999; |
|
} |
|
|
|
path.zero { |
|
stroke: #BBBBBB; |
|
stroke-dasharray: 0; |
|
stroke-opacity: 0.5; |
|
} |
|
|
|
path.difference { |
|
fill: #BBBBBB; |
|
opacity: 0.5; |
|
} |
|
|
|
path.rsi { |
|
stroke: #000000; |
|
} |
|
|
|
path.overbought, path.oversold { |
|
stroke: #FF9999; |
|
stroke-dasharray: 5, 5; |
|
} |
|
|
|
path.middle, path.zero { |
|
stroke: #BBBBBB; |
|
stroke-dasharray: 5, 5; |
|
} |
|
|
|
.analysis path, .analysis circle { |
|
stroke: blue; |
|
stroke-width: 0.8; |
|
} |
|
|
|
.trendline circle { |
|
stroke-width: 0; |
|
display: none; |
|
} |
|
|
|
.mouseover .trendline path { |
|
stroke-width: 1.2; |
|
} |
|
|
|
.mouseover .trendline circle { |
|
stroke-width: 1; |
|
display: inline; |
|
} |
|
|
|
.dragging .trendline path, .dragging .trendline circle { |
|
stroke: darkblue; |
|
} |
|
|
|
.interaction path, .interaction circle { |
|
pointer-events: all; |
|
} |
|
|
|
.interaction .body { |
|
cursor: move; |
|
} |
|
|
|
.trendlines .interaction .start, .trendlines .interaction .end { |
|
cursor: nwse-resize; |
|
} |
|
|
|
.supstance path { |
|
stroke-dasharray: 2, 2; |
|
} |
|
|
|
.supstances .interaction path { |
|
pointer-events: all; |
|
cursor: ns-resize; |
|
} |
|
|
|
.mouseover .supstance path { |
|
stroke-width: 1.5; |
|
} |
|
|
|
.dragging .supstance path { |
|
stroke: darkblue; |
|
} |
|
|
|
.crosshair { |
|
cursor: crosshair; |
|
} |
|
|
|
.crosshair path.wire { |
|
stroke: #DDDDDD; |
|
stroke-dasharray: 1, 1; |
|
} |
|
|
|
.crosshair .axisannotation path { |
|
fill: #DDDDDD; |
|
} |
|
|
|
.tradearrow path.tradearrow { |
|
stroke: none; |
|
} |
|
|
|
.tradearrow path.buy { |
|
fill: #0000FF; |
|
} |
|
|
|
.tradearrow path.sell { |
|
fill: #9900FF; |
|
} |
|
|
|
.tradearrow path.highlight { |
|
fill: none; |
|
stroke-width: 2; |
|
} |
|
|
|
.tradearrow path.highlight.buy { |
|
stroke: #0000FF; |
|
} |
|
|
|
.tradearrow path.highlight.sell { |
|
stroke: #9900FF; |
|
} |
|
|
|
</style> |
|
<body> |
|
<button>Reset</button> |
|
<script src="http://d3js.org/d3.v4.min.js"></script> |
|
<script src="http://techanjs.org/techan.min.js"></script> |
|
<script> |
|
|
|
var dim = { |
|
width: 960, height: 500, |
|
margin: { top: 20, right: 50, bottom: 30, left: 50 }, |
|
ohlc: { height: 305 }, |
|
indicator: { height: 65, padding: 5 } |
|
}; |
|
dim.plot = { |
|
width: dim.width - dim.margin.left - dim.margin.right, |
|
height: dim.height - dim.margin.top - dim.margin.bottom |
|
}; |
|
dim.indicator.top = dim.ohlc.height+dim.indicator.padding; |
|
dim.indicator.bottom = dim.indicator.top+dim.indicator.height+dim.indicator.padding; |
|
|
|
var indicatorTop = d3.scaleLinear() |
|
.range([dim.indicator.top, dim.indicator.bottom]); |
|
|
|
var parseDate = d3.timeParse("%d-%b-%y"); |
|
|
|
var zoom = d3.zoom() |
|
.on("zoom", zoomed); |
|
|
|
var x = techan.scale.financetime() |
|
.range([0, dim.plot.width]); |
|
|
|
var y = d3.scaleLinear() |
|
.range([dim.ohlc.height, 0]); |
|
|
|
|
|
var yPercent = y.copy(); // Same as y at this stage, will get a different domain later |
|
|
|
var yInit, yPercentInit, zoomableInit; |
|
|
|
var yVolume = d3.scaleLinear() |
|
.range([y(0), y(0.2)]); |
|
|
|
var candlestick = techan.plot.candlestick() |
|
.xScale(x) |
|
.yScale(y); |
|
|
|
var tradearrow = techan.plot.tradearrow() |
|
.xScale(x) |
|
.yScale(y) |
|
.y(function(d) { |
|
// Display the buy and sell arrows a bit above and below the price, so the price is still visible |
|
if(d.type === 'buy') return y(d.low)+5; |
|
if(d.type === 'sell') return y(d.high)-5; |
|
else return y(d.price); |
|
}); |
|
|
|
var sma0 = techan.plot.sma() |
|
.xScale(x) |
|
.yScale(y); |
|
|
|
var sma1 = techan.plot.sma() |
|
.xScale(x) |
|
.yScale(y); |
|
|
|
var ema2 = techan.plot.ema() |
|
.xScale(x) |
|
.yScale(y); |
|
|
|
var volume = techan.plot.volume() |
|
.accessor(candlestick.accessor()) // Set the accessor to a ohlc accessor so we get highlighted bars |
|
.xScale(x) |
|
.yScale(yVolume); |
|
|
|
var trendline = techan.plot.trendline() |
|
.xScale(x) |
|
.yScale(y); |
|
|
|
var supstance = techan.plot.supstance() |
|
.xScale(x) |
|
.yScale(y); |
|
|
|
var xAxis = d3.axisBottom(x); |
|
|
|
var timeAnnotation = techan.plot.axisannotation() |
|
.axis(xAxis) |
|
.orient('bottom') |
|
.format(d3.timeFormat('%Y-%m-%d')) |
|
.width(65) |
|
.translate([0, dim.plot.height]); |
|
|
|
var yAxis = d3.axisRight(y); |
|
|
|
var ohlcAnnotation = techan.plot.axisannotation() |
|
.axis(yAxis) |
|
.orient('right') |
|
.format(d3.format(',.2f')) |
|
.translate([x(1), 0]); |
|
|
|
var closeAnnotation = techan.plot.axisannotation() |
|
.axis(yAxis) |
|
.orient('right') |
|
.accessor(candlestick.accessor()) |
|
.format(d3.format(',.2f')) |
|
.translate([x(1), 0]); |
|
|
|
var percentAxis = d3.axisLeft(yPercent) |
|
.tickFormat(d3.format('+.1%')); |
|
|
|
var percentAnnotation = techan.plot.axisannotation() |
|
.axis(percentAxis) |
|
.orient('left'); |
|
|
|
var volumeAxis = d3.axisRight(yVolume) |
|
.ticks(3) |
|
.tickFormat(d3.format(",.3s")); |
|
|
|
var volumeAnnotation = techan.plot.axisannotation() |
|
.axis(volumeAxis) |
|
.orient("right") |
|
.width(35); |
|
|
|
var macdScale = d3.scaleLinear() |
|
.range([indicatorTop(0)+dim.indicator.height, indicatorTop(0)]); |
|
|
|
var rsiScale = macdScale.copy() |
|
.range([indicatorTop(1)+dim.indicator.height, indicatorTop(1)]); |
|
|
|
var macd = techan.plot.macd() |
|
.xScale(x) |
|
.yScale(macdScale); |
|
|
|
var macdAxis = d3.axisRight(macdScale) |
|
.ticks(3); |
|
|
|
var macdAnnotation = techan.plot.axisannotation() |
|
.axis(macdAxis) |
|
.orient("right") |
|
.format(d3.format(',.2f')) |
|
.translate([x(1), 0]); |
|
|
|
var macdAxisLeft = d3.axisLeft(macdScale) |
|
.ticks(3); |
|
|
|
var macdAnnotationLeft = techan.plot.axisannotation() |
|
.axis(macdAxisLeft) |
|
.orient("left") |
|
.format(d3.format(',.2f')); |
|
|
|
var rsi = techan.plot.rsi() |
|
.xScale(x) |
|
.yScale(rsiScale); |
|
|
|
var rsiAxis = d3.axisRight(rsiScale) |
|
.ticks(3); |
|
|
|
var rsiAnnotation = techan.plot.axisannotation() |
|
.axis(rsiAxis) |
|
.orient("right") |
|
.format(d3.format(',.2f')) |
|
.translate([x(1), 0]); |
|
|
|
var rsiAxisLeft = d3.axisLeft(rsiScale) |
|
.ticks(3); |
|
|
|
var rsiAnnotationLeft = techan.plot.axisannotation() |
|
.axis(rsiAxisLeft) |
|
.orient("left") |
|
.format(d3.format(',.2f')); |
|
|
|
var ohlcCrosshair = techan.plot.crosshair() |
|
.xScale(timeAnnotation.axis().scale()) |
|
.yScale(ohlcAnnotation.axis().scale()) |
|
.xAnnotation(timeAnnotation) |
|
.yAnnotation([ohlcAnnotation, percentAnnotation, volumeAnnotation]) |
|
.verticalWireRange([0, dim.plot.height]); |
|
|
|
var macdCrosshair = techan.plot.crosshair() |
|
.xScale(timeAnnotation.axis().scale()) |
|
.yScale(macdAnnotation.axis().scale()) |
|
.xAnnotation(timeAnnotation) |
|
.yAnnotation([macdAnnotation, macdAnnotationLeft]) |
|
.verticalWireRange([0, dim.plot.height]); |
|
|
|
var rsiCrosshair = techan.plot.crosshair() |
|
.xScale(timeAnnotation.axis().scale()) |
|
.yScale(rsiAnnotation.axis().scale()) |
|
.xAnnotation(timeAnnotation) |
|
.yAnnotation([rsiAnnotation, rsiAnnotationLeft]) |
|
.verticalWireRange([0, dim.plot.height]); |
|
|
|
var svg = d3.select("body").append("svg") |
|
.attr("width", dim.width) |
|
.attr("height", dim.height); |
|
|
|
var defs = svg.append("defs"); |
|
|
|
defs.append("clipPath") |
|
.attr("id", "ohlcClip") |
|
.append("rect") |
|
.attr("x", 0) |
|
.attr("y", 0) |
|
.attr("width", dim.plot.width) |
|
.attr("height", dim.ohlc.height); |
|
|
|
defs.selectAll("indicatorClip").data([0, 1]) |
|
.enter() |
|
.append("clipPath") |
|
.attr("id", function(d, i) { return "indicatorClip-" + i; }) |
|
.append("rect") |
|
.attr("x", 0) |
|
.attr("y", function(d, i) { return indicatorTop(i); }) |
|
.attr("width", dim.plot.width) |
|
.attr("height", dim.indicator.height); |
|
|
|
svg = svg.append("g") |
|
.attr("transform", "translate(" + dim.margin.left + "," + dim.margin.top + ")"); |
|
|
|
svg.append('text') |
|
.attr("class", "symbol") |
|
.attr("x", 20) |
|
.text("Facebook, Inc. (FB)"); |
|
|
|
svg.append("g") |
|
.attr("class", "x axis") |
|
.attr("transform", "translate(0," + dim.plot.height + ")"); |
|
|
|
var ohlcSelection = svg.append("g") |
|
.attr("class", "ohlc") |
|
.attr("transform", "translate(0,0)"); |
|
|
|
ohlcSelection.append("g") |
|
.attr("class", "axis") |
|
.attr("transform", "translate(" + x(1) + ",0)") |
|
.append("text") |
|
.attr("transform", "rotate(-90)") |
|
.attr("y", -12) |
|
.attr("dy", ".71em") |
|
.style("text-anchor", "end") |
|
.text("Price ($)"); |
|
|
|
ohlcSelection.append("g") |
|
.attr("class", "close annotation up"); |
|
|
|
ohlcSelection.append("g") |
|
.attr("class", "volume") |
|
.attr("clip-path", "url(#ohlcClip)"); |
|
|
|
ohlcSelection.append("g") |
|
.attr("class", "candlestick") |
|
.attr("clip-path", "url(#ohlcClip)"); |
|
|
|
ohlcSelection.append("g") |
|
.attr("class", "indicator sma ma-0") |
|
.attr("clip-path", "url(#ohlcClip)"); |
|
|
|
ohlcSelection.append("g") |
|
.attr("class", "indicator sma ma-1") |
|
.attr("clip-path", "url(#ohlcClip)"); |
|
|
|
ohlcSelection.append("g") |
|
.attr("class", "indicator ema ma-2") |
|
.attr("clip-path", "url(#ohlcClip)"); |
|
|
|
ohlcSelection.append("g") |
|
.attr("class", "percent axis"); |
|
|
|
ohlcSelection.append("g") |
|
.attr("class", "volume axis"); |
|
|
|
var indicatorSelection = svg.selectAll("svg > g.indicator").data(["macd", "rsi"]).enter() |
|
.append("g") |
|
.attr("class", function(d) { return d + " indicator"; }); |
|
|
|
indicatorSelection.append("g") |
|
.attr("class", "axis right") |
|
.attr("transform", "translate(" + x(1) + ",0)"); |
|
|
|
indicatorSelection.append("g") |
|
.attr("class", "axis left") |
|
.attr("transform", "translate(" + x(0) + ",0)"); |
|
|
|
indicatorSelection.append("g") |
|
.attr("class", "indicator-plot") |
|
.attr("clip-path", function(d, i) { return "url(#indicatorClip-" + i + ")"; }); |
|
|
|
// Add trendlines and other interactions last to be above zoom pane |
|
svg.append('g') |
|
.attr("class", "crosshair ohlc"); |
|
|
|
svg.append("g") |
|
.attr("class", "tradearrow") |
|
.attr("clip-path", "url(#ohlcClip)"); |
|
|
|
svg.append('g') |
|
.attr("class", "crosshair macd"); |
|
|
|
svg.append('g') |
|
.attr("class", "crosshair rsi"); |
|
|
|
svg.append("g") |
|
.attr("class", "trendlines analysis") |
|
.attr("clip-path", "url(#ohlcClip)"); |
|
svg.append("g") |
|
.attr("class", "supstances analysis") |
|
.attr("clip-path", "url(#ohlcClip)"); |
|
|
|
d3.select("button").on("click", reset); |
|
|
|
d3.csv("data.csv", function(error, data) { |
|
var accessor = candlestick.accessor(), |
|
indicatorPreRoll = 33; // Don't show where indicators don't have data |
|
|
|
data = data.map(function(d) { |
|
return { |
|
date: parseDate(d.Date), |
|
open: +d.Open, |
|
high: +d.High, |
|
low: +d.Low, |
|
close: +d.Close, |
|
volume: +d.Volume |
|
}; |
|
}).sort(function(a, b) { return d3.ascending(accessor.d(a), accessor.d(b)); }); |
|
|
|
x.domain(techan.scale.plot.time(data).domain()); |
|
y.domain(techan.scale.plot.ohlc(data.slice(indicatorPreRoll)).domain()); |
|
yPercent.domain(techan.scale.plot.percent(y, accessor(data[indicatorPreRoll])).domain()); |
|
yVolume.domain(techan.scale.plot.volume(data).domain()); |
|
|
|
var trendlineData = [ |
|
{ start: { date: new Date(2014, 2, 11), value: 72.50 }, end: { date: new Date(2014, 5, 9), value: 63.34 } }, |
|
{ start: { date: new Date(2013, 10, 21), value: 43 }, end: { date: new Date(2014, 2, 17), value: 70.50 } } |
|
]; |
|
|
|
var supstanceData = [ |
|
{ start: new Date(2014, 2, 11), end: new Date(2014, 5, 9), value: 63.64 }, |
|
{ start: new Date(2013, 10, 21), end: new Date(2014, 2, 17), value: 55.50 } |
|
]; |
|
|
|
var trades = [ |
|
{ date: data[67].date, type: "buy", price: data[67].low, low: data[67].low, high: data[67].high }, |
|
{ date: data[100].date, type: "sell", price: data[100].high, low: data[100].low, high: data[100].high }, |
|
{ date: data[130].date, type: "buy", price: data[130].low, low: data[130].low, high: data[130].high }, |
|
{ date: data[170].date, type: "sell", price: data[170].low, low: data[170].low, high: data[170].high } |
|
]; |
|
|
|
var macdData = techan.indicator.macd()(data); |
|
macdScale.domain(techan.scale.plot.macd(macdData).domain()); |
|
var rsiData = techan.indicator.rsi()(data); |
|
rsiScale.domain(techan.scale.plot.rsi(rsiData).domain()); |
|
|
|
svg.select("g.candlestick").datum(data).call(candlestick); |
|
svg.select("g.close.annotation").datum([data[data.length-1]]).call(closeAnnotation); |
|
svg.select("g.volume").datum(data).call(volume); |
|
svg.select("g.sma.ma-0").datum(techan.indicator.sma().period(10)(data)).call(sma0); |
|
svg.select("g.sma.ma-1").datum(techan.indicator.sma().period(20)(data)).call(sma1); |
|
svg.select("g.ema.ma-2").datum(techan.indicator.ema().period(50)(data)).call(ema2); |
|
svg.select("g.macd .indicator-plot").datum(macdData).call(macd); |
|
svg.select("g.rsi .indicator-plot").datum(rsiData).call(rsi); |
|
|
|
svg.select("g.crosshair.ohlc").call(ohlcCrosshair).call(zoom); |
|
svg.select("g.crosshair.macd").call(macdCrosshair).call(zoom); |
|
svg.select("g.crosshair.rsi").call(rsiCrosshair).call(zoom); |
|
svg.select("g.trendlines").datum(trendlineData).call(trendline).call(trendline.drag); |
|
svg.select("g.supstances").datum(supstanceData).call(supstance).call(supstance.drag); |
|
|
|
svg.select("g.tradearrow").datum(trades).call(tradearrow); |
|
|
|
// Stash for zooming |
|
zoomableInit = x.zoomable().domain([indicatorPreRoll, data.length]).copy(); // Zoom in a little to hide indicator preroll |
|
yInit = y.copy(); |
|
yPercentInit = yPercent.copy(); |
|
|
|
draw(); |
|
}); |
|
|
|
function reset() { |
|
zoom.scale(1); |
|
zoom.translate([0,0]); |
|
draw(); |
|
} |
|
|
|
function zoomed() { |
|
x.zoomable().domain(d3.event.transform.rescaleX(zoomableInit).domain()); |
|
y.domain(d3.event.transform.rescaleY(yInit).domain()); |
|
yPercent.domain(d3.event.transform.rescaleY(yPercentInit).domain()); |
|
|
|
draw(); |
|
} |
|
|
|
function draw() { |
|
svg.select("g.x.axis").call(xAxis); |
|
svg.select("g.ohlc .axis").call(yAxis); |
|
svg.select("g.volume.axis").call(volumeAxis); |
|
svg.select("g.percent.axis").call(percentAxis); |
|
svg.select("g.macd .axis.right").call(macdAxis); |
|
svg.select("g.rsi .axis.right").call(rsiAxis); |
|
svg.select("g.macd .axis.left").call(macdAxisLeft); |
|
svg.select("g.rsi .axis.left").call(rsiAxisLeft); |
|
|
|
// We know the data does not change, a simple refresh that does not perform data joins will suffice. |
|
svg.select("g.candlestick").call(candlestick.refresh); |
|
svg.select("g.close.annotation").call(closeAnnotation.refresh); |
|
svg.select("g.volume").call(volume.refresh); |
|
svg.select("g .sma.ma-0").call(sma0.refresh); |
|
svg.select("g .sma.ma-1").call(sma1.refresh); |
|
svg.select("g .ema.ma-2").call(ema2.refresh); |
|
svg.select("g.macd .indicator-plot").call(macd.refresh); |
|
svg.select("g.rsi .indicator-plot").call(rsi.refresh); |
|
svg.select("g.crosshair.ohlc").call(ohlcCrosshair.refresh); |
|
svg.select("g.crosshair.macd").call(macdCrosshair.refresh); |
|
svg.select("g.crosshair.rsi").call(rsiCrosshair.refresh); |
|
svg.select("g.trendlines").call(trendline.refresh); |
|
svg.select("g.supstances").call(supstance.refresh); |
|
svg.select("g.tradearrow").call(tradearrow.refresh); |
|
} |
|
|
|
</script> |
Great job, Let me know if you have ZigZag indicator on chart.
I will follow your job.
[email protected]