Skip to content

Commit

Permalink
Merge pull request #5 from smartcontractkit/169768648-Bollinger-Bands…
Browse files Browse the repository at this point in the history
…-and-Moving-Average

Bollinger Bands and Moving Average stats
  • Loading branch information
hellobart authored and rupurt committed Jan 31, 2020
1 parent 2ccea2c commit 1789bf0
Show file tree
Hide file tree
Showing 7 changed files with 202 additions and 56 deletions.
38 changes: 33 additions & 5 deletions feeds-ui/src/components/answerHistory/AnswerHistory.component.js
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -14,12 +15,39 @@ function AnswerHistory({ answerHistory, options }) {
graph.current.update(answerHistory)
}, [answerHistory])

function onChange() {
setBollinger(!bollinger)
graph.current.toggleBollinger(!bollinger)
}

return (
<>
<div className="answer-history">
<h2 className="answer-history-header">
24h Price history {!answerHistory && <Icon type="loading" />}
</h2>
<div className="answer-history-header">
<h2>24h Price history {!answerHistory && <Icon type="loading" />}</h2>
<div className="answer-history-options">
<Tooltip
title={
<>
Statistical chart characterizing the prices and volatility
over time.
<br />
<a
style={{ color: 'white', fontWeight: 'bold' }}
target="_BLANK"
rel="noopener noreferrer"
href={`https://en.wikipedia.org/wiki/Bollinger_Bands`}
>
Read more.
</a>
</>
}
>
Moving min max averages{' '}
<Switch defaultChecked={options.bollinger} onChange={onChange} />
</Tooltip>
</div>
</div>
<div className="answer-history-graph"></div>
</div>
</>
Expand Down
148 changes: 109 additions & 39 deletions feeds-ui/src/components/answerHistory/HistoryGraph.d3.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
Expand All @@ -40,7 +60,6 @@ export default class HistoryGraph {
)
.append('path')
.attr('class', 'line')
.attr('class', 'line')
.attr('stroke', '#a0a0a0')
.attr('fill', 'none')

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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)
}
}
17 changes: 11 additions & 6 deletions feeds-ui/src/components/deviationHistory/DeviationGraph.d3.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions feeds-ui/src/pages/EthUsdPage.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ const OPTIONS = {
counter: 300,
network: 'mainnet',
history: true,
bollinger: false,
}

const NetworkPage = ({ initContract, clearState }) => {
Expand Down
4 changes: 4 additions & 0 deletions feeds-ui/src/state/ducks/aggregation/selectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ const NODE_NAMES = [
address: '0x78E76126719715Eddf107cD70f3A31dddF31f85A',
name: 'Honeycomb.market',
},
{
address: '0x24A718307Ce9B2420962fd5043fb876e17430934',
name: 'Infinity Stones',
},
]

const oracles = state => state.aggregation.oracles
Expand Down
22 changes: 19 additions & 3 deletions feeds-ui/src/theme.css
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 {
Expand All @@ -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,
Expand All @@ -21559,7 +21576,6 @@ hr {
}
.deviation-history-header {
font-weight: 200;
top: 20px;
position: relative;
}
.deviation-history-graph--timestamp,
Expand Down
Loading

0 comments on commit 1789bf0

Please sign in to comment.