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;
}