bandkit 1.0.1 → 1.0.3

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.
Files changed (66) hide show
  1. package/README.md +222 -77
  2. package/dist/BandPanel.d.ts.map +1 -1
  3. package/dist/BandPanel.js +23 -2
  4. package/dist/BandPanel.js.map +1 -1
  5. package/dist/BotDecisionPanel.d.ts +7 -0
  6. package/dist/BotDecisionPanel.d.ts.map +1 -0
  7. package/dist/BotDecisionPanel.js +124 -0
  8. package/dist/BotDecisionPanel.js.map +1 -0
  9. package/dist/ContractEventFeed.d.ts +7 -0
  10. package/dist/ContractEventFeed.d.ts.map +1 -0
  11. package/dist/ContractEventFeed.js +76 -0
  12. package/dist/ContractEventFeed.js.map +1 -0
  13. package/dist/GasBadge.d.ts +6 -0
  14. package/dist/GasBadge.d.ts.map +1 -0
  15. package/dist/GasBadge.js +37 -0
  16. package/dist/GasBadge.js.map +1 -0
  17. package/dist/OrderBookWidget.d.ts +8 -0
  18. package/dist/OrderBookWidget.d.ts.map +1 -0
  19. package/dist/OrderBookWidget.js +92 -0
  20. package/dist/OrderBookWidget.js.map +1 -0
  21. package/dist/RegimeBadge.d.ts +6 -0
  22. package/dist/RegimeBadge.d.ts.map +1 -0
  23. package/dist/RegimeBadge.js +39 -0
  24. package/dist/RegimeBadge.js.map +1 -0
  25. package/dist/TradeTape.d.ts +8 -0
  26. package/dist/TradeTape.d.ts.map +1 -0
  27. package/dist/TradeTape.js +67 -0
  28. package/dist/TradeTape.js.map +1 -0
  29. package/dist/decisionEngine.d.ts +41 -0
  30. package/dist/decisionEngine.d.ts.map +1 -0
  31. package/dist/decisionEngine.js +166 -0
  32. package/dist/decisionEngine.js.map +1 -0
  33. package/dist/index.d.ts +14 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +18 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/useBinanceOrderBook.d.ts +27 -0
  38. package/dist/useBinanceOrderBook.d.ts.map +1 -0
  39. package/dist/useBinanceOrderBook.js +95 -0
  40. package/dist/useBinanceOrderBook.js.map +1 -0
  41. package/dist/useBinanceTrades.d.ts +23 -0
  42. package/dist/useBinanceTrades.d.ts.map +1 -0
  43. package/dist/useBinanceTrades.js +77 -0
  44. package/dist/useBinanceTrades.js.map +1 -0
  45. package/dist/useBotDecision.d.ts +32 -0
  46. package/dist/useBotDecision.d.ts.map +1 -0
  47. package/dist/useBotDecision.js +104 -0
  48. package/dist/useBotDecision.js.map +1 -0
  49. package/dist/useCoinbaseTicker.d.ts +28 -0
  50. package/dist/useCoinbaseTicker.d.ts.map +1 -0
  51. package/dist/useCoinbaseTicker.js +87 -0
  52. package/dist/useCoinbaseTicker.js.map +1 -0
  53. package/dist/useGasPrice.d.ts +18 -0
  54. package/dist/useGasPrice.d.ts.map +1 -0
  55. package/dist/useGasPrice.js +44 -0
  56. package/dist/useGasPrice.js.map +1 -0
  57. package/dist/useStrategyContractEvents.d.ts +23 -0
  58. package/dist/useStrategyContractEvents.d.ts.map +1 -0
  59. package/dist/useStrategyContractEvents.js +79 -0
  60. package/dist/useStrategyContractEvents.js.map +1 -0
  61. package/dist/useVolatilityRegime.d.ts +17 -0
  62. package/dist/useVolatilityRegime.d.ts.map +1 -0
  63. package/dist/useVolatilityRegime.js +48 -0
  64. package/dist/useVolatilityRegime.js.map +1 -0
  65. package/package.json +4 -2
  66. package/scripts/test-decision-stream.cjs +417 -0
