From 1789bf09484123700680ed7401cd141b4a96b9d9 Mon Sep 17 00:00:00 2001 From: Bartosz Wojciechowski <32734780+hellobart@users.noreply.github.com> Date: Sun, 24 Nov 2019 14:33:53 +0100 Subject: [PATCH] Merge pull request #5 from smartcontractkit/169768648-Bollinger-Bands-and-Moving-Average Bollinger Bands and Moving Average stats --- .../answerHistory/AnswerHistory.component.js | 38 ++++- .../answerHistory/HistoryGraph.d3.js | 148 +++++++++++++----- .../deviationHistory/DeviationGraph.d3.js | 17 +- feeds-ui/src/pages/EthUsdPage.js | 1 + .../src/state/ducks/aggregation/selectors.js | 4 + feeds-ui/src/theme.css | 22 ++- feeds-ui/src/theme/network-graph.less | 28 +++- 7 files changed, 202 insertions(+), 56 deletions(-) diff --git a/feeds-ui/src/components/answerHistory/AnswerHistory.component.js b/feeds-ui/src/components/answerHistory/AnswerHistory.component.js index 1fd554ed910..00e744db587 100644 --- a/feeds-ui/src/components/answerHistory/AnswerHistory.component.js +++ b/feeds-ui/src/components/answerHistory/AnswerHistory.component.js @@ -1,9 +1,10 @@ -import React, { useEffect, useRef } from 'react' +import React, { useEffect, useState, useRef } from 'react' import HistoryGraphD3 from './HistoryGraph.d3' -import { Icon } from 'antd' +import { Icon, Switch, Tooltip } from 'antd' function AnswerHistory({ answerHistory, options }) { const graph = useRef() + const [bollinger, setBollinger] = useState(options.bollinger) useEffect(() => { graph.current = new HistoryGraphD3(options) @@ -14,12 +15,39 @@ function AnswerHistory({ answerHistory, options }) { graph.current.update(answerHistory) }, [answerHistory]) + function onChange() { + setBollinger(!bollinger) + graph.current.toggleBollinger(!bollinger) + } + return ( <>
-

- 24h Price history {!answerHistory && } -

+
+

24h Price history {!answerHistory && }

