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.
- package/README.md +222 -77
- package/dist/BandPanel.d.ts.map +1 -1
- package/dist/BandPanel.js +23 -2
- package/dist/BandPanel.js.map +1 -1
- package/dist/BotDecisionPanel.d.ts +7 -0
- package/dist/BotDecisionPanel.d.ts.map +1 -0
- package/dist/BotDecisionPanel.js +124 -0
- package/dist/BotDecisionPanel.js.map +1 -0
- package/dist/ContractEventFeed.d.ts +7 -0
- package/dist/ContractEventFeed.d.ts.map +1 -0
- package/dist/ContractEventFeed.js +76 -0
- package/dist/ContractEventFeed.js.map +1 -0
- package/dist/GasBadge.d.ts +6 -0
- package/dist/GasBadge.d.ts.map +1 -0
- package/dist/GasBadge.js +37 -0
- package/dist/GasBadge.js.map +1 -0
- package/dist/OrderBookWidget.d.ts +8 -0
- package/dist/OrderBookWidget.d.ts.map +1 -0
- package/dist/OrderBookWidget.js +92 -0
- package/dist/OrderBookWidget.js.map +1 -0
- package/dist/RegimeBadge.d.ts +6 -0
- package/dist/RegimeBadge.d.ts.map +1 -0
- package/dist/RegimeBadge.js +39 -0
- package/dist/RegimeBadge.js.map +1 -0
- package/dist/TradeTape.d.ts +8 -0
- package/dist/TradeTape.d.ts.map +1 -0
- package/dist/TradeTape.js +67 -0
- package/dist/TradeTape.js.map +1 -0
- package/dist/decisionEngine.d.ts +41 -0
- package/dist/decisionEngine.d.ts.map +1 -0
- package/dist/decisionEngine.js +166 -0
- package/dist/decisionEngine.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/useBinanceOrderBook.d.ts +27 -0
- package/dist/useBinanceOrderBook.d.ts.map +1 -0
- package/dist/useBinanceOrderBook.js +95 -0
- package/dist/useBinanceOrderBook.js.map +1 -0
- package/dist/useBinanceTrades.d.ts +23 -0
- package/dist/useBinanceTrades.d.ts.map +1 -0
- package/dist/useBinanceTrades.js +77 -0
- package/dist/useBinanceTrades.js.map +1 -0
- package/dist/useBotDecision.d.ts +32 -0
- package/dist/useBotDecision.d.ts.map +1 -0
- package/dist/useBotDecision.js +104 -0
- package/dist/useBotDecision.js.map +1 -0
- package/dist/useCoinbaseTicker.d.ts +28 -0
- package/dist/useCoinbaseTicker.d.ts.map +1 -0
- package/dist/useCoinbaseTicker.js +87 -0
- package/dist/useCoinbaseTicker.js.map +1 -0
- package/dist/useGasPrice.d.ts +18 -0
- package/dist/useGasPrice.d.ts.map +1 -0
- package/dist/useGasPrice.js +44 -0
- package/dist/useGasPrice.js.map +1 -0
- package/dist/useStrategyContractEvents.d.ts +23 -0
- package/dist/useStrategyContractEvents.d.ts.map +1 -0
- package/dist/useStrategyContractEvents.js +79 -0
- package/dist/useStrategyContractEvents.js.map +1 -0
- package/dist/useVolatilityRegime.d.ts +17 -0
- package/dist/useVolatilityRegime.d.ts.map +1 -0
- package/dist/useVolatilityRegime.js +48 -0
- package/dist/useVolatilityRegime.js.map +1 -0
- package/package.json +4 -2
- 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
|
+
});
|