-
Notifications
You must be signed in to change notification settings - Fork 86
/
candles.go
202 lines (180 loc) · 5.84 KB
/
candles.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
// This code is available on the terms of the project LICENSE.md file,
// also available online at https://blueoakcouncil.org/license/1.0.0.
package candles
import (
"time"
"decred.org/dcrdex/dex/msgjson"
)
const (
// DefaultCandleRequest is the number of candles to return if the request
// does not specify otherwise.
DefaultCandleRequest = 50
// CacheSize is the default cache size. Also represents the maximum number
// of candles that can be requested at once.
CacheSize = 1000
)
var (
// BinSizes is the default bin sizes for candlestick data sets. Exported for
// use in the 'config' response. Internally, we will parse these to uint64
// milliseconds.
BinSizes = []string{"24h", "1h", "5m"}
)
// Candle is a report about the trading activity of a market over some specified
// period of time. Candles are managed with a Cache, which takes into account
// bin sizes and handles candle addition.
type Candle = msgjson.Candle
// Cache is a sized cache of candles. Cache provides methods for adding to the
// cache and reading cache data out. Candles is a typical slice until it reaches
// capacity, when it becomes a "circular array" to avoid re-allocations.
type Cache struct {
Candles []Candle
BinSize uint64
cap int
// cursor will be the index of the last inserted candle.
cursor int
}
// NewCache is a constructor for a Cache.
func NewCache(cap int, binSize uint64) *Cache {
return &Cache{
cap: cap,
BinSize: binSize,
}
}
// Add adds a new candle TO THE END of the Cache. The caller is responsible to
// ensure that candles added with Add are always newer than the last candle
// added.
func (c *Cache) Add(candle *Candle) {
sz := len(c.Candles)
if sz == 0 {
c.Candles = append(c.Candles, *candle)
return
}
if c.combineCandles(c.Last(), candle) {
return
}
if sz == c.cap { // circular mode
c.cursor = (c.cursor + 1) % c.cap
c.Candles[c.cursor] = *candle
return
}
c.Candles = append(c.Candles, *candle)
c.cursor = sz // len(c.candles) - 1
}
func (c *Cache) Reset() {
c.cursor = 0
c.Candles = nil
}
// WireCandles encodes up to 'count' most recent candles as
// *msgjson.WireCandles. If the Cache contains fewer than 'count', only those
// available will be returned, with no indication of error.
func (c *Cache) WireCandles(count int) *msgjson.WireCandles {
n := count
sz := len(c.Candles)
if sz < n {
n = sz
}
wc := msgjson.NewWireCandles(n)
for i := sz - n; i < sz; i++ {
candle := &c.Candles[(c.cursor+1+i)%sz]
wc.StartStamps = append(wc.StartStamps, candle.StartStamp)
wc.EndStamps = append(wc.EndStamps, candle.EndStamp)
wc.MatchVolumes = append(wc.MatchVolumes, candle.MatchVolume)
wc.QuoteVolumes = append(wc.QuoteVolumes, candle.QuoteVolume)
wc.HighRates = append(wc.HighRates, candle.HighRate)
wc.LowRates = append(wc.LowRates, candle.LowRate)
wc.StartRates = append(wc.StartRates, candle.StartRate)
wc.EndRates = append(wc.EndRates, candle.EndRate)
}
return wc
}
// CandlesCopy returns a deep copy of Candles with the oldest candle at the
// first index.
func (c *Cache) CandlesCopy() []msgjson.Candle {
sz := len(c.Candles)
candles := make([]msgjson.Candle, sz)
switch {
case sz == 0:
case sz == c.cap: // circular mode
oldIdx := (c.cursor + 1) % c.cap
copy(candles, c.Candles[oldIdx:])
copy(candles[sz-oldIdx:], c.Candles)
default:
copy(candles, c.Candles)
}
return candles
}
// Delta calculates the change in rate, as a percentage, and total volume over
// the specified period going backwards from now. Because the first candle does
// not necessarily align with the cutoff, the rate and volume contribution from
// that candle is linearly interpreted between the endpoints. The caller is
// responsible for making sure that dur >> binSize, otherwise the results will
// be of little value.
func (c *Cache) Delta(since time.Time) (changePct float64, vol, high, low uint64) {
cutoff := uint64(since.UnixMilli())
sz := len(c.Candles)
if sz == 0 {
return 0, 0, 0, 0
}
var startRate, endRate uint64
for i := 0; i < sz; i++ {
candle := &c.Candles[(c.cursor+sz-i)%sz]
if candle.EndStamp <= cutoff {
break
}
if endRate == 0 {
endRate = candle.EndRate
if endRate == 0 {
endRate = candle.StartRate
}
}
if low == 0 || (candle.LowRate > 0 && candle.LowRate < low) {
low = candle.LowRate
}
if candle.HighRate > high {
high = candle.HighRate
}
if candle.StartStamp <= cutoff && candle.StartRate != 0 {
// Interpret the point linearly between the start and end stamps
cut := float64(cutoff-candle.StartStamp) / float64(candle.EndStamp-candle.StartStamp)
rateDelta := float64(candle.EndRate) - float64(candle.StartRate)
r := candle.StartRate + uint64(cut*rateDelta)
if r > 0 {
startRate = r
}
vol += uint64((1 - cut) * float64(candle.MatchVolume))
break
} else if candle.StartRate != 0 {
startRate = candle.StartRate
} else if candle.EndRate != 0 {
startRate = candle.EndRate
}
vol += candle.MatchVolume
}
if startRate == 0 {
return 0, vol, high, low
}
return (float64(endRate) - float64(startRate)) / float64(startRate), vol, high, low
}
// Last gets the most recent candle in the cache.
func (c *Cache) Last() *Candle {
return &c.Candles[c.cursor]
}
// combineCandles attempts to add the candidate candle to the target candle
// in-place, if they're in the same bin, otherwise returns false.
func (c *Cache) combineCandles(target, candidate *Candle) bool {
if target.EndStamp/c.BinSize != candidate.EndStamp/c.BinSize {
// The candidate candle cannot be added.
return false
}
target.EndStamp = candidate.EndStamp
target.EndRate = candidate.EndRate
if candidate.HighRate > target.HighRate {
target.HighRate = candidate.HighRate
}
if candidate.LowRate < target.LowRate || target.LowRate == 0 {
target.LowRate = candidate.LowRate
}
target.MatchVolume += candidate.MatchVolume
target.QuoteVolume += candidate.QuoteVolume
return true
}