+
+ + Statistical chart characterizing the prices and volatility + over time. +
+ + Read more. + + + } + > + Moving min max averages{' '} + +
+
+
diff --git a/feeds-ui/src/components/answerHistory/HistoryGraph.d3.js b/feeds-ui/src/components/answerHistory/HistoryGraph.d3.js index 325d3766ef4..43ddad0e696 100644 --- a/feeds-ui/src/components/answerHistory/HistoryGraph.d3.js +++ b/feeds-ui/src/components/answerHistory/HistoryGraph.d3.js @@ -5,8 +5,8 @@ import { humanizeUnixTimestamp } from 'utils' export default class HistoryGraph { margin = { top: 30, right: 30, bottom: 30, left: 50 } - width = 1200 - height = 300 + width = 1300 + height = 250 svg path tooltip @@ -22,15 +22,35 @@ export default class HistoryGraph { this.options = options } - bisectDate = d3.bisector(d => { - return d.timestamp - }).left + bisectDate = d3.bisector(d => d.timestamp).left build() { this.svg = d3 .select('.answer-history-graph') .append('svg') - .attr('viewBox', `0 0 ${1300} ${400}`) + .attr( + 'viewBox', + `0 0 ${this.width} ${this.height + + this.margin.top + + this.margin.bottom}`, + ) + + this.bollinger = this.svg + .append('g') + .attr( + 'transform', + 'translate(' + this.margin.left + ',' + this.margin.top + ')', + ) + .style('opacity', this.options.bollinger ? 1 : 0) + .attr('class', 'bollinger') + + this.bollingerArea = this.bollinger + .append('path') + .attr('class', 'bollinger-area') + + this.bollingerMa = this.bollinger + .append('path') + .attr('class', 'bollinger-ma') this.path = this.svg .append('g') @@ -40,7 +60,6 @@ export default class HistoryGraph { ) .append('path') .attr('class', 'line') - .attr('class', 'line') .attr('stroke', '#a0a0a0') .attr('fill', 'none') @@ -78,9 +97,7 @@ export default class HistoryGraph { 'transform', 'translate(' + this.margin.left + ',' + this.margin.top + ')', ) - .on('mouseout', () => { - this.tooltip.style('display', 'none') - }) + .on('mouseout', () => this.tooltip.style('display', 'none')) } update(updatedData) { @@ -131,12 +148,8 @@ export default class HistoryGraph { this.line = d3 .line() - .x(d => { - return this.x(d.timestamp) - }) - .y(d => { - return this.y(Number(d.response)) - }) + .x(d => this.x(d.timestamp)) + .y(d => this.y(Number(d.response))) .curve(d3.curveMonotoneX) this.path.datum(data).attr('d', this.line) @@ -150,30 +163,87 @@ export default class HistoryGraph { .duration(2000) .attr('stroke-dashoffset', 0) - const mousemove = () => { - const x0 = this.x.invert(d3.mouse(this.overlay.node())[0]) - const i = this.bisectDate(data, x0, 1) - const d0 = data[i - 1] - const d1 = data[i] - if (!d1) { - return - } - const d = x0 - d0.timestamp > d1.timestamp - x0 ? d1 : d0 - this.tooltip - .style('display', 'block') - .attr( - 'transform', - 'translate(' + - (this.x(d.timestamp) + this.margin.left) + - ',' + - (this.y(d.response) + this.margin.top) + - ')', - ) - this.tooltipTimestamp.text(() => humanizeUnixTimestamp(d.timestamp)) - this.tooltipPrice.text( - () => `${this.options.valuePrefix} ${d.responseFormatted}`, + this.overlay.on('mousemove', () => this.mousemove(data)) + + this.updateMa(data) + } + + mousemove(data) { + const x0 = this.x.invert(d3.mouse(this.overlay.node())[0]) + const i = this.bisectDate(data, x0, 1) + const d0 = data[i - 1] + const d1 = data[i] + if (!d1) { + return + } + const d = x0 - d0.timestamp > d1.timestamp - x0 ? d1 : d0 + this.tooltip + .style('display', 'block') + .attr( + 'transform', + 'translate(' + + (this.x(d.timestamp) + this.margin.left) + + ',' + + (this.y(d.response) + this.margin.top) + + ')', + ) + this.tooltipTimestamp.text(() => humanizeUnixTimestamp(d.timestamp)) + this.tooltipPrice.text( + () => `${this.options.valuePrefix} ${d.responseFormatted}`, + ) + } + + getBollingerBands(n, k, data) { + const bands = [] + for (let i = n - 1, len = data.length; i < len; i++) { + const slice = data.slice(i + 1 - n, i) + const mean = d3.mean(slice, d => d.response) + const stdDev = Math.sqrt( + d3.mean(slice.map(d => Math.pow(d.response - mean, 2))), ) + bands.push({ + timestamp: data[i].timestamp, + answerId: data[i].answerId, + ma: mean, + low: mean - k * stdDev, + high: mean + k * stdDev, + }) } - this.overlay.on('mousemove', () => mousemove()) + + return bands + } + + updateMa(data) { + const n = 20 // n-period of moving average + const k = 2 // k times n-period standard deviation above/below moving average + + const x = d3.scaleTime().range([0, this.width - this.margin.left]) + const y = d3.scaleLinear().range([this.height, 0]) + const bandsData = this.getBollingerBands(n, k, data) + + x.domain(d3.extent(data, d => d.timestamp)) + y.domain([d3.min(bandsData, d => d.low), d3.max(bandsData, d => d.high)]) + + const ma = d3 + .line() + .x(d => x(d.timestamp)) + .y(d => y(d.ma)) + + const bandsArea = d3 + .area() + .x(d => x(d.timestamp)) + .y0(d => y(d.low)) + .y1(d => y(d.high)) + + this.bollingerArea.datum(bandsData).attr('d', bandsArea) + + this.bollingerMa + .datum(bandsData) + .style('opacity', 0) + .attr('d', ma) + } + + toggleBollinger(toggle) { + this.bollinger.style('opacity', toggle ? 1 : 0) } } diff --git a/feeds-ui/src/components/deviationHistory/DeviationGraph.d3.js b/feeds-ui/src/components/deviationHistory/DeviationGraph.d3.js index ec470a4d1ba..3bdaaa025ab 100644 --- a/feeds-ui/src/components/deviationHistory/DeviationGraph.d3.js +++ b/feeds-ui/src/components/deviationHistory/DeviationGraph.d3.js @@ -3,8 +3,8 @@ import { humanizeUnixTimestamp } from 'utils' export default class DeviationGraph { margin = { top: 30, right: 30, bottom: 30, left: 50 } - width = 1200 - height = 200 + width = 1300 + height = 250 svg path tooltip @@ -29,7 +29,12 @@ export default class DeviationGraph { this.svg = d3 .select('.deviation-history-graph') .append('svg') - .attr('viewBox', `0 0 ${1300} ${400}`) + .attr( + 'viewBox', + `0 0 ${this.width} ${this.height + + this.margin.top + + this.margin.bottom}`, + ) this.path = this.svg .append('g') @@ -65,19 +70,19 @@ export default class DeviationGraph { this.tooltipPercentage = this.info .append('text') .attr('class', 'deviation-history-graph--percentage') - .attr('x', '10') + .attr('x', '0') .attr('y', '0') this.tooltipPrice = this.info .append('text') .attr('class', 'deviation-history-graph--price') - .attr('x', '10') + .attr('x', '0') .attr('y', '15') this.tooltipTimestamp = this.info .append('text') .attr('class', 'deviation-history-graph--timestamp') - .attr('x', '10') + .attr('x', '0') .attr('y', '30') this.overlay = this.svg diff --git a/feeds-ui/src/pages/EthUsdPage.js b/feeds-ui/src/pages/EthUsdPage.js index 913cde1b8ac..fa732eb3222 100644 --- a/feeds-ui/src/pages/EthUsdPage.js +++ b/feeds-ui/src/pages/EthUsdPage.js @@ -17,6 +17,7 @@ const OPTIONS = { counter: 300, network: 'mainnet', history: true, + bollinger: false, } const NetworkPage = ({ initContract, clearState }) => { diff --git a/feeds-ui/src/state/ducks/aggregation/selectors.js b/feeds-ui/src/state/ducks/aggregation/selectors.js index f0e43c58e2f..8423b2c4567 100755 --- a/feeds-ui/src/state/ducks/aggregation/selectors.js +++ b/feeds-ui/src/state/ducks/aggregation/selectors.js @@ -85,6 +85,10 @@ const NODE_NAMES = [ address: '0x78E76126719715Eddf107cD70f3A31dddF31f85A', name: 'Honeycomb.market', }, + { + address: '0x24A718307Ce9B2420962fd5043fb876e17430934', + name: 'Infinity Stones', + }, ] const oracles = state => state.aggregation.oracles diff --git a/feeds-ui/src/theme.css b/feeds-ui/src/theme.css index a67ba12de9d..888a3a18fbf 100644 --- a/feeds-ui/src/theme.css +++ b/feeds-ui/src/theme.css @@ -21522,7 +21522,10 @@ hr { font-weight: 600; } .answer-history { - margin: 60px 0; + margin: 120px 0; +} +.answer-history svg { + overflow: visible; } .answer-history .x-axis .domain, .answer-history .y-axis .domain, @@ -21534,10 +21537,21 @@ hr { .answer-history .y-axis text { color: #a0a0a0; } +.answer-history .bollinger-ma { + fill: none; + stroke: #d6dae2; + stroke-width: 1.5px; +} +.answer-history .bollinger-area { + fill: #e2e5ec; + opacity: 1; +} .answer-history-header { font-weight: 200; - top: 20px; position: relative; + display: flex; + align-items: center; + justify-content: space-between; } .answer-history-graph--timestamp, .answer-history-graph--price { @@ -21547,6 +21561,9 @@ hr { .deviation-history { margin: 60px 0; } +.deviation-history svg { + overflow: visible; +} .deviation-history .x-axis .domain, .deviation-history .y-axis .domain, .deviation-history .x-axis line, @@ -21559,7 +21576,6 @@ hr { } .deviation-history-header { font-weight: 200; - top: 20px; position: relative; } .deviation-history-graph--timestamp, diff --git a/feeds-ui/src/theme/network-graph.less b/feeds-ui/src/theme/network-graph.less index 31677d4af75..a090a52c713 100644 --- a/feeds-ui/src/theme/network-graph.less +++ b/feeds-ui/src/theme/network-graph.less @@ -186,7 +186,11 @@ } .answer-history { - margin: 60px 0; + margin: 120px 0; + + svg { + overflow: visible; + } .x-axis, .y-axis { @@ -199,10 +203,25 @@ } } + .bollinger { + &-ma { + fill: none; + stroke: #d6dae2; + stroke-width: 1.5px; + } + + &-area { + fill: #e2e5ec; + opacity: 1; + } + } + &-header { font-weight: 200; - top: 20px; position: relative; + display: flex; + align-items: center; + justify-content: space-between; } &-graph--timestamp, @@ -215,6 +234,10 @@ .deviation-history { margin: 60px 0; + svg { + overflow: visible; + } + .x-axis, .y-axis { .domain, @@ -228,7 +251,6 @@ &-header { font-weight: 200; - top: 20px; position: relative; }