SPB Git forge

spb/hfmarketdata

Public

Open high-frequency market data platform — FirstRate full-history downloader, DuckDB/Parquet lake, open REST API and React docs platform (www.hfmarketdata.io)

127commits 1branches 0releases
24.7 MBsize
maindefault branch
11 days agolast push
JavaScript 53.7% Python 38.3% CSS 4.6% TypeScript 3.1%

charts: engine/data/store.js (ignoré par la règle data/ du .gitignore racine) + exception .gitignore

Simon-Pierre Boucher committed 18 days ago (Sep 7, 2026) parent 51837f4

2 changed files +178 −0

modified hfmarketdata/web/.gitignore +1 −0
@@ -8,3 +8,4 @@ e2e/.*.tmp.mjs
8 8
9 9 # src/charts/data is source code, not a data directory (root .gitignore ignores data/)
10 10 !src/charts/data/
11 +!src/charts/engine/data/
added hfmarketdata/web/src/charts/engine/data/store.js +177 −0
@@ -0,0 +1,177 @@
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