|
1 |
+// Bar store: keeps the caller's Bar[] plus column typed arrays for fast rendering and range scans. |
|
2 |
+ |
|
3 |
+export class BarStore { |
|
4 |
+ constructor(maxBars = 500_000) { |
|
5 |
+ this.maxBars = maxBars |
|
6 |
+ this.bars = [] |
|
7 |
+ this._rebuild() |
|
8 |
+ } |
|
9 |
+ |
|
10 |
+ get length() { return this.bars.length } |
|
11 |
+ |
|
12 |
+ _rebuild() { |
|
13 |
+ const n = this.bars.length |
|
14 |
+ this.t = new Float64Array(n); this.o = new Float64Array(n); this.h = new Float64Array(n) |
|
15 |
+ this.l = new Float64Array(n); this.c = new Float64Array(n); this.v = new Float64Array(n) |
|
16 |
+ for (let i = 0; i < n; i++) this._write(i, this.bars[i]) |
|
17 |
+ this._cap = n |
|
18 |
+ this.version = (this.version || 0) + 1 |
|
19 |
+ } |
|
20 |
+ |
|
21 |
+ _write(i, b) { |
|
22 |
+ this.t[i] = b.t; this.o[i] = b.o; this.h[i] = b.h; this.l[i] = b.l; this.c[i] = b.c |
|
23 |
+ this.v[i] = b.v == null ? NaN : b.v |
|
24 |
+ } |
|
25 |
+ |
|
26 |
+ set(bars) { |
|
27 |
+ this.bars = bars.slice(-this.maxBars) |
|
28 |
+ this._rebuild() |
|
29 |
+ } |
|
30 |
+ |
|
31 |
+ /** Prepend older bars (must end before the first existing bar). Returns the number actually prepended. */ |
|
32 |
+ prepend(older) { |
|
33 |
+ if (!older.length) return 0 |
|
34 |
+ const firstT = this.bars.length ? this.bars[0].t : Infinity |
|
35 |
+ let cut = older.length |
|
36 |
+ while (cut > 0 && older[cut - 1].t >= firstT) cut-- |
|
37 |
+ const add = older.slice(0, cut) |
|
38 |
+ if (!add.length) return 0 |
|
39 |
+ this.bars = add.concat(this.bars) |
|
40 |
+ if (this.bars.length > this.maxBars) this.bars = this.bars.slice(0, this.maxBars) |
|
41 |
+ this._rebuild() |
|
42 |
+ return add.length |
|
43 |
+ } |
|
44 |
+ |
|
45 |
+ /** Append newer bars (only those after the last existing bar are kept). Returns the count appended. */ |
|
46 |
+ append(newer) { |
|
47 |
+ if (!newer.length) return 0 |
|
48 |
+ const lastT = this.bars.length ? this.bars[this.bars.length - 1].t : -Infinity |
|
49 |
+ let i = 0 |
|
50 |
+ while (i < newer.length && newer[i].t <= lastT) i++ |
|
51 |
+ const add = newer.slice(i) |
|
52 |
+ if (!add.length) return 0 |
|
53 |
+ this.bars = this.bars.concat(add) |
|
54 |
+ if (this.bars.length > this.maxBars) this.bars = this.bars.slice(this.bars.length - this.maxBars) |
|
55 |
+ this._rebuild() |
|
56 |
+ return add.length |
|
57 |
+ } |
|
58 |
+ |
|
59 |
+ /** Update the last bar in place (same t) or append a new one. Returns 'update' | 'append' | 'ignored'. */ |
|
60 |
+ updateLast(bar) { |
|
61 |
+ const n = this.bars.length |
|
62 |
+ if (n && this.bars[n - 1].t === bar.t) { |
|
63 |
+ this.bars[n - 1] = bar |
|
64 |
+ this._write(n - 1, bar) |
|
65 |
+ this.version++ |
|
66 |
+ return 'update' |
|
67 |
+ } |
|
68 |
+ if (!n || bar.t > this.bars[n - 1].t) { |
|
69 |
+ this.bars.push(bar) |
|
70 |
+ if (this.bars.length > this.maxBars) { this.bars.shift(); this._rebuild(); return 'append' } |
|
71 |
+ // Grow typed arrays geometrically to avoid a full rebuild per tick. |
|
72 |
+ if (this._cap < this.bars.length) this._grow() |
|
73 |
+ this._write(n, bar) |
|
74 |
+ this.version++ |
|
75 |
+ return 'append' |
|
76 |
+ } |
|
77 |
+ return 'ignored' |
|
78 |
+ } |
|
79 |
+ |
|
80 |
+ _grow() { |
|
81 |
+ const n = this.bars.length |
|
82 |
+ const cap = Math.max(n + 1, Math.ceil(n * 1.5)) |
|
83 |
+ for (const k of ['t', 'o', 'h', 'l', 'c', 'v']) { |
|
84 |
+ const next = new Float64Array(cap) |
|
85 |
+ next.set(this[k].subarray(0, Math.min(this[k].length, n - 1))) |
|
86 |
+ this[k] = next |
|
87 |
+ } |
|
88 |
+ this._cap = cap |
|
89 |
+ } |
|
90 |
+ |
|
91 |
+ /** Index of the bar at time `t`, or the fractional position between neighbours (extrapolated at the edges). */ |
|
92 |
+ indexOfTime(t) { |
|
93 |
+ const n = this.bars.length |
|
94 |
+ if (n === 0) return 0 |
|
95 |
+ const T = this.t |
|
96 |
+ if (t <= T[0]) { |
|
97 |
+ const step = n > 1 ? T[1] - T[0] : 60_000 |
|
98 |
+ return step > 0 ? (t - T[0]) / step : 0 |
|
99 |
+ } |
|
100 |
+ if (t >= T[n - 1]) { |
|
101 |
+ const step = n > 1 ? T[n - 1] - T[n - 2] : 60_000 |
|
102 |
+ return n - 1 + (step > 0 ? (t - T[n - 1]) / step : 0) |
|
103 |
+ } |
|
104 |
+ let lo = 0, hi = n - 1 |
|
105 |
+ while (hi - lo > 1) { |
|
106 |
+ const mid = (lo + hi) >> 1 |
|
107 |
+ if (T[mid] <= t) lo = mid; else hi = mid |
|
108 |
+ } |
|
109 |
+ if (T[lo] === t) return lo |
|
110 |
+ const span = T[hi] - T[lo] |
|
111 |
+ return span > 0 ? lo + (t - T[lo]) / span : lo |
|
112 |
+ } |
|
113 |
+ |
|
114 |
+ /** Nearest existing bar index for a time (rounded). */ |
|
115 |
+ nearestIndex(t) { |
|
116 |
+ const n = this.bars.length |
|
117 |
+ if (!n) return -1 |
|
118 |
+ return Math.max(0, Math.min(n - 1, Math.round(this.indexOfTime(t)))) |
|
119 |
+ } |
|
120 |
+ |
|
121 |
+ /** Time for a (possibly fractional or out-of-range) index, extrapolating with the median bar step. */ |
|
122 |
+ timeAtIndex(i) { |
|
123 |
+ const n = this.bars.length |
|
124 |
+ if (!n) return null |
|
125 |
+ const T = this.t |
|
126 |
+ if (i >= 0 && i <= n - 1) { |
|
127 |
+ const lo = Math.floor(i), hi = Math.min(n - 1, lo + 1) |
|
128 |
+ return T[lo] + (T[hi] - T[lo]) * (i - lo) |
|
129 |
+ } |
|
130 |
+ const step = this.medianStep() |
|
131 |
+ if (i < 0) return T[0] + i * step |
|
132 |
+ return T[n - 1] + (i - (n - 1)) * step |
|
133 |
+ } |
|
134 |
+ |
|
135 |
+ medianStep() { |
|
136 |
+ const n = this.bars.length |
|
137 |
+ if (n < 2) return 60_000 |
|
138 |
+ if (this._medianStep && this._medianN === n) return this._medianStep |
|
139 |
+ const sample = [] |
|
140 |
+ const stride = Math.max(1, Math.floor(n / 200)) |
|
141 |
+ for (let i = stride; i < n; i += stride) sample.push(this.t[i] - this.t[i - stride]) |
|
142 |
+ sample.sort((a, b) => a - b) |
|
143 |
+ this._medianStep = sample[sample.length >> 1] / stride || 60_000 |
|
144 |
+ this._medianN = n |
|
145 |
+ return this._medianStep |
|
146 |
+ } |
|
147 |
+ |
|
148 |
+ /** [min low, max high] over [from, to]. */ |
|
149 |
+ minMax(from, to) { |
|
150 |
+ let lo = Infinity, hi = -Infinity |
|
151 |
+ const L = this.l, H = this.h |
|
152 |
+ for (let i = from; i <= to; i++) { |
|
153 |
+ const l = L[i], h = H[i] |
|
154 |
+ if (l < lo) lo = l |
|
155 |
+ if (h > hi) hi = h |
|
156 |
+ } |
|
157 |
+ return [lo, hi] |
|
158 |
+ } |
|
159 |
+ |
|
160 |
+ /** Index of the highest high and lowest low over [from, to]. */ |
|
161 |
+ extremes(from, to) { |
|
162 |
+ let lo = Infinity, hi = -Infinity, iLo = -1, iHi = -1 |
|
163 |
+ const L = this.l, H = this.h |
|
164 |
+ for (let i = from; i <= to; i++) { |
|
165 |
+ if (L[i] < lo) { lo = L[i]; iLo = i } |
|
166 |
+ if (H[i] > hi) { hi = H[i]; iHi = i } |
|
167 |
+ } |
|
168 |
+ return { lo, hi, iLo, iHi } |
|
169 |
+ } |
|
170 |
+ |
|
171 |
+ maxVolume(from, to) { |
|
172 |
+ let m = 0 |
|
173 |
+ const V = this.v |
|
174 |
+ for (let i = from; i <= to; i++) if (V[i] > m) m = V[i] |
|
175 |
+ return m |
|
176 |
+ } |
|
177 |
+} |
|
178 |
|