@@ -0,0 +1,417 @@
1
+ /* eslint-disable */
2
+ //
3
+ // test-decision-stream.cjs
4
+ //
5
+ // Standalone Node script that streams live Binance ETH/USDT order book + trades
6
+ // and Coinbase ETH-USD ticker, and runs the same decision logic as
7
+ // src/decisionEngine.ts. Prints live decisions; after the run window,
8
+ // prints a summary tally and a diagnostic signal-vs-forward-price comparison.
9
+ //
10
+ // IMPORTANT — what this proves and does NOT prove:
11
+ // - It DOES verify the decision logic responds to live data without crashing.
12
+ // - It DOES show the cadence and balance of signals.
13
+ // - It DOES report a crude correlation between BUY/SELL signals and the
14
+ // next minute's price change.
15
+ // - It DOES NOT prove the strategy is profitable.
16
+ // Real validation needs months of historical data, realistic slippage and
17
+ // gas modelling, out-of-sample windows, and statistical significance tests.
18
+ //
19
+ // Usage:
20
+ // node scripts/test-decision-stream.cjs [durationMinutes]
21
+ // default duration: 7 minutes
22
+ //
23
+
24
+ const WebSocket = require("ws");
25
+
26
+ const DURATION_MIN = Number(process.argv[2] ?? 7);
27
+ const BINANCE_DEPTH_URL = "wss://stream.binance.com:9443/ws/ethusdt@depth20@100ms";
28
+ const BINANCE_TRADE_URL = "wss://stream.binance.com:9443/ws/ethusdt@trade";
29
+ const COINBASE_URL = "wss://ws-feed.exchange.coinbase.com";
30
+
31
+ // ---- State ----------------------------------------------------------------
32
+ const state = {
33
+ binanceBook: { bids: [], asks: [], midPrice: null, spreadPct: null, ts: 0 },
34
+ coinbaseMid: null,
35
+ recentTrades: [], // { price, size, side, ts }
36
+ priceTicks: [], // for volatility regime
37
+ decisions: [], // { at, decision, confidence, midPrice, reasons }
38
+ startedAt: Date.now()
39
+ };
40
+
41
+ const PRICE_TICK_WINDOW = 60;
42
+ const FLOW_WINDOW_MS = 60_000;
43
+ const EVAL_INTERVAL_MS = 2000;
44
+ const FORWARD_PRICE_DELAY_MS = 60_000;
45
+
46
+ // ---- Decision logic (mirrors src/decisionEngine.ts) ----------------------
47
+ function computeBookImbalance(bids, asks, mid, withinPct = 0.005) {
48
+ const lo = mid * (1 - withinPct);
49
+ const hi = mid * (1 + withinPct);
50
+ let bidSum = 0;
51
+ let askSum = 0;
52
+ for (const b of bids) if (b.price >= lo) bidSum += b.size;
53
+ for (const a of asks) if (a.price <= hi) askSum += a.size;
54
+ const total = bidSum + askSum;
55
+ return total > 0 ? (bidSum - askSum) / total : 0;
56
+ }
57
+
58
+ function computeBookDepth(bids, asks, mid, withinPct = 0.005) {
59
+ const lo = mid * (1 - withinPct);
60
+ const hi = mid * (1 + withinPct);
61
+ let total = 0;
62
+ for (const b of bids) if (b.price >= lo) total += b.size;
63
+ for (const a of asks) if (a.price <= hi) total += a.size;
64
+ return total;
65
+ }
66
+
67
+ function computeTradeImbalance(trades, windowMs, nowMs) {
68
+ const cutoff = nowMs - windowMs;
69
+ let buyVol = 0;
70
+ let sellVol = 0;
71
+ for (const t of trades) {
72
+ if (t.ts < cutoff) continue;
73
+ if (t.side === "buy") buyVol += t.size;
74
+ else sellVol += t.size;
75
+ }
76
+ const total = buyVol + sellVol;
77
+ return total > 0 ? (buyVol - sellVol) / total : 0;
78
+ }
79
+
80
+ function classifyRegime(prices) {
81
+ if (prices.length < Math.max(8, Math.floor(PRICE_TICK_WINDOW / 4))) {
82
+ return { regime: "UNKNOWN", stddevPct: null, driftPct: null };
83
+ }
84
+ const mean = prices.reduce((s, v) => s + v, 0) / prices.length;
85
+ const variance = prices.reduce((s, v) => s + (v - mean) ** 2, 0) / prices.length;
86
+ const stddev = Math.sqrt(variance);
87
+ const stddevPct = mean > 0 ? (stddev / mean) * 100 : null;
88
+ const drift = prices[prices.length - 1] - prices[0];
89
+ const driftPct = mean > 0 ? (drift / mean) * 100 : null;
90
+ const absDriftPct = driftPct === null ? null : Math.abs(driftPct);
91
+
92
+ let regime = "MIXED";
93
+ if (stddevPct !== null && absDriftPct !== null) {
94
+ if (absDriftPct >= 0.45) regime = "TRENDING";
95
+ else if (stddevPct <= 0.15 && absDriftPct < 0.45) regime = "RANGING";
96
+ }
97
+ return { regime, stddevPct, driftPct };
98
+ }
99
+
100
+ function computeDecision(inputs) {
101
+ const reasons = [];
102
+ if (inputs.regime === "UNKNOWN") {
103
+ return { decision: "WAIT", confidence: 0, reasons: ["Warming up"] };
104
+ }
105
+ if (inputs.regime === "TRENDING") {
106
+ return {
107
+ decision: "STAND DOWN",
108
+ confidence: 90,
109
+ reasons: ["TRENDING regime — bot sits out"]
110
+ };
111
+ }
112
+
113
+ let scoreBuy = 0;
114
+ let scoreSell = 0;
115
+ let confidence = inputs.regime === "RANGING" ? 45 : 30;
116
+ reasons.push(`Regime: ${inputs.regime}`);
117
+
118
+ if (inputs.bookImbalance >= 0.15) {
119
+ scoreBuy += 25;
120
+ reasons.push(`Book +${(inputs.bookImbalance * 100).toFixed(0)}% bid`);
121
+ } else if (inputs.bookImbalance <= -0.15) {
122
+ scoreSell += 25;
123
+ reasons.push(`Book ${(inputs.bookImbalance * 100).toFixed(0)}% ask`);
124
+ } else {
125
+ reasons.push(`Book ${(inputs.bookImbalance * 100).toFixed(0)}%`);
126
+ }
127
+
128
+ if (inputs.tradeImbalance >= 0.10) {
129
+ scoreBuy += 20;
130
+ reasons.push(`Flow ${((0.5 + inputs.tradeImbalance / 2) * 100).toFixed(0)}% buys`);
131
+ } else if (inputs.tradeImbalance <= -0.10) {
132
+ scoreSell += 20;
133
+ reasons.push(`Flow ${((0.5 - inputs.tradeImbalance / 2) * 100).toFixed(0)}% sells`);
134
+ } else {
135
+ reasons.push("Flow balanced");
136
+ }
137
+
138
+ if (inputs.spreadPct > 0.05) {
139
+ confidence -= 10;
140
+ reasons.push(`Spread wide ${inputs.spreadPct.toFixed(3)}%`);
141
+ } else {
142
+ confidence += 5;
143
+ }
144
+
145
+ if (inputs.venueDivergenceBps !== null && Number.isFinite(inputs.venueDivergenceBps)) {
146
+ if (inputs.venueDivergenceBps > 5) {
147
+ scoreBuy += 12;
148
+ reasons.push(`BIN +${inputs.venueDivergenceBps.toFixed(1)}bps`);
149
+ } else if (inputs.venueDivergenceBps < -5) {
150
+ scoreSell += 12;
151
+ reasons.push(`BIN ${inputs.venueDivergenceBps.toFixed(1)}bps`);
152
+ }
153
+ }
154
+
155
+ if (inputs.bookDepth < 50) {
156
+ confidence -= 15;
157
+ reasons.push(`Thin book (${inputs.bookDepth.toFixed(0)} ETH)`);
158
+ } else {
159
+ confidence += 5;
160
+ }
161
+
162
+ let decision = "WAIT";
163
+ if (scoreBuy > scoreSell + 12) {
164
+ decision = "BUY";
165
+ confidence = Math.min(100, Math.max(0, confidence + scoreBuy));
166
+ } else if (scoreSell > scoreBuy + 12) {
167
+ decision = "SELL";
168
+ confidence = Math.min(100, Math.max(0, confidence + scoreSell));
169
+ } else {
170
+ confidence = Math.min(100, Math.max(0, confidence + Math.max(scoreBuy, scoreSell) / 2));
171
+ }
172
+
173
+ return { decision, confidence: Math.round(confidence), reasons };
174
+ }
175
+
176
+ // ---- WebSocket setup -----------------------------------------------------
177
+ function connectBinanceDepth() {
178
+ const ws = new WebSocket(BINANCE_DEPTH_URL);
179
+ ws.on("message", (raw) => {
180
+ try {
181
+ const msg = JSON.parse(raw.toString());
182
+ const bids = (msg.bids || [])
183
+ .map(([p, q]) => ({ price: Number(p), size: Number(q) }))
184
+ .filter((l) => l.size > 0);
185
+ const asks = (msg.asks || [])
186
+ .map(([p, q]) => ({ price: Number(p), size: Number(q) }))
187
+ .filter((l) => l.size > 0);
188
+ const bestBid = bids[0]?.price ?? null;
189
+ const bestAsk = asks[0]?.price ?? null;
190
+ if (bestBid && bestAsk) {
191
+ const mid = (bestBid + bestAsk) / 2;
192
+ state.binanceBook = {
193
+ bids,
194
+ asks,
195
+ midPrice: mid,
196
+ spreadPct: ((bestAsk - bestBid) / mid) * 100,
197
+ ts: Date.now()
198
+ };
199
+ // Sample price tick (rate-limited)
200
+ if (
201
+ state.priceTicks.length === 0 ||
202
+ Date.now() - (state.priceTicks._lastSampleAt || 0) > 1000
203
+ ) {
204
+ state.priceTicks.push(mid);
205
+ if (state.priceTicks.length > PRICE_TICK_WINDOW) state.priceTicks.shift();
206
+ state.priceTicks._lastSampleAt = Date.now();
207
+ }
208
+ }
209
+ } catch (err) {
210
+ /* swallow parse */
211
+ }
212
+ });
213
+ ws.on("error", () => {});
214
+ ws.on("close", () => setTimeout(connectBinanceDepth, 3000));
215
+ return ws;
216
+ }
217
+
218
+ function connectBinanceTrades() {
219
+ const ws = new WebSocket(BINANCE_TRADE_URL);
220
+ ws.on("message", (raw) => {
221
+ try {
222
+ const msg = JSON.parse(raw.toString());
223
+ const trade = {
224
+ price: Number(msg.p),
225
+ size: Number(msg.q),
226
+ side: msg.m ? "sell" : "buy",
227
+ ts: Number(msg.T)
228
+ };
229
+ if (Number.isFinite(trade.price) && Number.isFinite(trade.size)) {
230
+ state.recentTrades.push(trade);
231
+ // Prune
232
+ const cutoff = Date.now() - FLOW_WINDOW_MS * 2;
233
+ while (state.recentTrades.length > 0 && state.recentTrades[0].ts < cutoff) {
234
+ state.recentTrades.shift();
235
+ }
236
+ }
237
+ } catch (err) {
238
+ /* swallow */
239
+ }
240
+ });
241
+ ws.on("error", () => {});
242
+ ws.on("close", () => setTimeout(connectBinanceTrades, 3000));
243
+ return ws;
244
+ }
245
+
246
+ function connectCoinbase() {
247
+ const ws = new WebSocket(COINBASE_URL);
248
+ ws.on("open", () => {
249
+ ws.send(
250
+ JSON.stringify({
251
+ type: "subscribe",
252
+ product_ids: ["ETH-USD"],
253
+ channels: ["ticker"]
254
+ })
255
+ );
256
+ });
257
+ ws.on("message", (raw) => {
258
+ try {
259
+ const msg = JSON.parse(raw.toString());
260
+ if (msg.type === "ticker" && msg.product_id === "ETH-USD") {
261
+ const bid = Number(msg.best_bid);
262
+ const ask = Number(msg.best_ask);
263
+ if (Number.isFinite(bid) && Number.isFinite(ask)) {
264
+ state.coinbaseMid = (bid + ask) / 2;
265
+ }
266
+ }
267
+ } catch (err) {
268
+ /* swallow */
269
+ }
270
+ });
271
+ ws.on("error", () => {});
272
+ ws.on("close", () => setTimeout(connectCoinbase, 3000));
273
+ return ws;
274
+ }
275
+
276
+ // ---- Main loop -----------------------------------------------------------
277
+ function pad(s, n) {
278
+ s = String(s);
279
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
280
+ }
281
+
282
+ function evaluate() {
283
+ const mid = state.binanceBook.midPrice;
284
+ if (!mid) return;
285
+
286
+ const bookImbalance = computeBookImbalance(state.binanceBook.bids, state.binanceBook.asks, mid);
287
+ const bookDepth = computeBookDepth(state.binanceBook.bids, state.binanceBook.asks, mid);
288
+ const tradeImbalance = computeTradeImbalance(state.recentTrades, FLOW_WINDOW_MS, Date.now());
289
+ const { regime, stddevPct, driftPct } = classifyRegime(state.priceTicks);
290
+ const venueDivergenceBps =
291
+ state.coinbaseMid && state.coinbaseMid > 0
292
+ ? ((mid - state.coinbaseMid) / state.coinbaseMid) * 10000
293
+ : null;
294
+
295
+ const result = computeDecision({
296
+ regime,
297
+ bookImbalance,
298
+ tradeImbalance,
299
+ spreadPct: state.binanceBook.spreadPct ?? 0,
300
+ venueDivergenceBps,
301
+ bookDepth
302
+ });
303
+
304
+ const entry = {
305
+ at: Date.now(),
306
+ decision: result.decision,
307
+ confidence: result.confidence,
308
+ midPrice: mid,
309
+ reasons: result.reasons,
310
+ regime,
311
+ bookImbalance,
312
+ tradeImbalance,
313
+ venueDivergenceBps,
314
+ stddevPct,
315
+ driftPct
316
+ };
317
+ state.decisions.push(entry);
318
+
319
+ const elapsedSec = ((Date.now() - state.startedAt) / 1000).toFixed(0);
320
+ console.log(
321
+ `[t+${pad(elapsedSec, 4)}s] ${pad(result.decision, 11)} conf=${pad(result.confidence, 3)}` +
322
+ ` mid=${mid.toFixed(2)} regime=${pad(regime, 8)}` +
323
+ ` book=${pad((bookImbalance * 100).toFixed(0) + "%", 5)}` +
324
+ ` flow=${pad((tradeImbalance * 100).toFixed(0) + "%", 5)}` +
325
+ ` div=${venueDivergenceBps === null ? " — " : (venueDivergenceBps.toFixed(1) + "bps")}` +
326
+ ` ${result.reasons.slice(0, 3).join(", ")}`
327
+ );
328
+ }
329
+
330
+ function printSummary() {
331
+ console.log("\n" + "=".repeat(80));
332
+ console.log(`SUMMARY (duration ${DURATION_MIN} min, ${state.decisions.length} evaluations)`);
333
+ console.log("=".repeat(80));
334
+
335
+ const tally = { BUY: 0, SELL: 0, WAIT: 0, "STAND DOWN": 0 };
336
+ const regimeTally = { RANGING: 0, MIXED: 0, TRENDING: 0, UNKNOWN: 0 };
337
+ for (const d of state.decisions) {
338
+ tally[d.decision]++;
339
+ regimeTally[d.regime]++;
340
+ }
341
+
342
+ console.log("\nDecision tally:");
343
+ for (const [k, v] of Object.entries(tally)) {
344
+ const pct = state.decisions.length > 0 ? ((v / state.decisions.length) * 100).toFixed(1) : "0.0";
345
+ console.log(` ${pad(k, 12)} ${pad(v, 5)} (${pct}%)`);
346
+ }
347
+
348
+ console.log("\nRegime time-in-state:");
349
+ for (const [k, v] of Object.entries(regimeTally)) {
350
+ const pct = state.decisions.length > 0 ? ((v / state.decisions.length) * 100).toFixed(1) : "0.0";
351
+ console.log(` ${pad(k, 12)} ${pad(v, 5)} (${pct}%)`);
352
+ }
353
+
354
+ // Forward-price diagnostic: for each BUY/SELL signal, look at the price
355
+ // ~60s later and tally whether it moved in the signal's favor.
356
+ const directional = state.decisions.filter((d) => d.decision === "BUY" || d.decision === "SELL");
357
+ let evaluated = 0;
358
+ let correct = 0;
359
+ let totalForwardBps = 0;
360
+ for (const d of directional) {
361
+ const targetTs = d.at + FORWARD_PRICE_DELAY_MS;
362
+ // find first decision at or after target
363
+ const futureIdx = state.decisions.findIndex((x) => x.at >= targetTs);
364
+ if (futureIdx === -1) continue;
365
+ const futurePrice = state.decisions[futureIdx].midPrice;
366
+ const moveBps = ((futurePrice - d.midPrice) / d.midPrice) * 10000;
367
+ evaluated++;
368
+ const signedMove = d.decision === "BUY" ? moveBps : -moveBps;
369
+ totalForwardBps += signedMove;
370
+ if (signedMove > 0) correct++;
371
+ }
372
+ console.log("\nDirectional diagnostic (60s-forward, NOT a profitability test):");
373
+ if (evaluated === 0) {
374
+ console.log(" no eligible directional signals with forward window");
375
+ } else {
376
+ const hitRate = ((correct / evaluated) * 100).toFixed(1);
377
+ const avgSignedBps = (totalForwardBps / evaluated).toFixed(2);
378
+ console.log(` signals scored: ${evaluated}`);
379
+ console.log(` moved in our favour: ${correct} (${hitRate}%)`);
380
+ console.log(` avg signed move: ${avgSignedBps} bps`);
381
+ console.log(
382
+ ` ↳ baseline ~50% means no edge; small samples (<50) are noise; gas + slippage`
383
+ );
384
+ console.log(` would eat anything below ~10 bps on Uniswap mainnet.`);
385
+ }
386
+
387
+ console.log("\nReminders:");
388
+ console.log(" - This verifies the logic responds. It does NOT prove profitability.");
389
+ console.log(" - Binance signals ≠ Uniswap fills. Cross-venue + AMM curves differ.");
390
+ console.log(" - Real validation needs months of historical data + walk-forward.\n");
391
+ }
392
+
393
+ // ---- Run ------------------------------------------------------------------
394
+ console.log(`Starting decision stream test for ${DURATION_MIN} minutes…`);
395
+ console.log(`Streams: Binance depth + trade (ETHUSDT), Coinbase ticker (ETH-USD)`);
396
+ console.log(`Evaluating every ${EVAL_INTERVAL_MS}ms. Press Ctrl-C to abort.\n`);
397
+
398
+ connectBinanceDepth();
399
+ connectBinanceTrades();
400
+ connectCoinbase();
401
+
402
+ const evalTimer = setInterval(evaluate, EVAL_INTERVAL_MS);
403
+ const stopAt = Date.now() + DURATION_MIN * 60_000;
404
+ const stopTimer = setInterval(() => {
405
+ if (Date.now() >= stopAt) {
406
+ clearInterval(evalTimer);
407
+ clearInterval(stopTimer);
408
+ printSummary();
409
+ process.exit(0);
410
+ }
411
+ }, 1000);
412
+
413
+ process.on("SIGINT", () => {
414
+ console.log("\n\nAborted by user. Printing partial summary…");
415
+ printSummary();
416
+ process.exit(0);
417
+ });