wickchart 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/feeds.js ADDED
@@ -0,0 +1,192 @@
1
+ /* ==========================================================================
2
+ * HabView feeds — data-source helpers shared by <hab-feed> and the demo app.
3
+ * Browser module (uses fetch/WebSocket inside functions); importable in Node
4
+ * for unit-testing the pure generators.
5
+ * MIT License.
6
+ * ========================================================================== */
7
+
8
+ export const TF_SECONDS = {
9
+ '1m': 60, '3m': 180, '5m': 300, '15m': 900, '30m': 1800,
10
+ '1h': 3600, '2h': 7200, '4h': 14400, '6h': 21600, '12h': 43200,
11
+ '1d': 86400, '3d': 259200, '1w': 604800,
12
+ };
13
+
14
+ /** Seconds for a timeframe id ('15m', '1h', '1D'…); defaults to 1h. */
15
+ export function tfToSeconds(tf) {
16
+ return TF_SECONDS[String(tf).toLowerCase()] || 3600;
17
+ }
18
+
19
+ export const BASE_PRICES = { BTC: 64250, ETH: 3120, SOL: 148, DEMO: 100 };
20
+
21
+ /* ---------------- deterministic synthetic data ---------------- */
22
+
23
+ export function mulberry32(seed) {
24
+ let a = seed >>> 0;
25
+ return function () {
26
+ a |= 0;
27
+ a = (a + 0x6d2b79f5) | 0;
28
+ let t = Math.imul(a ^ (a >>> 15), 1 | a);
29
+ t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
30
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
31
+ };
32
+ }
33
+
34
+ export function gauss(rnd) {
35
+ let u = 0;
36
+ let v = 0;
37
+ while (!u) u = rnd();
38
+ while (!v) v = rnd();
39
+ return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
40
+ }
41
+
42
+ export function hashStr(s) {
43
+ let h = 2166136261;
44
+ for (const c of String(s)) {
45
+ h ^= c.charCodeAt(0);
46
+ h = Math.imul(h, 16777619);
47
+ }
48
+ return h >>> 0;
49
+ }
50
+
51
+ /**
52
+ * Deterministic synthetic OHLCV history (random walk with volatility regimes
53
+ * and mild mean reversion). Same key → same series.
54
+ * @param {string} key seed key (symbol, symbol+tf, …)
55
+ * @param {number} sec timeframe in seconds
56
+ * @param {number} n bar count
57
+ * @param {number} [base=100] starting/base price
58
+ */
59
+ export function genSynthetic(key, sec, n, base = 100) {
60
+ const rnd = mulberry32(hashStr(key) ^ 0x9e3779b9);
61
+ const tfMs = sec * 1000;
62
+ const t0 = Math.floor(Date.now() / tfMs) * tfMs - (n - 1) * tfMs;
63
+ let price = base;
64
+ let drift = 0.0002;
65
+ let vol = 0.011;
66
+ let regimeLeft = 0;
67
+ const bars = [];
68
+ for (let i = 0; i < n; i++) {
69
+ if (regimeLeft <= 0) {
70
+ regimeLeft = (40 + rnd() * 140) | 0;
71
+ drift = (rnd() - 0.48) * 0.0016;
72
+ vol = 0.005 + rnd() * 0.02;
73
+ }
74
+ regimeLeft--;
75
+ const open = price;
76
+ const revert = -0.004 * Math.log(price / base);
77
+ const ret = drift + revert + vol * gauss(rnd);
78
+ const close = open * Math.exp(ret);
79
+ const high = Math.max(open, close) * (1 + Math.abs(gauss(rnd)) * vol * 0.6);
80
+ const low = Math.min(open, close) * (1 - Math.abs(gauss(rnd)) * vol * 0.6);
81
+ const volume = Math.max(1, Math.round(420 * (1 + (Math.abs(ret) / vol) * 2 + rnd() * 0.6)));
82
+ bars.push({ time: t0 + i * tfMs, open, high, low, close, volume });
83
+ price = close;
84
+ }
85
+ return bars;
86
+ }
87
+
88
+ /**
89
+ * Stateful synthetic live stream: mutates the current bar each tick and
90
+ * rolls over on timeframe boundaries. Bridges from `startPrice` when given.
91
+ * @param {number} sec timeframe in seconds
92
+ * @param {number} [startPrice] bridge continuity from the last known price
93
+ */
94
+ export function makeSynthStream(sec, startPrice) {
95
+ const tfMs = sec * 1000;
96
+ const rnd = mulberry32((Math.random() * 1e9) >>> 0);
97
+ let cur = null;
98
+ return () => {
99
+ const t = Math.floor(Date.now() / tfMs) * tfMs;
100
+ if (!cur || cur.time !== t) {
101
+ const open = cur ? cur.close : (startPrice || 100) * (1 + gauss(rnd) * 0.002);
102
+ cur = { time: t, open, high: open, low: open, close: open, volume: 0 };
103
+ } else {
104
+ const vol = 0.004;
105
+ cur.close = Math.max(1e-8, cur.close * Math.exp(vol * gauss(rnd) * 0.35));
106
+ cur.high = Math.max(cur.high, cur.close);
107
+ cur.low = Math.min(cur.low, cur.close);
108
+ cur.volume += Math.round(20 + rnd() * 60);
109
+ }
110
+ return { ...cur };
111
+ };
112
+ }
113
+
114
+ /* ---------------- Binance public API ---------------- */
115
+
116
+ /**
117
+ * Fetch klines from Binance's public REST API.
118
+ * @param {string} symbol e.g. 'BTCUSDT'
119
+ * @param {string} tfId interval id ('15m','1h','1d'…)
120
+ * @param {number} [limit=500]
121
+ * @param {number} [endTime] fetch bars older than this (ms) — for backfill
122
+ */
123
+ export async function fetchBinanceKlines(symbol, tfId, limit = 500, endTime) {
124
+ let url =
125
+ `https://api.binance.com/api/v3/klines?symbol=${encodeURIComponent(symbol)}` +
126
+ `&interval=${encodeURIComponent(tfId)}&limit=${limit}`;
127
+ if (endTime) url += `&endTime=${endTime - 1}`;
128
+ const res = await fetch(url);
129
+ if (!res.ok) throw new Error(`Binance HTTP ${res.status}`);
130
+ const rows = await res.json();
131
+ return rows.map((k) => ({
132
+ time: k[0],
133
+ open: +k[1],
134
+ high: +k[2],
135
+ low: +k[3],
136
+ close: +k[4],
137
+ volume: +k[5],
138
+ }));
139
+ }
140
+
141
+ /**
142
+ * Open a Binance kline WebSocket. `onDown(err)` fires on error/close/timeout
143
+ * (after which the socket is dead and the caller should fall back).
144
+ * @returns {{close(): void}}
145
+ */
146
+ export function openBinanceSocket(symbol, tfId, onBar, onDown, timeoutMs = 8000) {
147
+ let ws;
148
+ try {
149
+ ws = new WebSocket(
150
+ `wss://stream.binance.com:9443/ws/${symbol.toLowerCase()}@kline_${tfId}`
151
+ );
152
+ } catch (err) {
153
+ onDown(err);
154
+ return { close() {} };
155
+ }
156
+ let dead = false;
157
+ const failTimer = setTimeout(() => {
158
+ if (!dead && ws.readyState !== WebSocket.OPEN) {
159
+ dead = true;
160
+ try {
161
+ ws.close();
162
+ } catch (_) {}
163
+ onDown(new Error('timeout'));
164
+ }
165
+ }, timeoutMs);
166
+ ws.onopen = () => clearTimeout(failTimer);
167
+ ws.onmessage = (ev) => {
168
+ try {
169
+ const k = JSON.parse(ev.data).k;
170
+ if (!k) return;
171
+ onBar({ time: k.t, open: +k.o, high: +k.h, low: +k.l, close: +k.c, volume: +k.v });
172
+ } catch (_) {}
173
+ };
174
+ ws.onclose = () => {
175
+ if (dead) return;
176
+ dead = true;
177
+ clearTimeout(failTimer);
178
+ onDown(new Error('closed'));
179
+ };
180
+ ws.onerror = () => {};
181
+ return {
182
+ close() {
183
+ dead = true;
184
+ ws.onclose = null;
185
+ ws.onerror = null;
186
+ clearTimeout(failTimer);
187
+ try {
188
+ ws.close();
189
+ } catch (_) {}
190
+ },
191
+ };
192
+ }