@tradejs/node 1.0.8 → 1.0.10

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/dist/backtest.js CHANGED
@@ -31,19 +31,25 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  // src/backtest.ts
32
32
  var backtest_exports = {};
33
33
  __export(backtest_exports, {
34
+ canRunTestsInSharedCandleLoop: () => canRunTestsInSharedCandleLoop,
34
35
  createTestConnector: () => createTestConnector,
36
+ releaseTestingSymbolCache: () => releaseTestingSymbolCache,
35
37
  resetTestingKlineCache: () => resetTestingKlineCache,
36
- testing: () => testing
38
+ resolveExecutionCosts: () => resolveExecutionCosts,
39
+ testing: () => testing,
40
+ testingGroupInSharedCandleLoop: () => testingGroupInSharedCandleLoop
37
41
  });
38
42
  module.exports = __toCommonJS(backtest_exports);
39
43
  __reExport(backtest_exports, require("@tradejs/core/backtest"), module.exports);
40
44
 
41
45
  // src/testing.ts
42
46
  var import_indicators3 = require("@tradejs/core/indicators");
47
+ var import_constants4 = require("@tradejs/core/constants");
48
+ var import_strategies4 = require("@tradejs/core/strategies");
43
49
  var import_time = require("@tradejs/core/time");
44
50
  var import_ai2 = require("@tradejs/infra/ai");
45
51
  var import_ml2 = require("@tradejs/infra/ml");
46
- var import_logger5 = require("@tradejs/infra/logger");
52
+ var import_logger7 = require("@tradejs/infra/logger");
47
53
 
48
54
  // src/ai.ts
49
55
  var import_aiLanguages = require("@tradejs/infra/aiLanguages");
@@ -52,6 +58,10 @@ var import_userSettings = require("@tradejs/infra/userSettings");
52
58
 
53
59
  // src/aiShared.ts
54
60
  var MAX_AI_SERIES_POINTS = 5;
61
+ var COMPACT_INDICATORS_SNAPSHOT_SYMBOL = /* @__PURE__ */ Symbol.for(
62
+ "tradejs.indicators.compactSnapshot"
63
+ );
64
+ var COMPACT_INDICATORS_SNAPSHOT_KEY = "__tradejsCompactIndicatorsSnapshot";
55
65
  var trimSeriesDeep = (value) => {
56
66
  if (Array.isArray(value)) {
57
67
  const trimmed = value.slice(-MAX_AI_SERIES_POINTS);
@@ -73,13 +83,15 @@ var trimSeriesDeep = (value) => {
73
83
  }
74
84
  return value;
75
85
  };
86
+ var buildCompactAiIndicatorsSnapshot = (value) => {
87
+ const compactSnapshot = value && typeof value === "object" ? value[COMPACT_INDICATORS_SNAPSHOT_SYMBOL] ?? value[COMPACT_INDICATORS_SNAPSHOT_KEY] : void 0;
88
+ if (typeof compactSnapshot === "function") {
89
+ return compactSnapshot({ limit: MAX_AI_SERIES_POINTS });
90
+ }
91
+ return trimSeriesDeep(value);
92
+ };
76
93
 
77
94
  // src/aiMarketContext.ts
78
- var SESSION_WINDOWS = [
79
- { name: "asia", startMinuteUtc: 0, endMinuteUtc: 8 * 60 },
80
- { name: "europe", startMinuteUtc: 7 * 60, endMinuteUtc: 16 * 60 },
81
- { name: "us", startMinuteUtc: 13 * 60, endMinuteUtc: 22 * 60 }
82
- ];
83
95
  var toRecord = (value) => {
84
96
  if (!value || typeof value !== "object" || Array.isArray(value)) {
85
97
  return null;
@@ -90,92 +102,485 @@ var toFiniteNumber = (value) => {
90
102
  const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
91
103
  return Number.isFinite(numeric) ? numeric : null;
92
104
  };
93
- var getLastFiniteNumber = (value) => {
94
- const numeric = toFiniteNumber(value);
95
- if (numeric != null) {
96
- return numeric;
97
- }
98
- if (!Array.isArray(value)) {
99
- return null;
100
- }
101
- for (let i = value.length - 1; i >= 0; i -= 1) {
102
- const nested = getLastFiniteNumber(value[i]);
103
- if (nested != null) {
104
- return nested;
105
- }
106
- }
107
- return null;
108
- };
109
105
  var roundTo = (value, decimals) => {
110
106
  const factor = 10 ** decimals;
111
107
  return Math.round(value * factor) / factor;
112
108
  };
113
- var isInsideSession = (minuteUtc, startMinuteUtc, endMinuteUtc) => startMinuteUtc <= endMinuteUtc ? minuteUtc >= startMinuteUtc && minuteUtc < endMinuteUtc : minuteUtc >= startMinuteUtc || minuteUtc < endMinuteUtc;
114
- var buildTradingSessionContext = (timestamp) => {
115
- const date = new Date(timestamp);
116
- const utcHour = date.getUTCHours();
117
- const utcMinute = date.getUTCMinutes();
118
- const minuteUtc = utcHour * 60 + utcMinute;
119
- const activeSessions = SESSION_WINDOWS.filter(
120
- (session) => isInsideSession(minuteUtc, session.startMinuteUtc, session.endMinuteUtc)
121
- ).map((session) => session.name);
122
- const primarySession = activeSessions.includes("us") ? "us" : activeSessions.includes("europe") ? "europe" : activeSessions.includes("asia") ? "asia" : "off_hours";
123
- return {
124
- timezone: "UTC",
125
- utcHour,
126
- utcMinute,
127
- primarySession,
128
- activeSessions,
129
- isOverlap: activeSessions.length > 1,
130
- overlap: activeSessions.length > 1 ? `${activeSessions.join("_")}_overlap` : null
131
- };
132
- };
133
109
  var buildMissingSpreadContext = () => ({
134
- source: "binance_coinbase_btc",
135
- indicatorKey: "payload.indicators.spread",
110
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
111
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
136
112
  available: false,
137
113
  value: null,
114
+ zScore: null,
138
115
  bps: null,
139
116
  absBps: null,
140
117
  bias: null,
141
118
  severity: null
142
119
  });
143
- var buildSpreadContextFromValue = (spread) => {
120
+ var buildSpreadContextFromSignal = (signal) => {
121
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
122
+ const relative = toRecord(baseContext?.relative);
123
+ const execution = toRecord(relative?.execution);
124
+ const spread = toFiniteNumber(execution?.venueSpread);
125
+ const zScore = toFiniteNumber(execution?.venueSpreadZScore);
126
+ if (spread == null) {
127
+ return buildMissingSpreadContext();
128
+ }
144
129
  const value = roundTo(spread, 8);
145
130
  const bps = roundTo(value * 1e4, 2);
146
131
  const absBps = Math.abs(bps);
147
- const bias = Math.abs(bps) < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
132
+ const bias = absBps < 1 ? "flat" : bps > 0 ? "coinbase_premium" : "binance_premium";
148
133
  const severity = absBps >= 20 ? "wide" : absBps >= 5 ? "elevated" : "normal";
149
134
  return {
150
- source: "binance_coinbase_btc",
151
- indicatorKey: "payload.indicators.spread",
135
+ source: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
136
+ indicatorKey: "payload.additionalIndicators.baseContext.relative.execution.venueSpread",
152
137
  available: true,
153
138
  value,
139
+ zScore,
154
140
  bps,
155
141
  absBps,
156
142
  bias,
157
143
  severity
158
144
  };
159
145
  };
160
- var readSpreadFromSignal = (signal) => {
161
- const indicatorSpread = getLastFiniteNumber(signal.indicators?.spread);
162
- if (indicatorSpread != null) {
163
- return indicatorSpread;
146
+ var buildTrueDeltaContextFromSignal = (signal) => {
147
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
148
+ const participation = toRecord(baseContext?.participation);
149
+ const delta = toRecord(participation?.delta);
150
+ const source = String(delta?.source ?? "");
151
+ const isTrueDeltaSource = source === "kline_taker_volume" || source === "agg_trades" || source === "trades";
152
+ if (!delta || !isTrueDeltaSource) {
153
+ return {
154
+ source: source || null,
155
+ available: false,
156
+ buyPressurePct: null,
157
+ buyVolume: null,
158
+ sellVolume: null,
159
+ netDelta: null,
160
+ deltaPct: null,
161
+ signedVolumeZScore: null
162
+ };
164
163
  }
165
- return getLastFiniteNumber(signal.additionalIndicators?.spread);
164
+ return {
165
+ source,
166
+ available: true,
167
+ buyPressurePct: toFiniteNumber(delta.buyPressurePct),
168
+ buyVolume: toFiniteNumber(delta.buyVolume),
169
+ sellVolume: toFiniteNumber(delta.sellVolume),
170
+ netDelta: toFiniteNumber(delta.netDelta),
171
+ deltaPct: toFiniteNumber(delta.deltaPct),
172
+ signedVolumeZScore: toFiniteNumber(delta.signedVolumeZScore)
173
+ };
166
174
  };
167
- var buildAiMarketContext = (signal) => {
168
- const existingMarketContext = toRecord(
169
- signal.additionalIndicators?.marketContext
170
- );
171
- const existingSpread = toRecord(existingMarketContext?.binanceCoinbaseSpread);
172
- const spread = readSpreadFromSignal(signal);
175
+ var buildTradeFlowContextFromSignal = (signal) => {
176
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
177
+ const participation = toRecord(baseContext?.participation);
178
+ const tradeFlow = toRecord(participation?.tradeFlow);
179
+ if (!tradeFlow) {
180
+ return {
181
+ source: null,
182
+ available: false,
183
+ interval: null,
184
+ stale: null,
185
+ trades: null,
186
+ buyPressurePct: null,
187
+ netBaseDelta: null,
188
+ netQuoteDelta: null
189
+ };
190
+ }
191
+ return {
192
+ source: String(tradeFlow.source ?? ""),
193
+ available: true,
194
+ interval: String(tradeFlow.interval ?? ""),
195
+ stale: typeof tradeFlow.stale === "boolean" ? tradeFlow.stale : null,
196
+ trades: toFiniteNumber(tradeFlow.trades),
197
+ buyPressurePct: toFiniteNumber(tradeFlow.buyPressurePct),
198
+ netBaseDelta: toFiniteNumber(tradeFlow.netBaseDelta),
199
+ netQuoteDelta: toFiniteNumber(tradeFlow.netQuoteDelta)
200
+ };
201
+ };
202
+ var buildMarketBreadthContextFromSignal = (signal) => {
203
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
204
+ const relative = toRecord(baseContext?.relative);
205
+ const breadth = toRecord(relative?.marketBreadth);
206
+ if (!breadth) {
207
+ return {
208
+ source: null,
209
+ available: false,
210
+ universe: null,
211
+ interval: null,
212
+ stale: null,
213
+ symbolsCount: null,
214
+ advanceDeclineRatio: null,
215
+ pctAboveMa20: null,
216
+ pctAboveMa50: null,
217
+ equalWeightedReturn: null,
218
+ volumeWeightedReturn: null,
219
+ dispersion: null
220
+ };
221
+ }
222
+ return {
223
+ source: String(breadth.source ?? ""),
224
+ available: true,
225
+ universe: String(breadth.universe ?? ""),
226
+ interval: String(breadth.interval ?? ""),
227
+ stale: typeof breadth.stale === "boolean" ? breadth.stale : null,
228
+ symbolsCount: toFiniteNumber(breadth.symbolsCount),
229
+ advanceDeclineRatio: toFiniteNumber(breadth.advanceDeclineRatio),
230
+ pctAboveMa20: toFiniteNumber(breadth.pctAboveMa20),
231
+ pctAboveMa50: toFiniteNumber(breadth.pctAboveMa50),
232
+ equalWeightedReturn: toFiniteNumber(breadth.equalWeightedReturn),
233
+ volumeWeightedReturn: toFiniteNumber(breadth.volumeWeightedReturn),
234
+ dispersion: toFiniteNumber(breadth.dispersion)
235
+ };
236
+ };
237
+ var buildTargetVsBtcContextFromSignal = (signal) => {
238
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
239
+ const relative = toRecord(baseContext?.relative);
240
+ const targetVsBtc = toRecord(relative?.targetVsBtc);
241
+ if (!targetVsBtc) {
242
+ return {
243
+ source: null,
244
+ available: false,
245
+ ratioReturn1h: null,
246
+ ratioReturn4h: null,
247
+ ratioReturn24h: null,
248
+ alphaVsBtc1h: null,
249
+ alphaVsBtc4h: null,
250
+ alphaVsBtc24h: null,
251
+ betaToBtc20: null,
252
+ correlationToBtc20: null,
253
+ ratioTrend: null
254
+ };
255
+ }
256
+ return {
257
+ source: String(targetVsBtc.source ?? ""),
258
+ available: true,
259
+ ratioReturn1h: toFiniteNumber(targetVsBtc.ratioReturn1h),
260
+ ratioReturn4h: toFiniteNumber(targetVsBtc.ratioReturn4h),
261
+ ratioReturn24h: toFiniteNumber(targetVsBtc.ratioReturn24h),
262
+ alphaVsBtc1h: toFiniteNumber(targetVsBtc.alphaVsBtc1h),
263
+ alphaVsBtc4h: toFiniteNumber(targetVsBtc.alphaVsBtc4h),
264
+ alphaVsBtc24h: toFiniteNumber(targetVsBtc.alphaVsBtc24h),
265
+ betaToBtc20: toFiniteNumber(targetVsBtc.betaToBtc20),
266
+ correlationToBtc20: toFiniteNumber(targetVsBtc.correlationToBtc20),
267
+ ratioTrend: typeof targetVsBtc.ratioTrend === "string" ? targetVsBtc.ratioTrend : null
268
+ };
269
+ };
270
+ var buildBtcAltRegimeContextFromSignal = (signal) => {
271
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
272
+ const relative = toRecord(baseContext?.relative);
273
+ const btcAltRegime = toRecord(relative?.btcAltRegime);
274
+ if (!btcAltRegime) {
275
+ return {
276
+ source: null,
277
+ available: false,
278
+ universe: null,
279
+ interval: null,
280
+ stale: null,
281
+ regime: null,
282
+ btcReturn24h: null,
283
+ altBasketReturn24h: null,
284
+ btcVsAltReturn24h: null,
285
+ btcTurnoverShare24h: null,
286
+ btcTurnoverShareChange24h: null,
287
+ altVolToBtcVol24h: null,
288
+ altDispersion24h: null
289
+ };
290
+ }
291
+ return {
292
+ source: String(btcAltRegime.source ?? ""),
293
+ available: true,
294
+ universe: String(btcAltRegime.universe ?? ""),
295
+ interval: String(btcAltRegime.interval ?? ""),
296
+ stale: typeof btcAltRegime.stale === "boolean" ? btcAltRegime.stale : null,
297
+ regime: typeof btcAltRegime.regime === "string" ? btcAltRegime.regime : null,
298
+ btcReturn24h: toFiniteNumber(btcAltRegime.btcReturn24h),
299
+ altBasketReturn24h: toFiniteNumber(btcAltRegime.altBasketReturn24h),
300
+ btcVsAltReturn24h: toFiniteNumber(btcAltRegime.btcVsAltReturn24h),
301
+ btcTurnoverShare24h: toFiniteNumber(btcAltRegime.btcTurnoverShare24h),
302
+ btcTurnoverShareChange24h: toFiniteNumber(
303
+ btcAltRegime.btcTurnoverShareChange24h
304
+ ),
305
+ altVolToBtcVol24h: toFiniteNumber(btcAltRegime.altVolToBtcVol24h),
306
+ altDispersion24h: toFiniteNumber(btcAltRegime.altDispersion24h)
307
+ };
308
+ };
309
+ var buildCmcGlobalContextFromSignal = (signal) => {
310
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
311
+ const relative = toRecord(baseContext?.relative);
312
+ const cmcGlobal = toRecord(relative?.cmcGlobal);
313
+ if (!cmcGlobal) {
314
+ return {
315
+ source: null,
316
+ available: false,
317
+ interval: null,
318
+ asOfTs: null,
319
+ stale: null,
320
+ totalMarketCapUsd: null,
321
+ totalVolumeUsd: null,
322
+ totalVolumeReportedUsd: null,
323
+ altMarketCapUsd: null,
324
+ altVolumeUsd: null,
325
+ altVolumeReportedUsd: null,
326
+ btcDominancePct: null,
327
+ ethDominancePct: null,
328
+ btcDominanceChange24hPct: null,
329
+ ethDominanceChange24hPct: null,
330
+ altMarketCapChange24hPct: null,
331
+ altVolumeChange24hPct: null,
332
+ activeCryptocurrencies: null,
333
+ activeExchanges: null,
334
+ activeMarketPairs: null,
335
+ altLiquidityRegime: null
336
+ };
337
+ }
338
+ return {
339
+ source: String(cmcGlobal.source ?? ""),
340
+ available: true,
341
+ interval: typeof cmcGlobal.interval === "string" ? cmcGlobal.interval : null,
342
+ asOfTs: toFiniteNumber(cmcGlobal.asOfTs),
343
+ stale: typeof cmcGlobal.stale === "boolean" ? cmcGlobal.stale : null,
344
+ totalMarketCapUsd: toFiniteNumber(cmcGlobal.totalMarketCapUsd),
345
+ totalVolumeUsd: toFiniteNumber(cmcGlobal.totalVolumeUsd),
346
+ totalVolumeReportedUsd: toFiniteNumber(cmcGlobal.totalVolumeReportedUsd),
347
+ altMarketCapUsd: toFiniteNumber(cmcGlobal.altMarketCapUsd),
348
+ altVolumeUsd: toFiniteNumber(cmcGlobal.altVolumeUsd),
349
+ altVolumeReportedUsd: toFiniteNumber(cmcGlobal.altVolumeReportedUsd),
350
+ btcDominancePct: toFiniteNumber(cmcGlobal.btcDominancePct),
351
+ ethDominancePct: toFiniteNumber(cmcGlobal.ethDominancePct),
352
+ btcDominanceChange24hPct: toFiniteNumber(
353
+ cmcGlobal.btcDominanceChange24hPct
354
+ ),
355
+ ethDominanceChange24hPct: toFiniteNumber(
356
+ cmcGlobal.ethDominanceChange24hPct
357
+ ),
358
+ altMarketCapChange24hPct: toFiniteNumber(
359
+ cmcGlobal.altMarketCapChange24hPct
360
+ ),
361
+ altVolumeChange24hPct: toFiniteNumber(cmcGlobal.altVolumeChange24hPct),
362
+ activeCryptocurrencies: toFiniteNumber(cmcGlobal.activeCryptocurrencies),
363
+ activeExchanges: toFiniteNumber(cmcGlobal.activeExchanges),
364
+ activeMarketPairs: toFiniteNumber(cmcGlobal.activeMarketPairs),
365
+ altLiquidityRegime: typeof cmcGlobal.altLiquidityRegime === "string" ? cmcGlobal.altLiquidityRegime : null
366
+ };
367
+ };
368
+ var buildCmcReferenceAssetsContextFromSignal = (signal) => {
369
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
370
+ const relative = toRecord(baseContext?.relative);
371
+ const cmcReferenceAssets = toRecord(relative?.cmcReferenceAssets);
372
+ if (!cmcReferenceAssets) {
373
+ return {
374
+ source: null,
375
+ available: false,
376
+ interval: null,
377
+ asOfTs: null,
378
+ stale: null,
379
+ btcMarketCapUsd: null,
380
+ ethMarketCapUsd: null,
381
+ btcVolumeUsd: null,
382
+ ethVolumeUsd: null,
383
+ btcVolumeToMarketCap: null,
384
+ ethVolumeToMarketCap: null,
385
+ ethBtcMarketCapRatio: null,
386
+ ethBtcMarketCapRatioChange24hPct: null,
387
+ ethVsBtcVolumeRatio: null,
388
+ referenceLiquidityRegime: null
389
+ };
390
+ }
391
+ return {
392
+ source: String(cmcReferenceAssets.source ?? ""),
393
+ available: true,
394
+ interval: typeof cmcReferenceAssets.interval === "string" ? cmcReferenceAssets.interval : null,
395
+ asOfTs: toFiniteNumber(cmcReferenceAssets.asOfTs),
396
+ stale: typeof cmcReferenceAssets.stale === "boolean" ? cmcReferenceAssets.stale : null,
397
+ btcMarketCapUsd: toFiniteNumber(cmcReferenceAssets.btcMarketCapUsd),
398
+ ethMarketCapUsd: toFiniteNumber(cmcReferenceAssets.ethMarketCapUsd),
399
+ btcVolumeUsd: toFiniteNumber(cmcReferenceAssets.btcVolumeUsd),
400
+ ethVolumeUsd: toFiniteNumber(cmcReferenceAssets.ethVolumeUsd),
401
+ btcVolumeToMarketCap: toFiniteNumber(
402
+ cmcReferenceAssets.btcVolumeToMarketCap
403
+ ),
404
+ ethVolumeToMarketCap: toFiniteNumber(
405
+ cmcReferenceAssets.ethVolumeToMarketCap
406
+ ),
407
+ ethBtcMarketCapRatio: toFiniteNumber(
408
+ cmcReferenceAssets.ethBtcMarketCapRatio
409
+ ),
410
+ ethBtcMarketCapRatioChange24hPct: toFiniteNumber(
411
+ cmcReferenceAssets.ethBtcMarketCapRatioChange24hPct
412
+ ),
413
+ ethVsBtcVolumeRatio: toFiniteNumber(cmcReferenceAssets.ethVsBtcVolumeRatio),
414
+ referenceLiquidityRegime: typeof cmcReferenceAssets.referenceLiquidityRegime === "string" ? cmcReferenceAssets.referenceLiquidityRegime : null
415
+ };
416
+ };
417
+ var buildCmcExchangeLiquidityContextFromSignal = (signal) => {
418
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
419
+ const relative = toRecord(baseContext?.relative);
420
+ const cmcExchangeLiquidity = toRecord(relative?.cmcExchangeLiquidity);
421
+ if (!cmcExchangeLiquidity) {
422
+ return {
423
+ source: null,
424
+ available: false,
425
+ interval: null,
426
+ asOfTs: null,
427
+ stale: null,
428
+ exchangesCount: null,
429
+ totalVolumeUsd: null,
430
+ totalVolumeChange24hPct: null,
431
+ binanceVolumeUsd: null,
432
+ binanceVolumeShare: null,
433
+ topExchangeVolumeShare: null,
434
+ liquidityRegime: null
435
+ };
436
+ }
437
+ return {
438
+ source: String(cmcExchangeLiquidity.source ?? ""),
439
+ available: true,
440
+ interval: typeof cmcExchangeLiquidity.interval === "string" ? cmcExchangeLiquidity.interval : null,
441
+ asOfTs: toFiniteNumber(cmcExchangeLiquidity.asOfTs),
442
+ stale: typeof cmcExchangeLiquidity.stale === "boolean" ? cmcExchangeLiquidity.stale : null,
443
+ exchangesCount: toFiniteNumber(cmcExchangeLiquidity.exchangesCount),
444
+ totalVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.totalVolumeUsd),
445
+ totalVolumeChange24hPct: toFiniteNumber(
446
+ cmcExchangeLiquidity.totalVolumeChange24hPct
447
+ ),
448
+ binanceVolumeUsd: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeUsd),
449
+ binanceVolumeShare: toFiniteNumber(cmcExchangeLiquidity.binanceVolumeShare),
450
+ topExchangeVolumeShare: toFiniteNumber(
451
+ cmcExchangeLiquidity.topExchangeVolumeShare
452
+ ),
453
+ liquidityRegime: typeof cmcExchangeLiquidity.liquidityRegime === "string" ? cmcExchangeLiquidity.liquidityRegime : null
454
+ };
455
+ };
456
+ var buildCmcFearGreedContextFromSignal = (signal) => {
457
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
458
+ const relative = toRecord(baseContext?.relative);
459
+ const cmcFearGreed = toRecord(relative?.cmcFearGreed);
460
+ if (!cmcFearGreed) {
461
+ return {
462
+ source: null,
463
+ available: false,
464
+ interval: null,
465
+ asOfTs: null,
466
+ stale: null,
467
+ value: null,
468
+ valueChange24h: null,
469
+ valueChange7d: null,
470
+ classification: null,
471
+ sentimentRegime: null
472
+ };
473
+ }
474
+ return {
475
+ source: String(cmcFearGreed.source ?? ""),
476
+ available: true,
477
+ interval: typeof cmcFearGreed.interval === "string" ? cmcFearGreed.interval : null,
478
+ asOfTs: toFiniteNumber(cmcFearGreed.asOfTs),
479
+ stale: typeof cmcFearGreed.stale === "boolean" ? cmcFearGreed.stale : null,
480
+ value: toFiniteNumber(cmcFearGreed.value),
481
+ valueChange24h: toFiniteNumber(cmcFearGreed.valueChange24h),
482
+ valueChange7d: toFiniteNumber(cmcFearGreed.valueChange7d),
483
+ classification: typeof cmcFearGreed.classification === "string" ? cmcFearGreed.classification : null,
484
+ sentimentRegime: typeof cmcFearGreed.sentimentRegime === "string" ? cmcFearGreed.sentimentRegime : null
485
+ };
486
+ };
487
+ var buildCmcIndexesContextFromSignal = (signal) => {
488
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
489
+ const relative = toRecord(baseContext?.relative);
490
+ const cmcIndexes = toRecord(relative?.cmcIndexes);
491
+ if (!cmcIndexes) {
492
+ return {
493
+ source: null,
494
+ available: false,
495
+ interval: null,
496
+ asOfTs: null,
497
+ stale: null,
498
+ cmc100Value: null,
499
+ cmc100Change24hPct: null,
500
+ cmc100TopConstituentSymbol: null,
501
+ cmc100TopConstituentWeightPct: null,
502
+ cmc20Value: null,
503
+ cmc20Change24hPct: null,
504
+ cmc20TopConstituentSymbol: null,
505
+ cmc20TopConstituentWeightPct: null,
506
+ cmc20ToCmc100Ratio: null,
507
+ cmc20ToCmc100RatioChange24hPct: null,
508
+ indexRegime: null
509
+ };
510
+ }
511
+ return {
512
+ source: String(cmcIndexes.source ?? ""),
513
+ available: true,
514
+ interval: typeof cmcIndexes.interval === "string" ? cmcIndexes.interval : null,
515
+ asOfTs: toFiniteNumber(cmcIndexes.asOfTs),
516
+ stale: typeof cmcIndexes.stale === "boolean" ? cmcIndexes.stale : null,
517
+ cmc100Value: toFiniteNumber(cmcIndexes.cmc100Value),
518
+ cmc100Change24hPct: toFiniteNumber(cmcIndexes.cmc100Change24hPct),
519
+ cmc100TopConstituentSymbol: typeof cmcIndexes.cmc100TopConstituentSymbol === "string" ? cmcIndexes.cmc100TopConstituentSymbol : null,
520
+ cmc100TopConstituentWeightPct: toFiniteNumber(
521
+ cmcIndexes.cmc100TopConstituentWeightPct
522
+ ),
523
+ cmc20Value: toFiniteNumber(cmcIndexes.cmc20Value),
524
+ cmc20Change24hPct: toFiniteNumber(cmcIndexes.cmc20Change24hPct),
525
+ cmc20TopConstituentSymbol: typeof cmcIndexes.cmc20TopConstituentSymbol === "string" ? cmcIndexes.cmc20TopConstituentSymbol : null,
526
+ cmc20TopConstituentWeightPct: toFiniteNumber(
527
+ cmcIndexes.cmc20TopConstituentWeightPct
528
+ ),
529
+ cmc20ToCmc100Ratio: toFiniteNumber(cmcIndexes.cmc20ToCmc100Ratio),
530
+ cmc20ToCmc100RatioChange24hPct: toFiniteNumber(
531
+ cmcIndexes.cmc20ToCmc100RatioChange24hPct
532
+ ),
533
+ indexRegime: typeof cmcIndexes.indexRegime === "string" ? cmcIndexes.indexRegime : null
534
+ };
535
+ };
536
+ var buildReferenceTradeFlowContextFromSignal = (signal) => {
537
+ const baseContext = toRecord(signal.additionalIndicators?.baseContext);
538
+ const relative = toRecord(baseContext?.relative);
539
+ const refs = toRecord(relative?.referenceTradeFlow);
540
+ const primaryReferenceSymbol = typeof refs?.primaryReferenceSymbol === "string" ? refs.primaryReferenceSymbol : null;
541
+ const tradeFlowBySymbol = toRecord(refs?.tradeFlowBySymbol);
542
+ const primaryTradeFlow = primaryReferenceSymbol != null ? toRecord(tradeFlowBySymbol?.[primaryReferenceSymbol]) : null;
543
+ if (!refs) {
544
+ return {
545
+ source: null,
546
+ available: false,
547
+ primaryReferenceSymbol: null,
548
+ referenceSymbols: [],
549
+ primaryTradeFlowBuyPressurePct: null,
550
+ primaryTradeFlowStale: null
551
+ };
552
+ }
173
553
  return {
174
- ...existingMarketContext ?? {},
175
- tradingSession: buildTradingSessionContext(signal.timestamp),
176
- binanceCoinbaseSpread: spread != null ? buildSpreadContextFromValue(spread) : existingSpread ?? buildMissingSpreadContext()
554
+ source: String(refs.source ?? ""),
555
+ available: true,
556
+ primaryReferenceSymbol,
557
+ referenceSymbols: Array.isArray(refs.referenceSymbols) ? refs.referenceSymbols.map(String) : [],
558
+ primaryTradeFlowBuyPressurePct: toFiniteNumber(
559
+ primaryTradeFlow?.buyPressurePct
560
+ ),
561
+ primaryTradeFlowStale: typeof primaryTradeFlow?.stale === "boolean" ? primaryTradeFlow.stale : null
177
562
  };
178
563
  };
564
+ var buildAiMarketContext = (signal) => ({
565
+ execution: {
566
+ binanceCoinbaseSpread: buildSpreadContextFromSignal(signal)
567
+ },
568
+ participation: {
569
+ trueDelta: buildTrueDeltaContextFromSignal(signal),
570
+ tradeFlow: buildTradeFlowContextFromSignal(signal)
571
+ },
572
+ relative: {
573
+ marketBreadth: buildMarketBreadthContextFromSignal(signal),
574
+ targetVsBtc: buildTargetVsBtcContextFromSignal(signal),
575
+ btcAltRegime: buildBtcAltRegimeContextFromSignal(signal),
576
+ cmcGlobal: buildCmcGlobalContextFromSignal(signal),
577
+ cmcReferenceAssets: buildCmcReferenceAssetsContextFromSignal(signal),
578
+ cmcExchangeLiquidity: buildCmcExchangeLiquidityContextFromSignal(signal),
579
+ cmcFearGreed: buildCmcFearGreedContextFromSignal(signal),
580
+ cmcIndexes: buildCmcIndexesContextFromSignal(signal),
581
+ referenceTradeFlow: buildReferenceTradeFlowContextFromSignal(signal)
582
+ }
583
+ });
179
584
 
180
585
  // src/strategy/manifests.ts
181
586
  var import_indicators = require("@tradejs/core/indicators");
@@ -184,7 +589,6 @@ var import_logger2 = require("@tradejs/infra/logger");
184
589
  // src/tradejsConfig.ts
185
590
  var import_fs = __toESM(require("fs"));
186
591
  var import_path = __toESM(require("path"));
187
- var import_module = require("module");
188
592
  var import_url = require("url");
189
593
  var import_config = require("@tradejs/core/config");
190
594
  var import_logger = require("@tradejs/infra/logger");
@@ -200,6 +604,7 @@ var cachedByCwd = /* @__PURE__ */ new Map();
200
604
  var announcedConfigFile = /* @__PURE__ */ new Set();
201
605
  var tsNodeRegistered = false;
202
606
  var tsconfigPathsRegisteredByCwd = /* @__PURE__ */ new Set();
607
+ var tsconfigPathMatchersByCwd = /* @__PURE__ */ new Map();
203
608
  var getTradejsProjectCwd = (cwd) => {
204
609
  const explicit = String(cwd ?? "").trim();
205
610
  if (explicit) {
@@ -229,7 +634,14 @@ var normalizeConfig = (rawConfig) => {
229
634
  ...hooks ? { hooks } : {}
230
635
  };
231
636
  };
232
- var getRequireFn = (cwd = getTradejsProjectCwd()) => (0, import_module.createRequire)(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
637
+ var getNodeCreateRequire = () => {
638
+ const builtinModule = process.getBuiltinModule?.("module");
639
+ if (typeof builtinModule?.createRequire === "function") {
640
+ return builtinModule.createRequire;
641
+ }
642
+ throw new TypeError("module.createRequire is not available");
643
+ };
644
+ var getRequireFn = (cwd = getTradejsProjectCwd()) => getNodeCreateRequire()(import_path.default.join(import_path.default.resolve(cwd), "__tradejs_loader__.js"));
233
645
  var ensureTsNodeRegistered = async () => {
234
646
  if (tsNodeRegistered) {
235
647
  return;
@@ -239,8 +651,8 @@ var ensureTsNodeRegistered = async () => {
239
651
  tsNode.register?.({
240
652
  transpileOnly: true,
241
653
  compilerOptions: {
242
- module: "commonjs",
243
- moduleResolution: "node"
654
+ module: "Node16",
655
+ moduleResolution: "node16"
244
656
  }
245
657
  });
246
658
  tsNodeRegistered = true;
@@ -267,6 +679,42 @@ var ensureTsconfigPathsRegistered = async (cwd = getTradejsProjectCwd()) => {
267
679
  });
268
680
  tsconfigPathsRegisteredByCwd.add(projectRoot);
269
681
  };
682
+ var resolveTsconfigPathModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
683
+ const projectRoot = getTradejsProjectCwd(cwd);
684
+ const cachedMatcher = tsconfigPathMatchersByCwd.get(projectRoot);
685
+ if (cachedMatcher) {
686
+ const resolved2 = cachedMatcher(moduleName);
687
+ return resolved2 || null;
688
+ }
689
+ const tsconfigPathsModule = await import("tsconfig-paths");
690
+ const loadConfig = tsconfigPathsModule.loadConfig;
691
+ const createMatchPath = tsconfigPathsModule.createMatchPath;
692
+ if (typeof loadConfig !== "function" || typeof createMatchPath !== "function") {
693
+ return null;
694
+ }
695
+ const loadedConfig = loadConfig(projectRoot);
696
+ if (loadedConfig.resultType !== "success") {
697
+ return null;
698
+ }
699
+ const matchPath = createMatchPath(
700
+ loadedConfig.absoluteBaseUrl,
701
+ loadedConfig.paths
702
+ );
703
+ const matcher = (requestedModule) => matchPath(requestedModule, void 0, import_fs.default.existsSync, [
704
+ ".ts",
705
+ ".tsx",
706
+ ".mts",
707
+ ".cts",
708
+ ".js",
709
+ ".jsx",
710
+ ".mjs",
711
+ ".cjs",
712
+ ".json"
713
+ ]) || "";
714
+ tsconfigPathMatchersByCwd.set(projectRoot, matcher);
715
+ const resolved = matcher(moduleName);
716
+ return resolved || null;
717
+ };
270
718
  var toImportSpecifier = (moduleName) => {
271
719
  if (moduleName.startsWith("file://")) {
272
720
  return moduleName;
@@ -325,7 +773,18 @@ var importTradejsModule = async (moduleName, cwd = getTradejsProjectCwd()) => {
325
773
  }
326
774
  if (isBareModuleSpecifier(normalized)) {
327
775
  await ensureTsconfigPathsRegistered(cwd);
328
- return requireFn(normalized);
776
+ try {
777
+ return requireFn(normalized);
778
+ } catch (error) {
779
+ const resolvedByTsconfig = await resolveTsconfigPathModule(
780
+ normalized,
781
+ cwd
782
+ );
783
+ if (resolvedByTsconfig && resolvedByTsconfig !== normalized) {
784
+ return requireFn(resolvedByTsconfig);
785
+ }
786
+ throw error;
787
+ }
329
788
  }
330
789
  try {
331
790
  return await import(
@@ -600,6 +1059,10 @@ var strategies = new Proxy(
600
1059
  }
601
1060
  );
602
1061
 
1062
+ // src/strategy/policyProfiles.ts
1063
+ var getStrategyProfileAiAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.aiAdapter ?? manifest?.aiAdapter;
1064
+ var getStrategyProfileMlAdapter = (manifest, profileId) => manifest?.policyProfiles?.find(({ id }) => id === profileId)?.mlAdapter ?? manifest?.mlAdapter;
1065
+
603
1066
  // src/strategyAdapters/ai.ts
604
1067
  var toRecord2 = (value) => {
605
1068
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -627,13 +1090,13 @@ var buildBaseAiPayload = (signal) => {
627
1090
  }
628
1091
  },
629
1092
  figures: trimSeriesDeep(signal.figures ?? {}),
630
- indicators: trimSeriesDeep(signal.indicators),
1093
+ indicators: buildCompactAiIndicatorsSnapshot(signal.indicators),
631
1094
  additionalIndicators: trimSeriesDeep(additionalIndicators)
632
1095
  };
633
1096
  };
634
1097
  var defaultAiAdapter = {};
635
- var getStrategyAiAdapter = (strategy) => getStrategyManifest(strategy)?.aiAdapter ?? defaultAiAdapter;
636
- var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy);
1098
+ var getStrategyAiAdapter = (strategy, profileId) => getStrategyProfileAiAdapter(getStrategyManifest(strategy), profileId) ?? defaultAiAdapter;
1099
+ var getSignalAiAdapter = (signal) => getStrategyAiAdapter(signal.strategy, signal.policyProfileId);
637
1100
  var buildAiPayloadByStrategy = (signal) => {
638
1101
  const basePayload = buildBaseAiPayload(signal);
639
1102
  const adapter = getSignalAiAdapter(signal);
@@ -643,165 +1106,1034 @@ var buildAiPayloadByStrategy = (signal) => {
643
1106
  // src/ai.ts
644
1107
  var buildAiPayload = (signal) => buildAiPayloadByStrategy(signal);
645
1108
 
646
- // src/strategyHelpers/derivativesContext.ts
647
- var import_indicators2 = require("@tradejs/core/indicators");
648
- var import_constants = require("@tradejs/core/constants");
1109
+ // src/strategyHelpers/binanceMarketContext.ts
649
1110
  var import_timescale = require("@tradejs/infra/timescale");
650
1111
  var import_logger3 = require("@tradejs/infra/logger");
651
- var DEFAULT_INTERVALS = ["15m", "1h"];
652
- var DEFAULT_LOOKBACK_HOURS = 48;
653
- var derivativesContextUnavailable = false;
1112
+ var import_strategies = require("@tradejs/core/strategies");
1113
+ var DEFAULT_MAX_AGE_BY_INTERVAL = {
1114
+ "1m": 3 * 6e4,
1115
+ "5m": 10 * 6e4,
1116
+ "15m": 30 * 6e4,
1117
+ "1h": 2 * 60 * 6e4
1118
+ };
1119
+ var binanceMarketContextUnavailable = false;
1120
+ var referenceRowsCache = /* @__PURE__ */ new Map();
1121
+ var breadthCache = /* @__PURE__ */ new Map();
654
1122
  var parseEnabledFlag = (value, env) => {
655
1123
  const normalized = String(value ?? "").trim().toLowerCase();
656
- if (!normalized) return false;
1124
+ if (!normalized)
1125
+ return env === "BACKTEST" || env === "CRON" || env === "PARITY";
657
1126
  if (["1", "true", "yes", "on"].includes(normalized)) return true;
1127
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
658
1128
  if (normalized === "backtest") return env === "BACKTEST";
659
1129
  if (normalized === "live") return env !== "BACKTEST";
660
1130
  return false;
661
1131
  };
662
- var parseLookbackMs = () => {
663
- const hours = Number(process.env.DERIVATIVES_CONTEXT_LOOKBACK_HOURS);
664
- const normalizedHours = Number.isFinite(hours) && hours > 0 ? hours : DEFAULT_LOOKBACK_HOURS;
665
- return normalizedHours * 60 * 60 * 1e3;
1132
+ var toFiniteNumberOrNull = (value) => {
1133
+ const numeric = typeof value === "number" ? value : Number(value);
1134
+ return Number.isFinite(numeric) ? numeric : null;
666
1135
  };
667
- var parseIntervals = () => {
668
- const fromEnv = (0, import_indicators2.normalizeDerivativesIntervals)(
669
- process.env.DERIVATIVES_CONTEXT_INTERVALS
670
- );
671
- return fromEnv.length ? fromEnv : DEFAULT_INTERVALS;
1136
+ var signalIntervalToMarketInterval = (value) => {
1137
+ const normalized = String(value).trim().toLowerCase();
1138
+ if (normalized === "1" || normalized === "1m") return "1m";
1139
+ if (normalized === "5" || normalized === "5m") return "5m";
1140
+ if (normalized === "60" || normalized === "1h") return "1h";
1141
+ return "15m";
1142
+ };
1143
+ var resolveMarketInterval = (signal, override) => override ?? signalIntervalToMarketInterval(signal.interval);
1144
+ var resolveBreadthUniverse = () => (process.env.BINANCE_MARKET_CONTEXT_BREADTH_UNIVERSE || "binance_top30_usdt").trim().toLowerCase();
1145
+ var getReferenceSymbols = () => {
1146
+ const symbols = (process.env.BINANCE_MARKET_CONTEXT_REFERENCE_SYMBOLS || "BTCUSDT,ETHUSDT").split(",").map((item) => item.trim().toUpperCase()).filter(Boolean);
1147
+ return symbols.length ? [...new Set(symbols)] : ["BTCUSDT", "ETHUSDT"];
672
1148
  };
673
- var getDerivativesContextReferenceSymbols = () => [
674
- ...import_constants.DERIVATIVES_CONTEXT_REFERENCE_SYMBOLS
675
- ];
676
- var normalizeSymbol = (symbol) => String(symbol || "").trim().toUpperCase();
677
1149
  var resolvePrimaryReferenceSymbol = (signalSymbol) => {
678
- const symbol = normalizeSymbol(signalSymbol);
679
- const referenceSymbols = getDerivativesContextReferenceSymbols();
680
- return referenceSymbols.some((referenceSymbol) => referenceSymbol === symbol) ? symbol : referenceSymbols[0];
1150
+ const symbol = signalSymbol.trim().toUpperCase();
1151
+ const referenceSymbols = getReferenceSymbols();
1152
+ return referenceSymbols.includes(symbol) ? symbol : referenceSymbols[0];
681
1153
  };
682
- var buildReferenceDerivativesContext = (params) => {
683
- const { targetSymbol, primaryReferenceSymbol, referenceContexts } = params;
684
- const primaryContext = referenceContexts[primaryReferenceSymbol] ?? referenceContexts[getDerivativesContextReferenceSymbols()[0]];
685
- if (!primaryContext) {
686
- throw new Error("No derivatives reference contexts built");
687
- }
688
- return {
689
- ...primaryContext,
690
- targetSymbol,
691
- primaryReferenceSymbol: primaryContext.symbol,
692
- referenceSymbols: getDerivativesContextReferenceSymbols(),
693
- referenceContexts
694
- };
1154
+ var hasBaseContext = (signal) => Boolean(
1155
+ signal.additionalIndicators?.baseContext && typeof signal.additionalIndicators.baseContext === "object" && !Array.isArray(signal.additionalIndicators.baseContext)
1156
+ );
1157
+ var isBinanceMarketContextEnabled = (env) => parseEnabledFlag(process.env.BINANCE_MARKET_CONTEXT_ENABLED, env);
1158
+ var toTradeFlowContext = (row, interval) => row ? {
1159
+ source: "binance_agg_trades",
1160
+ interval,
1161
+ asOfTs: row.ts.getTime(),
1162
+ ageMs: row.ageMs,
1163
+ stale: row.stale,
1164
+ trades: toFiniteNumberOrNull(row.trades),
1165
+ buyPressurePct: toFiniteNumberOrNull(row.buyPressurePct),
1166
+ buyBaseVolume: toFiniteNumberOrNull(row.buyBaseVolume),
1167
+ sellBaseVolume: toFiniteNumberOrNull(row.sellBaseVolume),
1168
+ buyQuoteVolume: toFiniteNumberOrNull(row.buyQuoteVolume),
1169
+ sellQuoteVolume: toFiniteNumberOrNull(row.sellQuoteVolume),
1170
+ netBaseDelta: toFiniteNumberOrNull(row.netBaseDelta),
1171
+ netQuoteDelta: toFiniteNumberOrNull(row.netQuoteDelta)
1172
+ } : null;
1173
+ var getCachedReferenceRows = ({
1174
+ referenceSymbols,
1175
+ interval,
1176
+ timestamp,
1177
+ maxAgeMs
1178
+ }) => {
1179
+ const key = `${referenceSymbols.join(",")}:${interval}:${timestamp}:${maxAgeMs}`;
1180
+ const cached = referenceRowsCache.get(key);
1181
+ if (cached) return cached;
1182
+ const promise = Promise.all(
1183
+ referenceSymbols.map(async (symbol) => {
1184
+ const tradeFlow = await (0, import_timescale.getLatestMarketTradeFlow)({
1185
+ symbol,
1186
+ interval,
1187
+ atMs: timestamp,
1188
+ maxAgeMs
1189
+ });
1190
+ return {
1191
+ symbol,
1192
+ tradeFlow: toTradeFlowContext(tradeFlow, interval)
1193
+ };
1194
+ })
1195
+ );
1196
+ referenceRowsCache.set(key, promise);
1197
+ return promise;
695
1198
  };
696
- var isDerivativesContextEnabled = (env) => parseEnabledFlag(process.env.DERIVATIVES_CONTEXT_ENABLED, env);
697
- var enrichSignalWithDerivativesContext = async (params) => {
698
- const { signal, env, enabled = isDerivativesContextEnabled(env) } = params;
699
- if (!enabled || derivativesContextUnavailable) {
1199
+ var getCachedBreadth = ({
1200
+ breadthUniverse,
1201
+ interval,
1202
+ timestamp,
1203
+ maxAgeMs
1204
+ }) => {
1205
+ const key = `${breadthUniverse}:${interval}:${timestamp}:${maxAgeMs}`;
1206
+ const cached = breadthCache.get(key);
1207
+ if (cached) return cached;
1208
+ const promise = (0, import_timescale.getLatestMarketBreadth)({
1209
+ universe: breadthUniverse,
1210
+ interval,
1211
+ atMs: timestamp,
1212
+ maxAgeMs
1213
+ });
1214
+ breadthCache.set(key, promise);
1215
+ return promise;
1216
+ };
1217
+ var enrichSignalWithBinanceMarketContext = async (params) => {
1218
+ const {
1219
+ signal,
1220
+ env,
1221
+ enabled = isBinanceMarketContextEnabled(env),
1222
+ interval = resolveMarketInterval(signal, params.interval),
1223
+ breadthUniverse = resolveBreadthUniverse(),
1224
+ maxAgeMs = DEFAULT_MAX_AGE_BY_INTERVAL[interval]
1225
+ } = params;
1226
+ if (signal.universe === "tradfi" || !enabled || binanceMarketContextUnavailable || !hasBaseContext(signal)) {
700
1227
  return false;
701
1228
  }
702
1229
  try {
703
- const intervals = parseIntervals();
704
- const referenceSymbols = getDerivativesContextReferenceSymbols();
705
- const lookbackMs = parseLookbackMs();
706
- const contexts = await Promise.all(
707
- referenceSymbols.map(async (symbol) => {
708
- const rowsByInterval = await (0, import_timescale.getDerivativesWindow)({
709
- symbol,
710
- intervals,
711
- endMs: signal.timestamp,
712
- lookbackMs
713
- });
714
- return [
715
- symbol,
716
- (0, import_indicators2.buildDerivativesContext)({
717
- symbol,
718
- direction: signal.direction,
719
- timestamp: signal.timestamp,
720
- rowsByInterval,
721
- intervals
722
- })
723
- ];
1230
+ const referenceSymbols = getReferenceSymbols();
1231
+ const primaryReferenceSymbol = resolvePrimaryReferenceSymbol(signal.symbol);
1232
+ const [referenceRows, breadth] = await Promise.all([
1233
+ getCachedReferenceRows({
1234
+ referenceSymbols,
1235
+ interval,
1236
+ timestamp: signal.timestamp,
1237
+ maxAgeMs
1238
+ }),
1239
+ getCachedBreadth({
1240
+ breadthUniverse,
1241
+ interval,
1242
+ timestamp: signal.timestamp,
1243
+ maxAgeMs
724
1244
  })
1245
+ ]);
1246
+ const tradeFlowBySymbol = Object.fromEntries(
1247
+ referenceRows.filter((row) => row.tradeFlow).map((row) => [row.symbol, row.tradeFlow])
725
1248
  );
726
- const referenceContexts = Object.fromEntries(contexts);
727
- const derivativesContext = buildReferenceDerivativesContext({
728
- targetSymbol: signal.symbol,
729
- primaryReferenceSymbol: resolvePrimaryReferenceSymbol(signal.symbol),
730
- referenceContexts
731
- });
1249
+ const targetReferenceSymbol = signal.symbol.trim().toUpperCase();
1250
+ const targetTradeFlow = tradeFlowBySymbol[targetReferenceSymbol];
1251
+ if (!Object.keys(tradeFlowBySymbol).length && !breadth) {
1252
+ return false;
1253
+ }
1254
+ const baseContext = signal.additionalIndicators.baseContext;
732
1255
  signal.additionalIndicators = {
733
- ...signal.additionalIndicators ?? {},
734
- derivativesContext
1256
+ ...signal.additionalIndicators,
1257
+ baseContext: {
1258
+ ...baseContext,
1259
+ participation: {
1260
+ ...baseContext.participation,
1261
+ ...targetTradeFlow ? {
1262
+ tradeFlow: targetTradeFlow
1263
+ } : {}
1264
+ },
1265
+ relative: {
1266
+ ...baseContext.relative,
1267
+ execution: {
1268
+ ...baseContext.relative.execution
1269
+ },
1270
+ ...Object.keys(tradeFlowBySymbol).length ? {
1271
+ referenceTradeFlow: {
1272
+ source: "binance_reference_market",
1273
+ primaryReferenceSymbol,
1274
+ referenceSymbols,
1275
+ tradeFlowBySymbol
1276
+ }
1277
+ } : {},
1278
+ ...breadth ? {
1279
+ marketBreadth: {
1280
+ source: "binance_klines",
1281
+ universe: breadth.universe,
1282
+ interval,
1283
+ asOfTs: breadth.ts.getTime(),
1284
+ ageMs: breadth.ageMs,
1285
+ stale: breadth.stale,
1286
+ symbolsCount: toFiniteNumberOrNull(breadth.symbolsCount),
1287
+ advancers: toFiniteNumberOrNull(breadth.advancers),
1288
+ decliners: toFiniteNumberOrNull(breadth.decliners),
1289
+ unchanged: toFiniteNumberOrNull(breadth.unchanged),
1290
+ advanceDeclineRatio: toFiniteNumberOrNull(
1291
+ breadth.advanceDeclineRatio
1292
+ ),
1293
+ pctAboveMa20: toFiniteNumberOrNull(breadth.pctAboveMa20),
1294
+ pctAboveMa50: toFiniteNumberOrNull(breadth.pctAboveMa50),
1295
+ equalWeightedReturn: toFiniteNumberOrNull(
1296
+ breadth.equalWeightedReturn
1297
+ ),
1298
+ volumeWeightedReturn: toFiniteNumberOrNull(
1299
+ breadth.volumeWeightedReturn
1300
+ ),
1301
+ dispersion: toFiniteNumberOrNull(breadth.dispersion)
1302
+ },
1303
+ btcAltRegime: {
1304
+ source: "binance_klines",
1305
+ universe: breadth.universe,
1306
+ interval,
1307
+ asOfTs: breadth.ts.getTime(),
1308
+ ageMs: breadth.ageMs,
1309
+ stale: breadth.stale,
1310
+ btcReturn1h: toFiniteNumberOrNull(breadth.btcReturn1h),
1311
+ btcReturn4h: toFiniteNumberOrNull(breadth.btcReturn4h),
1312
+ btcReturn24h: toFiniteNumberOrNull(breadth.btcReturn24h),
1313
+ altBasketReturn1h: toFiniteNumberOrNull(
1314
+ breadth.altBasketReturn1h
1315
+ ),
1316
+ altBasketReturn4h: toFiniteNumberOrNull(
1317
+ breadth.altBasketReturn4h
1318
+ ),
1319
+ altBasketReturn24h: toFiniteNumberOrNull(
1320
+ breadth.altBasketReturn24h
1321
+ ),
1322
+ btcVsAltReturn1h: toFiniteNumberOrNull(
1323
+ breadth.btcVsAltReturn1h
1324
+ ),
1325
+ btcVsAltReturn4h: toFiniteNumberOrNull(
1326
+ breadth.btcVsAltReturn4h
1327
+ ),
1328
+ btcVsAltReturn24h: toFiniteNumberOrNull(
1329
+ breadth.btcVsAltReturn24h
1330
+ ),
1331
+ btcTurnoverShare1h: toFiniteNumberOrNull(
1332
+ breadth.btcTurnoverShare1h
1333
+ ),
1334
+ btcTurnoverShare24h: toFiniteNumberOrNull(
1335
+ breadth.btcTurnoverShare24h
1336
+ ),
1337
+ btcTurnoverShareChange24h: toFiniteNumberOrNull(
1338
+ breadth.btcTurnoverShareChange24h
1339
+ ),
1340
+ altVolToBtcVol24h: toFiniteNumberOrNull(
1341
+ breadth.altVolToBtcVol24h
1342
+ ),
1343
+ altDispersion24h: toFiniteNumberOrNull(
1344
+ breadth.altDispersion24h
1345
+ ),
1346
+ regime: breadth.btcAltRegime ?? "unknown"
1347
+ }
1348
+ } : {}
1349
+ }
1350
+ }
735
1351
  };
1352
+ (0, import_strategies.refreshSignalBaseContextGateFeatures)(signal);
736
1353
  return true;
737
1354
  } catch (error) {
738
- derivativesContextUnavailable = true;
1355
+ binanceMarketContextUnavailable = true;
739
1356
  import_logger3.logger.warn(
740
- "Derivatives context disabled after Timescale read failure: %s",
1357
+ "Binance market context disabled after Timescale read failure: %s",
741
1358
  String(error)
742
1359
  );
743
1360
  return false;
744
1361
  }
745
1362
  };
746
1363
 
747
- // src/strategyAdapters/ml.ts
748
- var defaultMlAdapter = {
749
- normalizeStrategyConfig: (strategyConfig) => strategyConfig
1364
+ // src/strategyHelpers/coinMarketCapContext.ts
1365
+ var import_strategies2 = require("@tradejs/core/strategies");
1366
+ var import_logger4 = require("@tradejs/infra/logger");
1367
+ var import_timescale2 = require("@tradejs/infra/timescale");
1368
+ var DEFAULT_MAX_AGE_MS = 48 * 60 * 6e4;
1369
+ var SOURCE_GLOBAL_DAILY = "coinmarketcap_global";
1370
+ var SOURCE_REFERENCE = "coinmarketcap_reference_asset";
1371
+ var SOURCE_EXCHANGE_LIQUIDITY = "coinmarketcap_exchange_liquidity";
1372
+ var SOURCE_FEAR_GREED = "coinmarketcap_fear_greed";
1373
+ var SOURCE_INDEX = "coinmarketcap_index";
1374
+ var DAY_MS = 864e5;
1375
+ var coinMarketCapContextUnavailable = false;
1376
+ var globalContextCache = /* @__PURE__ */ new Map();
1377
+ var referenceContextCache = /* @__PURE__ */ new Map();
1378
+ var exchangeLiquidityContextCache = /* @__PURE__ */ new Map();
1379
+ var fearGreedContextCache = /* @__PURE__ */ new Map();
1380
+ var indexContextCache = /* @__PURE__ */ new Map();
1381
+ var parseEnabledFlag2 = (value, env) => {
1382
+ const normalized = String(value ?? "").trim().toLowerCase();
1383
+ if (!normalized) {
1384
+ return env === "BACKTEST" || env === "PARITY" || env === "CRON";
1385
+ }
1386
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
1387
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
1388
+ if (normalized === "backtest") return env === "BACKTEST";
1389
+ if (normalized === "live") return env !== "BACKTEST";
1390
+ return false;
750
1391
  };
751
- var getStrategyMlAdapter = (strategy) => {
752
- const strategyAdapter = getStrategyManifest(strategy)?.mlAdapter;
753
- if (!strategyAdapter) return defaultMlAdapter;
754
- return {
755
- ...defaultMlAdapter,
756
- ...strategyAdapter
757
- };
1392
+ var asInt = (value, fallback) => {
1393
+ const parsed = Number.parseInt(String(value ?? ""), 10);
1394
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
758
1395
  };
759
-
760
- // src/mlPayload.ts
761
- var normalizeStrategyConfig = (strategyConfig, strategyName) => {
762
- return getStrategyMlAdapter(strategyName).normalizeStrategyConfig?.(
763
- strategyConfig
764
- );
1396
+ var toFiniteNumberOrNull2 = (value) => {
1397
+ const numeric = typeof value === "number" ? value : typeof value === "string" && value.trim() ? Number(value) : Number.NaN;
1398
+ return Number.isFinite(numeric) ? numeric : null;
765
1399
  };
766
- var buildMlPayload = (payload) => {
767
- const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
768
- const mlAdapter = getStrategyMlAdapter(strategyName);
769
- const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
770
- const nextSignal = {
771
- ...normalizedSignal,
772
- indicators: {
773
- ...normalizedSignal?.indicators ?? {}
774
- }
775
- };
776
- const nextContext = payload.context ? {
777
- ...payload.context,
778
- strategyConfig: normalizeStrategyConfig(
779
- payload.context.strategyConfig,
780
- strategyName
781
- )
782
- } : void 0;
783
- return {
784
- signal: nextSignal,
785
- context: nextContext
786
- };
1400
+ var safeDivide = (numerator, denominator) => numerator != null && denominator != null && denominator > 0 ? numerator / denominator : null;
1401
+ var hasBaseContext2 = (signal) => Boolean(
1402
+ signal.additionalIndicators?.baseContext && typeof signal.additionalIndicators.baseContext === "object" && !Array.isArray(signal.additionalIndicators.baseContext)
1403
+ );
1404
+ var resolveMaxAgeMs = () => asInt(process.env.COINMARKETCAP_CONTEXT_MAX_AGE_MS, DEFAULT_MAX_AGE_MS);
1405
+ var toAltLiquidityRegime = ({
1406
+ stale,
1407
+ btcDominanceChange24hPct,
1408
+ altMarketCapChange24hPct,
1409
+ altVolumeChange24hPct
1410
+ }) => {
1411
+ if (stale) return "unknown";
1412
+ if (altMarketCapChange24hPct != null && altMarketCapChange24hPct <= -0.03 || altVolumeChange24hPct != null && altVolumeChange24hPct <= -0.15) {
1413
+ return "risk_off";
1414
+ }
1415
+ if (btcDominanceChange24hPct != null && btcDominanceChange24hPct >= 0.25) {
1416
+ return "btc_favored";
1417
+ }
1418
+ if (btcDominanceChange24hPct != null && btcDominanceChange24hPct <= -0.25 && (altMarketCapChange24hPct == null || altMarketCapChange24hPct >= 0)) {
1419
+ return "alt_friendly";
1420
+ }
1421
+ return "neutral";
787
1422
  };
788
-
789
- // src/connectorsRegistry.ts
790
- var import_logger4 = require("@tradejs/infra/logger");
791
- var createConnectorRegistryState = () => ({
792
- connectorCreators: /* @__PURE__ */ new Map(),
793
- providerToConnectorName: /* @__PURE__ */ new Map(),
794
- pluginsLoadPromise: null
795
- });
796
- var registryStateByProjectRoot2 = /* @__PURE__ */ new Map();
797
- var getConnectorRegistryState = (cwd = getTradejsProjectCwd()) => {
798
- const projectRoot = getTradejsProjectCwd(cwd);
799
- let state = registryStateByProjectRoot2.get(projectRoot);
800
- if (!state) {
801
- state = createConnectorRegistryState();
802
- registryStateByProjectRoot2.set(projectRoot, state);
1423
+ var toReferenceLiquidityRegime = ({
1424
+ stale,
1425
+ ethBtcMarketCapRatioChange24hPct,
1426
+ ethVsBtcVolumeRatio
1427
+ }) => {
1428
+ if (stale) return "unknown";
1429
+ if (ethVsBtcVolumeRatio != null && ethVsBtcVolumeRatio < 0.15) return "thin";
1430
+ if (ethBtcMarketCapRatioChange24hPct != null && ethBtcMarketCapRatioChange24hPct >= 0.01) {
1431
+ return "eth_led";
803
1432
  }
804
- return {
1433
+ if (ethBtcMarketCapRatioChange24hPct != null && ethBtcMarketCapRatioChange24hPct <= -0.01) {
1434
+ return "btc_led";
1435
+ }
1436
+ return "balanced";
1437
+ };
1438
+ var toExchangeLiquidityRegime = ({
1439
+ stale,
1440
+ totalVolumeChange24hPct,
1441
+ fallback
1442
+ }) => {
1443
+ if (stale) return "unknown";
1444
+ if (totalVolumeChange24hPct != null && totalVolumeChange24hPct >= 0.15) {
1445
+ return "expanding";
1446
+ }
1447
+ if (totalVolumeChange24hPct != null && totalVolumeChange24hPct <= -0.15) {
1448
+ return "contracting";
1449
+ }
1450
+ return fallback;
1451
+ };
1452
+ var toIndexRegime = ({
1453
+ stale,
1454
+ cmc100Change24hPct,
1455
+ cmc20Change24hPct,
1456
+ cmc20ToCmc100RatioChange24hPct
1457
+ }) => {
1458
+ if (stale) return "unknown";
1459
+ if (cmc100Change24hPct == null && cmc20Change24hPct == null) {
1460
+ return "unknown";
1461
+ }
1462
+ if ((cmc100Change24hPct ?? 0) <= -0.02 && (cmc20Change24hPct ?? 0) <= -0.02) {
1463
+ return "risk_off";
1464
+ }
1465
+ if ((cmc20ToCmc100RatioChange24hPct ?? 0) >= 5e-3) {
1466
+ return "top20_led";
1467
+ }
1468
+ if ((cmc20ToCmc100RatioChange24hPct ?? 0) <= -5e-3) {
1469
+ return "large_cap_led";
1470
+ }
1471
+ return "balanced";
1472
+ };
1473
+ var getCachedGlobalContext = ({
1474
+ timestamp,
1475
+ maxAgeMs
1476
+ }) => {
1477
+ const key = `${SOURCE_GLOBAL_DAILY}:${timestamp}:${maxAgeMs}`;
1478
+ const cached = globalContextCache.get(key);
1479
+ if (cached) return cached;
1480
+ const promise = (0, import_timescale2.getLatestMarketGlobalContext)({
1481
+ source: SOURCE_GLOBAL_DAILY,
1482
+ atMs: timestamp,
1483
+ maxAgeMs
1484
+ });
1485
+ globalContextCache.set(key, promise);
1486
+ return promise;
1487
+ };
1488
+ var getCachedReferenceContexts = ({
1489
+ timestamp,
1490
+ maxAgeMs
1491
+ }) => {
1492
+ const key = `${SOURCE_REFERENCE}:1d:${timestamp}:${maxAgeMs}`;
1493
+ const cached = referenceContextCache.get(key);
1494
+ if (cached) return cached;
1495
+ const promise = (0, import_timescale2.getLatestMarketReferenceAssetContexts)({
1496
+ source: SOURCE_REFERENCE,
1497
+ symbols: ["BTCUSDT", "ETHUSDT"],
1498
+ interval: "1d",
1499
+ atMs: timestamp,
1500
+ maxAgeMs
1501
+ });
1502
+ referenceContextCache.set(key, promise);
1503
+ return promise;
1504
+ };
1505
+ var getCachedExchangeLiquidityContext = ({
1506
+ timestamp,
1507
+ maxAgeMs
1508
+ }) => {
1509
+ const key = `${SOURCE_EXCHANGE_LIQUIDITY}:1d:${timestamp}:${maxAgeMs}`;
1510
+ const cached = exchangeLiquidityContextCache.get(key);
1511
+ if (cached) return cached;
1512
+ const promise = (0, import_timescale2.getLatestMarketCmcExchangeLiquidityContext)({
1513
+ source: SOURCE_EXCHANGE_LIQUIDITY,
1514
+ interval: "1d",
1515
+ atMs: timestamp,
1516
+ maxAgeMs
1517
+ });
1518
+ exchangeLiquidityContextCache.set(key, promise);
1519
+ return promise;
1520
+ };
1521
+ var getCachedFearGreedContext = ({
1522
+ timestamp,
1523
+ maxAgeMs
1524
+ }) => {
1525
+ const key = `${SOURCE_FEAR_GREED}:1d:${timestamp}:${maxAgeMs}`;
1526
+ const cached = fearGreedContextCache.get(key);
1527
+ if (cached) return cached;
1528
+ const promise = (0, import_timescale2.getLatestMarketCmcFearGreedContext)({
1529
+ source: SOURCE_FEAR_GREED,
1530
+ interval: "1d",
1531
+ atMs: timestamp,
1532
+ maxAgeMs
1533
+ });
1534
+ fearGreedContextCache.set(key, promise);
1535
+ return promise;
1536
+ };
1537
+ var getCachedIndexContexts = ({
1538
+ timestamp,
1539
+ maxAgeMs
1540
+ }) => {
1541
+ const key = `${SOURCE_INDEX}:1d:${timestamp}:${maxAgeMs}`;
1542
+ const cached = indexContextCache.get(key);
1543
+ if (cached) return cached;
1544
+ const promise = (0, import_timescale2.getLatestMarketCmcIndexContexts)({
1545
+ source: SOURCE_INDEX,
1546
+ indexSlugs: ["cmc100", "cmc20"],
1547
+ interval: "1d",
1548
+ atMs: timestamp,
1549
+ maxAgeMs
1550
+ });
1551
+ indexContextCache.set(key, promise);
1552
+ return promise;
1553
+ };
1554
+ var isCoinMarketCapContextEnabled = (env) => parseEnabledFlag2(process.env.COINMARKETCAP_CONTEXT_ENABLED, env);
1555
+ var enrichSignalWithCoinMarketCapContext = async (params) => {
1556
+ const {
1557
+ signal,
1558
+ env,
1559
+ enabled = isCoinMarketCapContextEnabled(env),
1560
+ maxAgeMs = resolveMaxAgeMs()
1561
+ } = params;
1562
+ if (signal.universe === "tradfi" || !enabled || coinMarketCapContextUnavailable || !hasBaseContext2(signal)) {
1563
+ return false;
1564
+ }
1565
+ try {
1566
+ const [
1567
+ globalDailyRow,
1568
+ dailyReferences,
1569
+ previousDailyReferences,
1570
+ exchangeLiquidityRow,
1571
+ fearGreedRow,
1572
+ indexRows
1573
+ ] = await Promise.all([
1574
+ getCachedGlobalContext({
1575
+ timestamp: signal.timestamp,
1576
+ maxAgeMs
1577
+ }),
1578
+ getCachedReferenceContexts({
1579
+ timestamp: signal.timestamp,
1580
+ maxAgeMs
1581
+ }),
1582
+ getCachedReferenceContexts({
1583
+ timestamp: signal.timestamp - DAY_MS,
1584
+ maxAgeMs: maxAgeMs + DAY_MS
1585
+ }),
1586
+ getCachedExchangeLiquidityContext({
1587
+ timestamp: signal.timestamp,
1588
+ maxAgeMs
1589
+ }),
1590
+ getCachedFearGreedContext({
1591
+ timestamp: signal.timestamp,
1592
+ maxAgeMs
1593
+ }),
1594
+ getCachedIndexContexts({
1595
+ timestamp: signal.timestamp,
1596
+ maxAgeMs
1597
+ })
1598
+ ]);
1599
+ const globalRow = globalDailyRow;
1600
+ const references = dailyReferences;
1601
+ const previousReferences = previousDailyReferences;
1602
+ if (!globalRow && !references.size && !exchangeLiquidityRow && !fearGreedRow && !indexRows.size) {
1603
+ return false;
1604
+ }
1605
+ const btcRow = references.get("BTCUSDT") ?? null;
1606
+ const ethRow = references.get("ETHUSDT") ?? null;
1607
+ const previousBtcRow = previousReferences.get("BTCUSDT") ?? null;
1608
+ const previousEthRow = previousReferences.get("ETHUSDT") ?? null;
1609
+ const btcMarketCapUsd = toFiniteNumberOrNull2(btcRow?.marketCapUsd);
1610
+ const ethMarketCapUsd = toFiniteNumberOrNull2(ethRow?.marketCapUsd);
1611
+ const previousBtcMarketCapUsd = toFiniteNumberOrNull2(
1612
+ previousBtcRow?.marketCapUsd
1613
+ );
1614
+ const previousEthMarketCapUsd = toFiniteNumberOrNull2(
1615
+ previousEthRow?.marketCapUsd
1616
+ );
1617
+ const ethBtcMarketCapRatio = safeDivide(ethMarketCapUsd, btcMarketCapUsd);
1618
+ const previousEthBtcMarketCapRatio = safeDivide(
1619
+ previousEthMarketCapUsd,
1620
+ previousBtcMarketCapUsd
1621
+ );
1622
+ const ethBtcMarketCapRatioChange24hPct = ethBtcMarketCapRatio != null && previousEthBtcMarketCapRatio != null && previousEthBtcMarketCapRatio > 0 ? (ethBtcMarketCapRatio - previousEthBtcMarketCapRatio) / previousEthBtcMarketCapRatio : null;
1623
+ const btcVolumeUsd = toFiniteNumberOrNull2(btcRow?.volumeUsd);
1624
+ const ethVolumeUsd = toFiniteNumberOrNull2(ethRow?.volumeUsd);
1625
+ const referenceStale = btcRow?.stale === true || ethRow?.stale === true || !btcRow || !ethRow;
1626
+ const btcDominanceChange24hPct = toFiniteNumberOrNull2(
1627
+ globalRow?.btcDominanceChange24hPct
1628
+ );
1629
+ const altMarketCapChange24hPct = toFiniteNumberOrNull2(
1630
+ globalRow?.altMarketCapChange24hPct
1631
+ );
1632
+ const altVolumeChange24hPct = toFiniteNumberOrNull2(
1633
+ globalRow?.altVolumeChange24hPct
1634
+ );
1635
+ const altLiquidityRegime = globalRow ? toAltLiquidityRegime({
1636
+ stale: globalRow.stale,
1637
+ btcDominanceChange24hPct,
1638
+ altMarketCapChange24hPct,
1639
+ altVolumeChange24hPct
1640
+ }) : "unknown";
1641
+ const exchangeLiquidityRegime = exchangeLiquidityRow ? toExchangeLiquidityRegime({
1642
+ stale: exchangeLiquidityRow.stale,
1643
+ totalVolumeChange24hPct: toFiniteNumberOrNull2(
1644
+ exchangeLiquidityRow.totalVolumeChange24hPct
1645
+ ),
1646
+ fallback: exchangeLiquidityRow.liquidityRegime ?? "unknown"
1647
+ }) : "unknown";
1648
+ const cmc100Row = indexRows.get("cmc100") ?? null;
1649
+ const cmc20Row = indexRows.get("cmc20") ?? null;
1650
+ const cmc100Value = toFiniteNumberOrNull2(cmc100Row?.value);
1651
+ const cmc20Value = toFiniteNumberOrNull2(cmc20Row?.value);
1652
+ const cmc100Change24hPct = toFiniteNumberOrNull2(
1653
+ cmc100Row?.valueChange24hPct
1654
+ );
1655
+ const cmc20Change24hPct = toFiniteNumberOrNull2(cmc20Row?.valueChange24hPct);
1656
+ const cmc20ToCmc100Ratio = safeDivide(cmc20Value, cmc100Value);
1657
+ const cmc20ToCmc100RatioChange24hPct = cmc20Change24hPct != null && cmc100Change24hPct != null ? (1 + cmc20Change24hPct) / (1 + cmc100Change24hPct) - 1 : null;
1658
+ const indexStale = cmc100Row?.stale === true || cmc20Row?.stale === true || !cmc100Row || !cmc20Row;
1659
+ const indexRegime = toIndexRegime({
1660
+ stale: indexStale,
1661
+ cmc100Change24hPct,
1662
+ cmc20Change24hPct,
1663
+ cmc20ToCmc100RatioChange24hPct
1664
+ });
1665
+ const baseContext = signal.additionalIndicators.baseContext;
1666
+ signal.additionalIndicators = {
1667
+ ...signal.additionalIndicators,
1668
+ baseContext: {
1669
+ ...baseContext,
1670
+ relative: {
1671
+ ...baseContext.relative,
1672
+ ...globalRow ? {
1673
+ cmcGlobal: {
1674
+ source: globalRow.source,
1675
+ interval: "1d",
1676
+ asOfTs: globalRow.ts.getTime(),
1677
+ ageMs: globalRow.ageMs,
1678
+ stale: globalRow.stale,
1679
+ totalMarketCapUsd: toFiniteNumberOrNull2(
1680
+ globalRow.totalMarketCapUsd
1681
+ ),
1682
+ totalVolumeUsd: toFiniteNumberOrNull2(
1683
+ globalRow.totalVolumeUsd
1684
+ ),
1685
+ totalVolumeReportedUsd: toFiniteNumberOrNull2(
1686
+ globalRow.totalVolumeReportedUsd
1687
+ ),
1688
+ altMarketCapUsd: toFiniteNumberOrNull2(
1689
+ globalRow.altMarketCapUsd
1690
+ ),
1691
+ altVolumeUsd: toFiniteNumberOrNull2(globalRow.altVolumeUsd),
1692
+ altVolumeReportedUsd: toFiniteNumberOrNull2(
1693
+ globalRow.altVolumeReportedUsd
1694
+ ),
1695
+ btcDominancePct: toFiniteNumberOrNull2(
1696
+ globalRow.btcDominancePct
1697
+ ),
1698
+ ethDominancePct: toFiniteNumberOrNull2(
1699
+ globalRow.ethDominancePct
1700
+ ),
1701
+ btcDominanceChange24hPct,
1702
+ ethDominanceChange24hPct: toFiniteNumberOrNull2(
1703
+ globalRow.ethDominanceChange24hPct
1704
+ ),
1705
+ altMarketCapChange24hPct,
1706
+ altVolumeChange24hPct,
1707
+ activeCryptocurrencies: toFiniteNumberOrNull2(
1708
+ globalRow.activeCryptocurrencies
1709
+ ),
1710
+ activeExchanges: toFiniteNumberOrNull2(
1711
+ globalRow.activeExchanges
1712
+ ),
1713
+ activeMarketPairs: toFiniteNumberOrNull2(
1714
+ globalRow.activeMarketPairs
1715
+ ),
1716
+ altLiquidityRegime
1717
+ }
1718
+ } : {},
1719
+ ...btcRow || ethRow ? {
1720
+ cmcReferenceAssets: {
1721
+ source: SOURCE_REFERENCE,
1722
+ interval: "1d",
1723
+ asOfTs: Math.max(
1724
+ btcRow?.ts.getTime() ?? 0,
1725
+ ethRow?.ts.getTime() ?? 0
1726
+ ),
1727
+ ageMs: btcRow?.ageMs != null && ethRow?.ageMs != null ? Math.max(btcRow.ageMs, ethRow.ageMs) : btcRow?.ageMs ?? ethRow?.ageMs ?? null,
1728
+ stale: referenceStale,
1729
+ btcMarketCapUsd,
1730
+ ethMarketCapUsd,
1731
+ btcVolumeUsd,
1732
+ ethVolumeUsd,
1733
+ btcVolumeToMarketCap: safeDivide(
1734
+ btcVolumeUsd,
1735
+ btcMarketCapUsd
1736
+ ),
1737
+ ethVolumeToMarketCap: safeDivide(
1738
+ ethVolumeUsd,
1739
+ ethMarketCapUsd
1740
+ ),
1741
+ ethBtcMarketCapRatio,
1742
+ ethBtcMarketCapRatioChange24hPct,
1743
+ ethVsBtcVolumeRatio: safeDivide(ethVolumeUsd, btcVolumeUsd),
1744
+ referenceLiquidityRegime: toReferenceLiquidityRegime({
1745
+ stale: referenceStale,
1746
+ ethBtcMarketCapRatioChange24hPct,
1747
+ ethVsBtcVolumeRatio: safeDivide(ethVolumeUsd, btcVolumeUsd)
1748
+ })
1749
+ }
1750
+ } : {},
1751
+ ...exchangeLiquidityRow ? {
1752
+ cmcExchangeLiquidity: {
1753
+ source: SOURCE_EXCHANGE_LIQUIDITY,
1754
+ interval: exchangeLiquidityRow.interval,
1755
+ asOfTs: exchangeLiquidityRow.ts.getTime(),
1756
+ ageMs: exchangeLiquidityRow.ageMs,
1757
+ stale: exchangeLiquidityRow.stale,
1758
+ exchangesCount: toFiniteNumberOrNull2(
1759
+ exchangeLiquidityRow.exchangesCount
1760
+ ),
1761
+ totalVolumeUsd: toFiniteNumberOrNull2(
1762
+ exchangeLiquidityRow.totalVolumeUsd
1763
+ ),
1764
+ totalVolumeChange24hPct: toFiniteNumberOrNull2(
1765
+ exchangeLiquidityRow.totalVolumeChange24hPct
1766
+ ),
1767
+ binanceVolumeUsd: toFiniteNumberOrNull2(
1768
+ exchangeLiquidityRow.binanceVolumeUsd
1769
+ ),
1770
+ binanceVolumeShare: toFiniteNumberOrNull2(
1771
+ exchangeLiquidityRow.binanceVolumeShare
1772
+ ),
1773
+ topExchangeVolumeShare: toFiniteNumberOrNull2(
1774
+ exchangeLiquidityRow.topExchangeVolumeShare
1775
+ ),
1776
+ liquidityRegime: exchangeLiquidityRegime
1777
+ }
1778
+ } : {},
1779
+ ...fearGreedRow ? {
1780
+ cmcFearGreed: {
1781
+ source: SOURCE_FEAR_GREED,
1782
+ interval: "1d",
1783
+ asOfTs: fearGreedRow.ts.getTime(),
1784
+ ageMs: fearGreedRow.ageMs,
1785
+ stale: fearGreedRow.stale,
1786
+ value: toFiniteNumberOrNull2(fearGreedRow.value),
1787
+ valueChange24h: toFiniteNumberOrNull2(
1788
+ fearGreedRow.valueChange24h
1789
+ ),
1790
+ valueChange7d: toFiniteNumberOrNull2(
1791
+ fearGreedRow.valueChange7d
1792
+ ),
1793
+ classification: fearGreedRow.classification ?? "Unknown",
1794
+ sentimentRegime: fearGreedRow.sentimentRegime ?? "unknown"
1795
+ }
1796
+ } : {},
1797
+ ...cmc100Row || cmc20Row ? {
1798
+ cmcIndexes: {
1799
+ source: SOURCE_INDEX,
1800
+ interval: "1d",
1801
+ asOfTs: Math.max(
1802
+ cmc100Row?.ts.getTime() ?? 0,
1803
+ cmc20Row?.ts.getTime() ?? 0
1804
+ ),
1805
+ ageMs: cmc100Row?.ageMs != null && cmc20Row?.ageMs != null ? Math.max(cmc100Row.ageMs, cmc20Row.ageMs) : cmc100Row?.ageMs ?? cmc20Row?.ageMs ?? null,
1806
+ stale: indexStale,
1807
+ cmc100Value,
1808
+ cmc100Change24hPct,
1809
+ cmc100TopConstituentSymbol: cmc100Row?.topConstituentSymbol ?? null,
1810
+ cmc100TopConstituentWeightPct: toFiniteNumberOrNull2(
1811
+ cmc100Row?.topConstituentWeightPct
1812
+ ),
1813
+ cmc20Value,
1814
+ cmc20Change24hPct,
1815
+ cmc20TopConstituentSymbol: cmc20Row?.topConstituentSymbol ?? null,
1816
+ cmc20TopConstituentWeightPct: toFiniteNumberOrNull2(
1817
+ cmc20Row?.topConstituentWeightPct
1818
+ ),
1819
+ cmc20ToCmc100Ratio,
1820
+ cmc20ToCmc100RatioChange24hPct,
1821
+ indexRegime
1822
+ }
1823
+ } : {}
1824
+ }
1825
+ }
1826
+ };
1827
+ (0, import_strategies2.refreshSignalBaseContextGateFeatures)(signal);
1828
+ return true;
1829
+ } catch (error) {
1830
+ coinMarketCapContextUnavailable = true;
1831
+ import_logger4.logger.warn(
1832
+ "CoinMarketCap context disabled after Timescale read failure: %s",
1833
+ String(error)
1834
+ );
1835
+ return false;
1836
+ }
1837
+ };
1838
+
1839
+ // src/strategyHelpers/derivativesContext.ts
1840
+ var import_indicators2 = require("@tradejs/core/indicators");
1841
+ var import_strategies3 = require("@tradejs/core/strategies");
1842
+ var import_constants = require("@tradejs/core/constants");
1843
+ var import_timescale3 = require("@tradejs/infra/timescale");
1844
+ var import_logger5 = require("@tradejs/infra/logger");
1845
+ var DEFAULT_INTERVALS = ["15m", "1h"];
1846
+ var DEFAULT_LOOKBACK_HOURS = 48;
1847
+ var PRIMARY_DERIVATIVES_REFERENCE_SYMBOL = import_constants.DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS[0];
1848
+ var SECONDARY_DERIVATIVES_REFERENCE_SYMBOL = import_constants.DERIVATIVES_CONTEXT_BASE_REFERENCE_SYMBOLS[1];
1849
+ var derivativesContextUnavailable = false;
1850
+ var parseEnabledFlag3 = (value, env) => {
1851
+ const normalized = String(value ?? "").trim().toLowerCase();
1852
+ if (!normalized) return true;
1853
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
1854
+ if (normalized === "backtest") return env === "BACKTEST";
1855
+ if (normalized === "live") return env !== "BACKTEST";
1856
+ return false;
1857
+ };
1858
+ var parseBooleanFlag = (value, fallback = false) => {
1859
+ const normalized = String(value ?? "").trim().toLowerCase();
1860
+ if (!normalized) return fallback;
1861
+ if (["1", "true", "yes", "on"].includes(normalized)) return true;
1862
+ if (["0", "false", "no", "off"].includes(normalized)) return false;
1863
+ return fallback;
1864
+ };
1865
+ var parseLookbackMs = () => {
1866
+ const hours = Number(process.env.DERIVATIVES_CONTEXT_LOOKBACK_HOURS);
1867
+ const normalizedHours = Number.isFinite(hours) && hours > 0 ? hours : DEFAULT_LOOKBACK_HOURS;
1868
+ return normalizedHours * 60 * 60 * 1e3;
1869
+ };
1870
+ var parseIntervals = () => {
1871
+ const fromEnv = (0, import_indicators2.normalizeDerivativesIntervals)(
1872
+ process.env.DERIVATIVES_CONTEXT_INTERVALS
1873
+ );
1874
+ return fromEnv.length ? fromEnv : DEFAULT_INTERVALS;
1875
+ };
1876
+ var getDerivativesContextReferenceSymbols = () => [
1877
+ ...(0, import_constants.resolveDerivativesContextReferenceSymbols)(
1878
+ process.env.DERIVATIVES_CONTEXT_EXTRA_REFERENCE_SYMBOLS
1879
+ )
1880
+ ];
1881
+ var normalizeSymbol = (symbol) => String(symbol || "").trim().toUpperCase();
1882
+ var getSignalPriceChangePct1h = (signal) => {
1883
+ const baseContext = signal.additionalIndicators?.baseContext;
1884
+ if (!baseContext || typeof baseContext !== "object" || Array.isArray(baseContext)) {
1885
+ return null;
1886
+ }
1887
+ const raw = typeof baseContext.raw === "object" && baseContext.raw && !Array.isArray(baseContext.raw) ? baseContext.raw : null;
1888
+ const price = raw && typeof raw.price === "object" && raw.price && !Array.isArray(raw.price) ? raw.price : null;
1889
+ const value = price?.price1hPct;
1890
+ const numeric = typeof value === "number" ? value : Number(value);
1891
+ return Number.isFinite(numeric) ? numeric : null;
1892
+ };
1893
+ var resolvePrimaryReferenceSymbol2 = () => PRIMARY_DERIVATIVES_REFERENCE_SYMBOL;
1894
+ var resolveSecondaryReferenceSymbol = () => SECONDARY_DERIVATIVES_REFERENCE_SYMBOL;
1895
+ var getPrimaryIntervalContext = (context) => context?.intervals["15m"] ?? context?.intervals["1h"] ?? null;
1896
+ var hasDerivativesSymbolData = (context) => Object.keys(context.intervals).length > 0 && !context.summary.riskFlags.includes("missing_derivatives");
1897
+ var toFiniteNumberOrNull3 = (value) => {
1898
+ if (typeof value === "number" && Number.isFinite(value)) return value;
1899
+ if (typeof value === "string" && value.trim()) {
1900
+ const parsed = Number(value);
1901
+ return Number.isFinite(parsed) ? parsed : null;
1902
+ }
1903
+ return null;
1904
+ };
1905
+ var roundNullable = (value, digits = 4) => {
1906
+ if (value == null || !Number.isFinite(value)) return null;
1907
+ const multiplier = 10 ** digits;
1908
+ return Math.round(value * multiplier) / multiplier;
1909
+ };
1910
+ var deltaNullable = (targetValue, referenceValue) => {
1911
+ const target = toFiniteNumberOrNull3(targetValue);
1912
+ const reference = toFiniteNumberOrNull3(referenceValue);
1913
+ return target == null || reference == null ? null : roundNullable(target - reference);
1914
+ };
1915
+ var buildTargetDerivedContext = (params) => {
1916
+ const { targetContext, primaryReferenceContext } = params;
1917
+ const targetPrimary = getPrimaryIntervalContext(targetContext);
1918
+ const referencePrimary = getPrimaryIntervalContext(primaryReferenceContext);
1919
+ const targetDirectionAligned = targetContext.summary.directionAligned;
1920
+ const referenceDirectionAligned = primaryReferenceContext?.summary.directionAligned ?? null;
1921
+ return {
1922
+ available: hasDerivativesSymbolData(targetContext),
1923
+ stale: targetContext.summary.riskFlags.includes("stale_derivatives") || targetPrimary?.stale === true ? true : targetPrimary == null ? null : false,
1924
+ sourceSymbol: targetContext.symbol,
1925
+ referenceSymbol: primaryReferenceContext?.symbol ?? null,
1926
+ directionAligned: targetDirectionAligned,
1927
+ referenceDirectionAligned,
1928
+ pressure: targetContext.summary.pressure ?? null,
1929
+ referencePressure: primaryReferenceContext?.summary.pressure ?? null,
1930
+ riskFlags: targetContext.summary.riskFlags,
1931
+ oiChangePct1h: targetPrimary?.oiChangePct1h ?? null,
1932
+ oiAcceleration: targetContext.summary.oiAcceleration ?? null,
1933
+ fundingRate: targetPrimary?.fundingRate ?? null,
1934
+ fundingZScore: targetPrimary?.fundingZScore ?? null,
1935
+ fundingChange1h: targetContext.summary.fundingChange1h ?? null,
1936
+ liqSpikeRatio: targetPrimary?.liqSpikeRatio ?? null,
1937
+ liqImbalance: targetPrimary?.liqImbalance ?? null,
1938
+ targetVsPrimaryOiChangePct1hDelta: deltaNullable(
1939
+ targetPrimary?.oiChangePct1h,
1940
+ referencePrimary?.oiChangePct1h
1941
+ ),
1942
+ targetVsPrimaryFundingZScoreDelta: deltaNullable(
1943
+ targetPrimary?.fundingZScore,
1944
+ referencePrimary?.fundingZScore
1945
+ ),
1946
+ targetReferenceConflict: targetDirectionAligned == null || referenceDirectionAligned == null ? null : targetDirectionAligned !== referenceDirectionAligned
1947
+ };
1948
+ };
1949
+ var buildReferenceDerivativesContext = (params) => {
1950
+ const {
1951
+ targetSymbol,
1952
+ primaryReferenceSymbol,
1953
+ secondaryReferenceSymbol,
1954
+ referenceSymbols,
1955
+ referenceContexts,
1956
+ targetContext
1957
+ } = params;
1958
+ const primaryContext = referenceContexts[primaryReferenceSymbol] ?? referenceContexts[referenceSymbols[0]];
1959
+ if (!primaryContext) {
1960
+ throw new Error("No derivatives reference contexts built");
1961
+ }
1962
+ const referenceSymbolsMetadata = [
1963
+ .../* @__PURE__ */ new Set([
1964
+ primaryReferenceSymbol,
1965
+ secondaryReferenceSymbol,
1966
+ ...referenceSymbols,
1967
+ ...Object.keys(referenceContexts)
1968
+ ])
1969
+ ];
1970
+ const targetDerived = targetContext && hasDerivativesSymbolData(targetContext) ? buildTargetDerivedContext({
1971
+ targetContext,
1972
+ primaryReferenceContext: primaryContext
1973
+ }) : void 0;
1974
+ return {
1975
+ ...primaryContext,
1976
+ targetSymbol,
1977
+ primaryReferenceSymbol: primaryContext.symbol,
1978
+ secondaryReferenceSymbol: referenceContexts[secondaryReferenceSymbol]?.symbol ?? secondaryReferenceSymbol,
1979
+ referenceSymbols: referenceSymbolsMetadata,
1980
+ referenceContexts,
1981
+ ...targetContext && targetDerived ? {
1982
+ targetContext,
1983
+ targetDerived
1984
+ } : {}
1985
+ };
1986
+ };
1987
+ var isDerivativesContextEnabled = (env) => parseEnabledFlag3(process.env.DERIVATIVES_CONTEXT_ENABLED, env);
1988
+ var isDerivativesTargetContextEnabled = () => parseBooleanFlag(process.env.DERIVATIVES_CONTEXT_TARGET_ENABLED, false);
1989
+ var enrichSignalWithDerivativesContext = async (params) => {
1990
+ const { signal, env, enabled = isDerivativesContextEnabled(env) } = params;
1991
+ if (signal.universe === "tradfi" || !enabled || derivativesContextUnavailable) {
1992
+ return false;
1993
+ }
1994
+ try {
1995
+ const intervals = parseIntervals();
1996
+ const referenceSymbols = getDerivativesContextReferenceSymbols();
1997
+ const targetSymbol = normalizeSymbol(signal.symbol);
1998
+ const lookbackMs = parseLookbackMs();
1999
+ const contexts = await Promise.all(
2000
+ referenceSymbols.map(async (symbol) => {
2001
+ const rowsByInterval = await (0, import_timescale3.getDerivativesWindow)({
2002
+ symbol,
2003
+ intervals,
2004
+ endMs: signal.timestamp,
2005
+ lookbackMs
2006
+ });
2007
+ return [
2008
+ symbol,
2009
+ (0, import_indicators2.buildDerivativesContext)({
2010
+ symbol,
2011
+ direction: signal.direction,
2012
+ timestamp: signal.timestamp,
2013
+ rowsByInterval,
2014
+ priceChangePct1h: getSignalPriceChangePct1h(signal),
2015
+ intervals
2016
+ })
2017
+ ];
2018
+ })
2019
+ );
2020
+ const referenceContexts = Object.fromEntries(contexts);
2021
+ const primaryReferenceSymbol = resolvePrimaryReferenceSymbol2();
2022
+ const secondaryReferenceSymbol = resolveSecondaryReferenceSymbol();
2023
+ const targetContextEnabled = isDerivativesTargetContextEnabled();
2024
+ const referenceTargetContext = targetContextEnabled && targetSymbol !== primaryReferenceSymbol ? referenceContexts[targetSymbol] : void 0;
2025
+ const shouldFetchTargetContext = targetContextEnabled && targetSymbol.length > 0 && !referenceSymbols.some(
2026
+ (referenceSymbol) => referenceSymbol === targetSymbol
2027
+ );
2028
+ const fetchedTargetContext = shouldFetchTargetContext ? await (async () => {
2029
+ const rowsByInterval = await (0, import_timescale3.getDerivativesWindow)({
2030
+ symbol: targetSymbol,
2031
+ intervals,
2032
+ endMs: signal.timestamp,
2033
+ lookbackMs
2034
+ });
2035
+ const context = (0, import_indicators2.buildDerivativesContext)({
2036
+ symbol: targetSymbol,
2037
+ direction: signal.direction,
2038
+ timestamp: signal.timestamp,
2039
+ rowsByInterval,
2040
+ priceChangePct1h: getSignalPriceChangePct1h(signal),
2041
+ intervals
2042
+ });
2043
+ return hasDerivativesSymbolData(context) ? context : void 0;
2044
+ })() : void 0;
2045
+ const targetContext = referenceTargetContext && hasDerivativesSymbolData(referenceTargetContext) ? referenceTargetContext : fetchedTargetContext;
2046
+ const derivativesContext = buildReferenceDerivativesContext({
2047
+ targetSymbol: targetSymbol || signal.symbol,
2048
+ primaryReferenceSymbol,
2049
+ secondaryReferenceSymbol,
2050
+ referenceSymbols,
2051
+ referenceContexts,
2052
+ targetContext
2053
+ });
2054
+ signal.additionalIndicators = {
2055
+ ...signal.additionalIndicators ?? {},
2056
+ baseContext: signal.additionalIndicators?.baseContext && typeof signal.additionalIndicators.baseContext === "object" && !Array.isArray(signal.additionalIndicators.baseContext) ? {
2057
+ ...signal.additionalIndicators.baseContext,
2058
+ derivatives: derivativesContext
2059
+ } : signal.additionalIndicators?.baseContext
2060
+ };
2061
+ (0, import_strategies3.refreshSignalBaseContextGateFeatures)(signal);
2062
+ return true;
2063
+ } catch (error) {
2064
+ derivativesContextUnavailable = true;
2065
+ import_logger5.logger.warn(
2066
+ "Derivatives context disabled after Timescale read failure: %s",
2067
+ String(error)
2068
+ );
2069
+ return false;
2070
+ }
2071
+ };
2072
+
2073
+ // src/strategyAdapters/ml.ts
2074
+ var defaultMlAdapter = {
2075
+ normalizeStrategyConfig: (strategyConfig) => strategyConfig
2076
+ };
2077
+ var getStrategyMlAdapter = (strategy, profileId) => {
2078
+ const strategyAdapter = getStrategyProfileMlAdapter(
2079
+ getStrategyManifest(strategy),
2080
+ profileId
2081
+ );
2082
+ if (!strategyAdapter) return defaultMlAdapter;
2083
+ return {
2084
+ ...defaultMlAdapter,
2085
+ ...strategyAdapter
2086
+ };
2087
+ };
2088
+
2089
+ // src/mlPayload.ts
2090
+ var normalizeStrategyConfig = (strategyConfig, strategyName, profileId) => {
2091
+ return getStrategyMlAdapter(
2092
+ strategyName,
2093
+ profileId
2094
+ ).normalizeStrategyConfig?.(strategyConfig);
2095
+ };
2096
+ var buildMlPayload = (payload) => {
2097
+ const strategyName = payload.signal?.strategy ?? payload.context?.strategyName;
2098
+ const profileId = payload.signal?.policyProfileId;
2099
+ const mlAdapter = getStrategyMlAdapter(strategyName, profileId);
2100
+ const normalizedSignal = mlAdapter.normalizeSignal?.(payload.signal) ?? payload.signal;
2101
+ const nextSignal = {
2102
+ ...normalizedSignal,
2103
+ indicators: {
2104
+ ...normalizedSignal?.indicators ?? {}
2105
+ }
2106
+ };
2107
+ const nextContext = payload.context ? {
2108
+ ...payload.context,
2109
+ strategyConfig: normalizeStrategyConfig(
2110
+ payload.context.strategyConfig,
2111
+ strategyName,
2112
+ profileId
2113
+ )
2114
+ } : void 0;
2115
+ return {
2116
+ signal: nextSignal,
2117
+ context: nextContext
2118
+ };
2119
+ };
2120
+
2121
+ // src/connectorsRegistry.ts
2122
+ var import_logger6 = require("@tradejs/infra/logger");
2123
+ var createConnectorRegistryState = () => ({
2124
+ connectorCreators: /* @__PURE__ */ new Map(),
2125
+ providerToConnectorName: /* @__PURE__ */ new Map(),
2126
+ pluginsLoadPromise: null
2127
+ });
2128
+ var registryStateByProjectRoot2 = /* @__PURE__ */ new Map();
2129
+ var getConnectorRegistryState = (cwd = getTradejsProjectCwd()) => {
2130
+ const projectRoot = getTradejsProjectCwd(cwd);
2131
+ let state = registryStateByProjectRoot2.get(projectRoot);
2132
+ if (!state) {
2133
+ state = createConnectorRegistryState();
2134
+ registryStateByProjectRoot2.set(projectRoot, state);
2135
+ }
2136
+ return {
805
2137
  projectRoot,
806
2138
  state
807
2139
  };
@@ -839,7 +2171,7 @@ var normalizeProviders = (providers, connectorName) => {
839
2171
  var registerProvider = (provider, connectorName, source, providerToConnectorName) => {
840
2172
  const existing = providerToConnectorName.get(provider);
841
2173
  if (existing && existing !== connectorName) {
842
- import_logger4.logger.warn(
2174
+ import_logger6.logger.warn(
843
2175
  'Skip duplicate connector provider "%s" from %s: already mapped to %s',
844
2176
  provider,
845
2177
  source,
@@ -852,11 +2184,11 @@ var registerProvider = (provider, connectorName, source, providerToConnectorName
852
2184
  var registerEntry = (entry, source, state) => {
853
2185
  const connectorName = String(entry?.name ?? "").trim();
854
2186
  if (!connectorName) {
855
- import_logger4.logger.warn("Skip connector entry without name from %s", source);
2187
+ import_logger6.logger.warn("Skip connector entry without name from %s", source);
856
2188
  return;
857
2189
  }
858
2190
  if (typeof entry.creator !== "function") {
859
- import_logger4.logger.warn(
2191
+ import_logger6.logger.warn(
860
2192
  'Skip connector entry "%s" from %s: creator must be a function',
861
2193
  connectorName,
862
2194
  source
@@ -868,7 +2200,7 @@ var registerEntry = (entry, source, state) => {
868
2200
  state.connectorCreators
869
2201
  );
870
2202
  if (existingByName) {
871
- import_logger4.logger.warn(
2203
+ import_logger6.logger.warn(
872
2204
  'Skip duplicate connector "%s" from %s: already registered as %s',
873
2205
  connectorName,
874
2206
  source,
@@ -940,7 +2272,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
940
2272
  );
941
2273
  const pluginDefinition = extractConnectorPluginDefinition(moduleExport);
942
2274
  if (!pluginDefinition) {
943
- import_logger4.logger.warn(
2275
+ import_logger6.logger.warn(
944
2276
  'Skip connector plugin "%s": export { connectorEntries } is missing',
945
2277
  moduleName
946
2278
  );
@@ -948,7 +2280,7 @@ var ensureConnectorPluginsLoaded = async (cwd = getTradejsProjectCwd()) => {
948
2280
  }
949
2281
  registerEntries2(pluginDefinition.connectorEntries, moduleName, state);
950
2282
  } catch (error) {
951
- import_logger4.logger.warn(
2283
+ import_logger6.logger.warn(
952
2284
  'Failed to load connector plugin "%s": %s',
953
2285
  moduleName,
954
2286
  String(error)
@@ -980,19 +2312,31 @@ var DEFAULT_CONNECTOR_NAME = BUILTIN_CONNECTOR_NAMES.ByBit;
980
2312
 
981
2313
  // src/testConnector.ts
982
2314
  var import_node_crypto = require("crypto");
2315
+ var import_constants2 = require("@tradejs/core/constants");
2316
+ var import_backtest = require("@tradejs/core/backtest");
2317
+ var import_trade = require("@tradejs/core/trade");
983
2318
  var import_math = require("@tradejs/core/math");
984
- var FEE = 5e-3;
985
- var INITIAL_AMOUNT = 100;
2319
+ var PRICE_PRECISION = 8;
986
2320
  var createTestConnector = (connector, context) => {
987
2321
  let state = {};
988
2322
  const orderLog = [];
989
2323
  const positionLog = [];
2324
+ const fastMode = Boolean(context?.fastMode);
2325
+ const executionCostModel = context?.executionCostModel;
2326
+ const makerFeeRate = executionCostModel?.fees.makerRate ?? import_constants2.FEE_PERCENT;
2327
+ const takerFeeRate = executionCostModel?.fees.takerRate ?? import_constants2.FEE_PERCENT;
2328
+ const fundingRates = [...context?.fundingRates ?? []].sort(
2329
+ (left, right) => left.timestamp - right.timestamp
2330
+ );
2331
+ const processedFundingTimestamps = /* @__PURE__ */ new Set();
990
2332
  let currentPosition = null;
991
- let amount = INITIAL_AMOUNT;
2333
+ let amount = import_constants2.INITIAL_BACKTEST_AMOUNT;
992
2334
  let originalQty = 0;
993
2335
  let currentPositionProfit = 0;
2336
+ let currentSignalId = null;
994
2337
  let takeProfits = [];
995
2338
  let stopLossPrice = null;
2339
+ let currentTradeResult = null;
996
2340
  const closedSignalResults = [];
997
2341
  const logOrder = (data) => {
998
2342
  const nextEntry = {
@@ -1003,10 +2347,157 @@ var createTestConnector = (connector, context) => {
1003
2347
  index: orderLog.length
1004
2348
  };
1005
2349
  if (nextEntry.signal) {
1006
- const { indicators: _indicators, ...signalWithoutIndicators } = nextEntry.signal;
1007
- nextEntry.signal = signalWithoutIndicators;
2350
+ const {
2351
+ additionalIndicators: _additionalIndicators,
2352
+ indicators: _indicators,
2353
+ ...signalWithoutHeavyContext
2354
+ } = nextEntry.signal;
2355
+ nextEntry.signal = signalWithoutHeavyContext;
2356
+ }
2357
+ if (!fastMode) {
2358
+ orderLog.push(nextEntry);
1008
2359
  }
1009
- orderLog.push(nextEntry);
2360
+ };
2361
+ const roundNullable2 = (value) => value == null ? null : (0, import_math.round)(value);
2362
+ const roundPrice = (value) => (0, import_math.round)(value, PRICE_PRECISION);
2363
+ const roundNullablePrice = (value) => value == null ? null : roundPrice(value);
2364
+ const getSlippageCost = ({
2365
+ requestedPrice,
2366
+ executionPrice,
2367
+ direction,
2368
+ stage,
2369
+ qty
2370
+ }) => {
2371
+ if (direction === "LONG") {
2372
+ return stage === "entry" ? Math.max(0, executionPrice - requestedPrice) * qty : Math.max(0, requestedPrice - executionPrice) * qty;
2373
+ }
2374
+ return stage === "entry" ? Math.max(0, requestedPrice - executionPrice) * qty : Math.max(0, executionPrice - requestedPrice) * qty;
2375
+ };
2376
+ const getSlippageBps = (requestedPrice, executionPrice) => requestedPrice ? (executionPrice - requestedPrice) / requestedPrice * 1e4 : 0;
2377
+ const getWeightedAverage = (previousValue, previousQty, nextValue, nextQty) => {
2378
+ if (previousValue == null || previousQty <= 0) {
2379
+ return nextValue;
2380
+ }
2381
+ return (previousValue * previousQty + nextValue * nextQty) / (previousQty + nextQty);
2382
+ };
2383
+ const finalizeTradeResult = (tradeResult, timestamp) => {
2384
+ if (!tradeResult.exitReason) {
2385
+ return void 0;
2386
+ }
2387
+ return {
2388
+ ...tradeResult,
2389
+ exitTimestamp: tradeResult.exitTimestamp ?? timestamp,
2390
+ exitReason: tradeResult.exitReason,
2391
+ requestedEntryPrice: roundPrice(tradeResult.requestedEntryPrice),
2392
+ entryPrice: roundPrice(tradeResult.entryPrice),
2393
+ requestedExitPrice: roundNullablePrice(tradeResult.requestedExitPrice),
2394
+ exitPrice: roundNullablePrice(tradeResult.exitPrice),
2395
+ grossProfit: (0, import_math.round)(tradeResult.grossProfit),
2396
+ netProfit: (0, import_math.round)(tradeResult.netProfit),
2397
+ openFee: (0, import_math.round)(tradeResult.openFee),
2398
+ closeFee: (0, import_math.round)(tradeResult.closeFee),
2399
+ fundingFee: roundNullable2(tradeResult.fundingFee),
2400
+ totalFee: (0, import_math.round)(tradeResult.totalFee),
2401
+ entrySlippagePrice: (0, import_math.round)(tradeResult.entrySlippagePrice),
2402
+ entrySlippageBps: (0, import_math.round)(tradeResult.entrySlippageBps),
2403
+ entryBaseSlippageBps: (0, import_math.round)(tradeResult.entryBaseSlippageBps),
2404
+ entrySpreadBps: (0, import_math.round)(tradeResult.entrySpreadBps),
2405
+ entrySpreadSlippageBps: (0, import_math.round)(tradeResult.entrySpreadSlippageBps),
2406
+ entryMarketImpactBps: (0, import_math.round)(tradeResult.entryMarketImpactBps),
2407
+ entryDelayRiskBps: roundNullable2(tradeResult.entryDelayRiskBps),
2408
+ entrySlippageCost: (0, import_math.round)(tradeResult.entrySlippageCost),
2409
+ exitSlippagePrice: roundNullable2(tradeResult.exitSlippagePrice),
2410
+ exitSlippageBps: roundNullable2(tradeResult.exitSlippageBps),
2411
+ exitBaseSlippageBps: roundNullable2(tradeResult.exitBaseSlippageBps),
2412
+ exitSpreadBps: roundNullable2(tradeResult.exitSpreadBps),
2413
+ exitSpreadSlippageBps: roundNullable2(tradeResult.exitSpreadSlippageBps),
2414
+ exitMarketImpactBps: roundNullable2(tradeResult.exitMarketImpactBps),
2415
+ exitDelayRiskBps: roundNullable2(tradeResult.exitDelayRiskBps),
2416
+ exitSlippageCost: (0, import_math.round)(tradeResult.exitSlippageCost),
2417
+ totalSlippageCost: (0, import_math.round)(tradeResult.totalSlippageCost),
2418
+ qty: (0, import_math.round)(tradeResult.qty),
2419
+ closedQty: (0, import_math.round)(tradeResult.closedQty)
2420
+ };
2421
+ };
2422
+ const recordExitResult = ({
2423
+ timestamp,
2424
+ reason,
2425
+ requestedPrice,
2426
+ executionPrice,
2427
+ qty,
2428
+ grossProfit,
2429
+ fee,
2430
+ slippageBreakdown
2431
+ }) => {
2432
+ if (!currentTradeResult || !currentPosition) {
2433
+ return;
2434
+ }
2435
+ const previousClosedQty = currentTradeResult.closedQty;
2436
+ const requestedExitPrice = getWeightedAverage(
2437
+ currentTradeResult.requestedExitPrice,
2438
+ previousClosedQty,
2439
+ requestedPrice,
2440
+ qty
2441
+ );
2442
+ const exitPrice = getWeightedAverage(
2443
+ currentTradeResult.exitPrice,
2444
+ previousClosedQty,
2445
+ executionPrice,
2446
+ qty
2447
+ );
2448
+ const exitBaseSlippageBps = getWeightedAverage(
2449
+ currentTradeResult.exitBaseSlippageBps,
2450
+ previousClosedQty,
2451
+ slippageBreakdown.baseSlippageBps,
2452
+ qty
2453
+ );
2454
+ const exitSpreadBps = getWeightedAverage(
2455
+ currentTradeResult.exitSpreadBps,
2456
+ previousClosedQty,
2457
+ slippageBreakdown.spreadBps,
2458
+ qty
2459
+ );
2460
+ const exitSpreadSlippageBps = getWeightedAverage(
2461
+ currentTradeResult.exitSpreadSlippageBps,
2462
+ previousClosedQty,
2463
+ slippageBreakdown.spreadSlippageBps,
2464
+ qty
2465
+ );
2466
+ const exitMarketImpactBps = getWeightedAverage(
2467
+ currentTradeResult.exitMarketImpactBps,
2468
+ previousClosedQty,
2469
+ slippageBreakdown.marketImpactBps,
2470
+ qty
2471
+ );
2472
+ const exitDelayRiskBps = null;
2473
+ const exitSlippageCost = currentTradeResult.exitSlippageCost + getSlippageCost({
2474
+ requestedPrice,
2475
+ executionPrice,
2476
+ direction: currentPosition.direction,
2477
+ stage: "exit",
2478
+ qty
2479
+ });
2480
+ currentTradeResult = {
2481
+ ...currentTradeResult,
2482
+ closedQty: previousClosedQty + qty,
2483
+ exitTimestamp: timestamp,
2484
+ exitReason: reason,
2485
+ requestedExitPrice,
2486
+ exitPrice,
2487
+ grossProfit: currentTradeResult.grossProfit + grossProfit,
2488
+ netProfit: currentTradeResult.netProfit + grossProfit - fee,
2489
+ closeFee: currentTradeResult.closeFee + fee,
2490
+ totalFee: currentTradeResult.openFee + currentTradeResult.closeFee + fee + (currentTradeResult.fundingFee ?? 0),
2491
+ exitSlippagePrice: exitPrice - requestedExitPrice,
2492
+ exitSlippageBps: getSlippageBps(requestedExitPrice, exitPrice),
2493
+ exitBaseSlippageBps,
2494
+ exitSpreadBps,
2495
+ exitSpreadSlippageBps,
2496
+ exitMarketImpactBps,
2497
+ exitDelayRiskBps,
2498
+ exitSlippageCost,
2499
+ totalSlippageCost: currentTradeResult.entrySlippageCost + exitSlippageCost
2500
+ };
1010
2501
  };
1011
2502
  const clearPosition = (timestamp) => {
1012
2503
  takeProfits = [];
@@ -1016,11 +2507,12 @@ var createTestConnector = (connector, context) => {
1016
2507
  return;
1017
2508
  }
1018
2509
  if (context?.mlEnabled || context?.aiEnabled) {
1019
- const signalId = currentPosition.signal?.signalId;
1020
- if (signalId) {
2510
+ if (currentSignalId) {
2511
+ const tradeResult = currentTradeResult ? finalizeTradeResult(currentTradeResult, timestamp) : void 0;
1021
2512
  closedSignalResults.push({
1022
- signalId,
1023
- profit: currentPositionProfit
2513
+ signalId: currentSignalId,
2514
+ profit: (0, import_math.round)(currentPositionProfit),
2515
+ ...tradeResult ? { tradeResult } : {}
1024
2516
  });
1025
2517
  }
1026
2518
  }
@@ -1036,29 +2528,117 @@ var createTestConnector = (connector, context) => {
1036
2528
  }
1037
2529
  });
1038
2530
  currentPosition = null;
2531
+ currentSignalId = null;
2532
+ currentTradeResult = null;
1039
2533
  currentPositionProfit = 0;
1040
2534
  };
1041
- return {
1042
- __tradejsTestConnector: true,
1043
- getState: async () => state,
1044
- setState: async (newState) => {
1045
- state = {
1046
- ...state,
1047
- ...newState
1048
- };
1049
- },
1050
- kline: async (options) => connector.kline(options),
1051
- getResult: async () => {
1052
- const orderLogId = (0, import_node_crypto.randomUUID)().slice(-12);
2535
+ const getNetProfit = ({
2536
+ grossProfit,
2537
+ price,
2538
+ qty,
2539
+ feeRate = takerFeeRate
2540
+ }) => {
2541
+ const fee = price * qty * feeRate;
2542
+ return {
2543
+ fee,
2544
+ profit: grossProfit - fee
2545
+ };
2546
+ };
2547
+ const applyFunding = (candle) => {
2548
+ if (!executionCostModel?.funding.enabled || !currentPosition) {
2549
+ return;
2550
+ }
2551
+ for (const point of fundingRates) {
2552
+ if (processedFundingTimestamps.has(point.timestamp) || point.symbol.toUpperCase() !== currentPosition.symbol.toUpperCase() || point.timestamp <= currentPosition.timestamp || point.timestamp > candle.timestamp) {
2553
+ continue;
2554
+ }
2555
+ processedFundingTimestamps.add(point.timestamp);
2556
+ const notional = candle.close * currentPosition.qty;
2557
+ const fundingCost = notional * point.rate * (currentPosition.direction === "LONG" ? 1 : -1);
2558
+ amount -= fundingCost;
2559
+ currentPositionProfit -= fundingCost;
2560
+ if (currentTradeResult) {
2561
+ currentTradeResult.fundingFee = (currentTradeResult.fundingFee ?? 0) + fundingCost;
2562
+ currentTradeResult.netProfit -= fundingCost;
2563
+ currentTradeResult.totalFee += fundingCost;
2564
+ }
2565
+ }
2566
+ };
2567
+ const getExitTimestamp = (candle) => currentPosition ? Math.max(candle.timestamp, currentPosition.timestamp) : candle.timestamp;
2568
+ const applyExecutionSlippage = ({
2569
+ price,
2570
+ direction,
2571
+ stage,
2572
+ signal
2573
+ }) => {
2574
+ const modelParams = {
2575
+ baseSlippageBps: executionCostModel?.slippage.baseBps,
2576
+ spreadBps: (0, import_trade.extractExecutionSpreadBps)(signal),
2577
+ spreadMultiplier: executionCostModel?.slippage.spreadMultiplier,
2578
+ marketImpactBps: (0, import_trade.extractExecutionMarketImpactBps)(signal) ?? executionCostModel?.slippage.marketImpactBps,
2579
+ delayRiskBps: stage === "entry" ? ((0, import_trade.extractExecutionDelayRiskBps)(signal) ?? 0) * (executionCostModel?.slippage.delayRiskMultiplier ?? 1) : null
2580
+ };
2581
+ return (0, import_trade.applyExecutionSlippage)({
2582
+ price,
2583
+ direction,
2584
+ stage,
2585
+ ...modelParams
2586
+ });
2587
+ };
2588
+ const getExecutionSlippageBreakdown = ({
2589
+ stage,
2590
+ signal
2591
+ }) => (0, import_trade.calculateExecutionSlippageBreakdown)({
2592
+ baseSlippageBps: executionCostModel?.slippage.baseBps,
2593
+ spreadBps: (0, import_trade.extractExecutionSpreadBps)(signal),
2594
+ spreadMultiplier: executionCostModel?.slippage.spreadMultiplier,
2595
+ marketImpactBps: (0, import_trade.extractExecutionMarketImpactBps)(signal) ?? executionCostModel?.slippage.marketImpactBps,
2596
+ delayRiskBps: stage === "entry" ? ((0, import_trade.extractExecutionDelayRiskBps)(signal) ?? 0) * (executionCostModel?.slippage.delayRiskMultiplier ?? 1) : null
2597
+ });
2598
+ const getExecutionSlippageLogData = (slippageBreakdown, stage) => ({
2599
+ executionSlippageStage: stage,
2600
+ executionSlippageBps: (0, import_math.round)(slippageBreakdown.effectiveSlippageBps),
2601
+ executionBaseSlippageBps: (0, import_math.round)(slippageBreakdown.baseSlippageBps),
2602
+ executionSpreadBps: (0, import_math.round)(slippageBreakdown.spreadBps),
2603
+ executionSpreadSlippageBps: (0, import_math.round)(slippageBreakdown.spreadSlippageBps),
2604
+ executionMarketImpactBps: (0, import_math.round)(slippageBreakdown.marketImpactBps),
2605
+ executionDelayRiskBps: stage === "entry" ? (0, import_math.round)(slippageBreakdown.delayRiskBps) : null
2606
+ });
2607
+ return {
2608
+ __tradejsTestConnector: true,
2609
+ capabilities: connector.capabilities,
2610
+ universe: connector.universe,
2611
+ accountId: connector.accountId,
2612
+ deploymentId: connector.deploymentId,
2613
+ listInstruments: (query) => connector.listInstruments(query),
2614
+ getFundingRateHistory: connector.getFundingRateHistory ? (request) => connector.getFundingRateHistory(request) : void 0,
2615
+ getTradingFeeRate: connector.getTradingFeeRate ? (symbol) => connector.getTradingFeeRate(symbol) : void 0,
2616
+ getState: async () => state,
2617
+ setState: async (newState) => {
2618
+ state = {
2619
+ ...state,
2620
+ ...newState
2621
+ };
2622
+ },
2623
+ kline: async (options) => connector.kline(options),
2624
+ getResult: async () => {
2625
+ const orderLogId = (0, import_node_crypto.randomUUID)().slice(-12);
2626
+ const fullStat = fastMode ? (0, import_backtest.calculateStatsFull)(positionLog) : null;
1053
2627
  return {
1054
- stat: {
1055
- amount,
1056
- profit: amount - INITIAL_AMOUNT,
2628
+ stat: fullStat ? {
2629
+ ...fullStat,
2630
+ profit: fullStat.netProfit
2631
+ } : {
2632
+ amount: (0, import_math.round)(amount),
2633
+ profit: (0, import_math.round)(amount - import_constants2.INITIAL_BACKTEST_AMOUNT),
1057
2634
  orders: positionLog.length
1058
2635
  },
1059
2636
  orderLogId,
1060
- inlineOrderLog: [...orderLog],
1061
- inlinePositionLog: [...positionLog]
2637
+ ...executionCostModel ? { executionCostModel } : {},
2638
+ ...fastMode ? {} : {
2639
+ inlineOrderLog: orderLog,
2640
+ inlinePositionLog: positionLog
2641
+ }
1062
2642
  };
1063
2643
  },
1064
2644
  getPosition: async () => currentPosition || null,
@@ -1084,6 +2664,7 @@ var createTestConnector = (connector, context) => {
1084
2664
  if (!candle || !currentPosition || !currentPosition.qty) {
1085
2665
  return;
1086
2666
  }
2667
+ applyFunding(candle);
1087
2668
  const isLong = currentPosition.direction === "LONG";
1088
2669
  const entryPrice = currentPosition.price;
1089
2670
  const high = candle.high;
@@ -1093,47 +2674,217 @@ var createTestConnector = (connector, context) => {
1093
2674
  const targetPrice = tp.price;
1094
2675
  const reached = isLong ? high >= targetPrice : low <= targetPrice;
1095
2676
  if (reached) {
2677
+ const exitTimestamp = getExitTimestamp(candle);
1096
2678
  const qty = originalQty * tp.rate;
1097
- const profit = isLong ? (targetPrice - entryPrice) * qty : (entryPrice - targetPrice) * qty;
2679
+ const slippageBreakdown = getExecutionSlippageBreakdown({
2680
+ stage: "exit",
2681
+ signal: currentPosition.signal
2682
+ });
2683
+ const executionPrice = applyExecutionSlippage({
2684
+ price: targetPrice,
2685
+ direction: currentPosition.direction,
2686
+ stage: "exit",
2687
+ signal: currentPosition.signal
2688
+ });
2689
+ const grossProfit = isLong ? (executionPrice - entryPrice) * qty : (entryPrice - executionPrice) * qty;
2690
+ const { fee, profit } = getNetProfit({
2691
+ grossProfit,
2692
+ price: executionPrice,
2693
+ qty
2694
+ });
2695
+ recordExitResult({
2696
+ timestamp: exitTimestamp,
2697
+ reason: "take_profit",
2698
+ requestedPrice: targetPrice,
2699
+ executionPrice,
2700
+ qty,
2701
+ grossProfit,
2702
+ fee,
2703
+ slippageBreakdown
2704
+ });
1098
2705
  amount += profit;
1099
2706
  currentPositionProfit += profit;
1100
2707
  currentPosition.qty = parseFloat(
1101
2708
  (currentPosition.qty - qty).toFixed(8)
1102
2709
  );
1103
2710
  logOrder({
1104
- timestamp: candle.timestamp,
2711
+ timestamp: exitTimestamp,
1105
2712
  qty,
1106
- price: targetPrice,
2713
+ price: executionPrice,
1107
2714
  profit,
1108
- type: isLong ? "TAKE_PROFIT_LONG" : "TAKE_PROFIT_SHORT"
2715
+ fee,
2716
+ type: isLong ? "TAKE_PROFIT_LONG" : "TAKE_PROFIT_SHORT",
2717
+ ...getExecutionSlippageLogData(slippageBreakdown, "exit")
1109
2718
  });
1110
2719
  tp.done = true;
1111
2720
  }
1112
2721
  }
1113
2722
  takeProfits = takeProfits.filter(({ done }) => !done);
1114
2723
  if (currentPosition && currentPosition.qty <= 0) {
1115
- clearPosition(candle.timestamp);
2724
+ clearPosition(getExitTimestamp(candle));
1116
2725
  }
1117
2726
  },
1118
2727
  checkSl: async (candle) => {
1119
2728
  if (!stopLossPrice || !currentPosition || !candle) {
1120
2729
  return;
1121
2730
  }
2731
+ applyFunding(candle);
1122
2732
  const isLong = currentPosition.direction === "LONG";
1123
2733
  const hitStop = isLong ? candle.low <= stopLossPrice : candle.high >= stopLossPrice;
1124
2734
  if (hitStop) {
2735
+ const exitTimestamp = getExitTimestamp(candle);
1125
2736
  const qty = currentPosition.qty;
1126
- const profit = isLong ? (stopLossPrice - currentPosition.price) * qty : (currentPosition.price - stopLossPrice) * qty;
2737
+ const slippageBreakdown = getExecutionSlippageBreakdown({
2738
+ stage: "exit",
2739
+ signal: currentPosition.signal
2740
+ });
2741
+ const executionPrice = applyExecutionSlippage({
2742
+ price: stopLossPrice,
2743
+ direction: currentPosition.direction,
2744
+ stage: "exit",
2745
+ signal: currentPosition.signal
2746
+ });
2747
+ const grossProfit = isLong ? (executionPrice - currentPosition.price) * qty : (currentPosition.price - executionPrice) * qty;
2748
+ const { fee, profit } = getNetProfit({
2749
+ grossProfit,
2750
+ price: executionPrice,
2751
+ qty
2752
+ });
2753
+ recordExitResult({
2754
+ timestamp: exitTimestamp,
2755
+ reason: "stop_loss",
2756
+ requestedPrice: stopLossPrice,
2757
+ executionPrice,
2758
+ qty,
2759
+ grossProfit,
2760
+ fee,
2761
+ slippageBreakdown
2762
+ });
1127
2763
  amount += profit;
1128
2764
  currentPositionProfit += profit;
1129
2765
  logOrder({
1130
- timestamp: candle.timestamp,
2766
+ timestamp: exitTimestamp,
1131
2767
  qty,
1132
2768
  profit,
1133
- price: stopLossPrice,
1134
- type: isLong ? "STOP_LOSS_LONG" : "STOP_LOSS_SHORT"
2769
+ price: executionPrice,
2770
+ fee,
2771
+ type: isLong ? "STOP_LOSS_LONG" : "STOP_LOSS_SHORT",
2772
+ ...getExecutionSlippageLogData(slippageBreakdown, "exit")
1135
2773
  });
1136
- clearPosition(candle.timestamp);
2774
+ clearPosition(exitTimestamp);
2775
+ }
2776
+ },
2777
+ checkExits: async (candle) => {
2778
+ if (!candle || !currentPosition) {
2779
+ return;
2780
+ }
2781
+ applyFunding(candle);
2782
+ if (stopLossPrice) {
2783
+ const isLong2 = currentPosition.direction === "LONG";
2784
+ const hitStop = isLong2 ? candle.low <= stopLossPrice : candle.high >= stopLossPrice;
2785
+ if (hitStop) {
2786
+ const exitTimestamp = getExitTimestamp(candle);
2787
+ const qty = currentPosition.qty;
2788
+ const slippageBreakdown = getExecutionSlippageBreakdown({
2789
+ stage: "exit",
2790
+ signal: currentPosition.signal
2791
+ });
2792
+ const executionPrice = applyExecutionSlippage({
2793
+ price: stopLossPrice,
2794
+ direction: currentPosition.direction,
2795
+ stage: "exit",
2796
+ signal: currentPosition.signal
2797
+ });
2798
+ const grossProfit = isLong2 ? (executionPrice - currentPosition.price) * qty : (currentPosition.price - executionPrice) * qty;
2799
+ const { fee, profit } = getNetProfit({
2800
+ grossProfit,
2801
+ price: executionPrice,
2802
+ qty
2803
+ });
2804
+ recordExitResult({
2805
+ timestamp: exitTimestamp,
2806
+ reason: "stop_loss",
2807
+ requestedPrice: stopLossPrice,
2808
+ executionPrice,
2809
+ qty,
2810
+ grossProfit,
2811
+ fee,
2812
+ slippageBreakdown
2813
+ });
2814
+ amount += profit;
2815
+ currentPositionProfit += profit;
2816
+ logOrder({
2817
+ timestamp: exitTimestamp,
2818
+ qty,
2819
+ profit,
2820
+ price: executionPrice,
2821
+ fee,
2822
+ type: isLong2 ? "STOP_LOSS_LONG" : "STOP_LOSS_SHORT",
2823
+ ...getExecutionSlippageLogData(slippageBreakdown, "exit")
2824
+ });
2825
+ clearPosition(exitTimestamp);
2826
+ }
2827
+ }
2828
+ if (!currentPosition || !currentPosition.qty) {
2829
+ return;
2830
+ }
2831
+ const isLong = currentPosition.direction === "LONG";
2832
+ const entryPrice = currentPosition.price;
2833
+ const high = candle.high;
2834
+ const low = candle.low;
2835
+ for (const tp of takeProfits) {
2836
+ if (!currentPosition || currentPosition.qty <= 0) break;
2837
+ const targetPrice = tp.price;
2838
+ const reached = isLong ? high >= targetPrice : low <= targetPrice;
2839
+ if (reached) {
2840
+ const exitTimestamp = getExitTimestamp(candle);
2841
+ const qty = originalQty * tp.rate;
2842
+ const slippageBreakdown = getExecutionSlippageBreakdown({
2843
+ stage: "exit",
2844
+ signal: currentPosition.signal
2845
+ });
2846
+ const executionPrice = applyExecutionSlippage({
2847
+ price: targetPrice,
2848
+ direction: currentPosition.direction,
2849
+ stage: "exit",
2850
+ signal: currentPosition.signal
2851
+ });
2852
+ const grossProfit = isLong ? (executionPrice - entryPrice) * qty : (entryPrice - executionPrice) * qty;
2853
+ const { fee, profit } = getNetProfit({
2854
+ grossProfit,
2855
+ price: executionPrice,
2856
+ qty
2857
+ });
2858
+ recordExitResult({
2859
+ timestamp: exitTimestamp,
2860
+ reason: "take_profit",
2861
+ requestedPrice: targetPrice,
2862
+ executionPrice,
2863
+ qty,
2864
+ grossProfit,
2865
+ fee,
2866
+ slippageBreakdown
2867
+ });
2868
+ amount += profit;
2869
+ currentPositionProfit += profit;
2870
+ currentPosition.qty = parseFloat(
2871
+ (currentPosition.qty - qty).toFixed(8)
2872
+ );
2873
+ logOrder({
2874
+ timestamp: exitTimestamp,
2875
+ qty,
2876
+ price: executionPrice,
2877
+ profit,
2878
+ fee,
2879
+ type: isLong ? "TAKE_PROFIT_LONG" : "TAKE_PROFIT_SHORT",
2880
+ ...getExecutionSlippageLogData(slippageBreakdown, "exit")
2881
+ });
2882
+ tp.done = true;
2883
+ }
2884
+ }
2885
+ takeProfits = takeProfits.filter(({ done }) => !done);
2886
+ if (currentPosition && currentPosition.qty <= 0) {
2887
+ clearPosition(getExitTimestamp(candle));
1137
2888
  }
1138
2889
  },
1139
2890
  placeOrder: async (order) => {
@@ -1141,17 +2892,77 @@ var createTestConnector = (connector, context) => {
1141
2892
  return false;
1142
2893
  }
1143
2894
  const isLong = order.direction === "LONG";
1144
- currentPosition = { ...order, amount };
2895
+ const entrySlippageBreakdown = getExecutionSlippageBreakdown({
2896
+ stage: "entry",
2897
+ signal: order.signal
2898
+ });
2899
+ const entryPrice = applyExecutionSlippage({
2900
+ price: order.price,
2901
+ direction: order.direction,
2902
+ stage: "entry",
2903
+ signal: order.signal
2904
+ });
2905
+ currentPosition = { ...order, price: entryPrice, amount };
2906
+ currentSignalId = typeof order.signal?.signalId === "string" && order.signal.signalId ? order.signal.signalId : null;
1145
2907
  originalQty = order.qty;
1146
- const fee = order.price * order.qty * FEE;
1147
- const profit = fee * -1;
2908
+ const { fee, profit } = getNetProfit({
2909
+ grossProfit: 0,
2910
+ price: entryPrice,
2911
+ qty: order.qty,
2912
+ feeRate: order.isLimit ? makerFeeRate : takerFeeRate
2913
+ });
2914
+ const entrySlippageCost = getSlippageCost({
2915
+ requestedPrice: order.price,
2916
+ executionPrice: entryPrice,
2917
+ direction: order.direction,
2918
+ stage: "entry",
2919
+ qty: order.qty
2920
+ });
1148
2921
  amount += profit;
1149
2922
  currentPositionProfit = profit;
2923
+ currentTradeResult = currentSignalId ? {
2924
+ signalId: currentSignalId,
2925
+ direction: order.direction,
2926
+ qty: order.qty,
2927
+ closedQty: 0,
2928
+ entryTimestamp: order.timestamp,
2929
+ exitTimestamp: null,
2930
+ exitReason: null,
2931
+ requestedEntryPrice: order.price,
2932
+ entryPrice,
2933
+ requestedExitPrice: null,
2934
+ exitPrice: null,
2935
+ grossProfit: 0,
2936
+ netProfit: profit,
2937
+ openFee: fee,
2938
+ closeFee: 0,
2939
+ fundingFee: executionCostModel?.funding.enabled ? 0 : null,
2940
+ totalFee: fee,
2941
+ entrySlippagePrice: entryPrice - order.price,
2942
+ entrySlippageBps: getSlippageBps(order.price, entryPrice),
2943
+ entryBaseSlippageBps: entrySlippageBreakdown.baseSlippageBps,
2944
+ entrySpreadBps: entrySlippageBreakdown.spreadBps,
2945
+ entrySpreadSlippageBps: entrySlippageBreakdown.spreadSlippageBps,
2946
+ entryMarketImpactBps: entrySlippageBreakdown.marketImpactBps,
2947
+ entryDelayRiskBps: entrySlippageBreakdown.delayRiskBps,
2948
+ entrySlippageCost,
2949
+ exitSlippagePrice: null,
2950
+ exitSlippageBps: null,
2951
+ exitBaseSlippageBps: null,
2952
+ exitSpreadBps: null,
2953
+ exitSpreadSlippageBps: null,
2954
+ exitMarketImpactBps: null,
2955
+ exitDelayRiskBps: null,
2956
+ exitSlippageCost: 0,
2957
+ totalSlippageCost: entrySlippageCost
2958
+ } : null;
1150
2959
  logOrder({
1151
2960
  ...order,
2961
+ price: entryPrice,
1152
2962
  profit,
1153
2963
  fee,
1154
- type: isLong ? "OPEN_LONG" : "OPEN_SHORT"
2964
+ type: isLong ? "OPEN_LONG" : "OPEN_SHORT",
2965
+ ...getExecutionSlippageLogData(entrySlippageBreakdown, "entry")
1155
2966
  });
1156
2967
  return true;
1157
2968
  },
@@ -1178,14 +2989,42 @@ var createTestConnector = (connector, context) => {
1178
2989
  return false;
1179
2990
  }
1180
2991
  const isLong = currentPosition.direction === "LONG";
1181
- const profit = isLong ? (order.price - currentPosition.price) * currentPosition.qty : (currentPosition.price - order.price) * currentPosition.qty;
2992
+ const slippageBreakdown = getExecutionSlippageBreakdown({
2993
+ stage: "exit",
2994
+ signal: currentPosition.signal
2995
+ });
2996
+ const executionPrice = applyExecutionSlippage({
2997
+ price: order.price,
2998
+ direction: currentPosition.direction,
2999
+ stage: "exit",
3000
+ signal: currentPosition.signal
3001
+ });
3002
+ const grossProfit = isLong ? (executionPrice - currentPosition.price) * currentPosition.qty : (currentPosition.price - executionPrice) * currentPosition.qty;
3003
+ const { fee, profit } = getNetProfit({
3004
+ grossProfit,
3005
+ price: executionPrice,
3006
+ qty: currentPosition.qty
3007
+ });
3008
+ recordExitResult({
3009
+ timestamp: order.timestamp,
3010
+ reason: "exit",
3011
+ requestedPrice: order.price,
3012
+ executionPrice,
3013
+ qty: currentPosition.qty,
3014
+ grossProfit,
3015
+ fee,
3016
+ slippageBreakdown
3017
+ });
1182
3018
  amount += profit;
1183
3019
  currentPositionProfit += profit;
1184
3020
  logOrder({
1185
3021
  ...order,
3022
+ price: executionPrice,
1186
3023
  qty: currentPosition.qty,
1187
3024
  profit,
1188
- type: isLong ? "CLOSE_LONG" : "CLOSE_SHORT"
3025
+ fee,
3026
+ type: isLong ? "CLOSE_LONG" : "CLOSE_SHORT",
3027
+ ...getExecutionSlippageLogData(slippageBreakdown, "exit")
1189
3028
  });
1190
3029
  clearPosition(order.timestamp);
1191
3030
  return true;
@@ -1196,10 +3035,194 @@ var createTestConnector = (connector, context) => {
1196
3035
  };
1197
3036
  };
1198
3037
 
3038
+ // src/executionCosts.ts
3039
+ var import_constants3 = require("@tradejs/core/constants");
3040
+ var finiteOr = (value, fallback) => {
3041
+ const parsed = Number(value);
3042
+ return Number.isFinite(parsed) ? parsed : fallback;
3043
+ };
3044
+ var feeCache = /* @__PURE__ */ new WeakMap();
3045
+ var fundingCache = /* @__PURE__ */ new WeakMap();
3046
+ var loadTradingFee = async (connector, symbol) => {
3047
+ if (!connector.getTradingFeeRate) return null;
3048
+ let cache = feeCache.get(connector);
3049
+ if (!cache) {
3050
+ cache = /* @__PURE__ */ new Map();
3051
+ feeCache.set(connector, cache);
3052
+ }
3053
+ const key = symbol.toUpperCase();
3054
+ const cached = cache.get(key);
3055
+ if (cached) return cached;
3056
+ const rate = await connector.getTradingFeeRate(symbol);
3057
+ cache.set(key, rate);
3058
+ return rate;
3059
+ };
3060
+ var loadFundingRates = (connector, symbol, startTime, endTime) => {
3061
+ if (!connector.getFundingRateHistory) return Promise.resolve([]);
3062
+ let cache = fundingCache.get(connector);
3063
+ if (!cache) {
3064
+ cache = /* @__PURE__ */ new Map();
3065
+ fundingCache.set(connector, cache);
3066
+ }
3067
+ const key = `${symbol.toUpperCase()}:${startTime}:${endTime}`;
3068
+ const cached = cache.get(key);
3069
+ if (cached) return cached;
3070
+ const pending = connector.getFundingRateHistory({ symbol, startTime, endTime }).catch(() => []);
3071
+ cache.set(key, pending);
3072
+ return pending;
3073
+ };
3074
+ var resolveExecutionCosts = async (params) => {
3075
+ const { connector, symbol, config, startTime, endTime, instrument } = params;
3076
+ const cacheOnly = config.EXECUTION_COSTS_CACHE_ONLY === true;
3077
+ const hasConfiguredFees = Number.isFinite(Number(config.MAKER_FEE_RATE)) && Number.isFinite(Number(config.TAKER_FEE_RATE));
3078
+ const exchangeFees = !cacheOnly && !hasConfiguredFees && connector.getTradingFeeRate ? await loadTradingFee(connector, symbol).catch(() => null) : null;
3079
+ const makerRate = hasConfiguredFees ? Number(config.MAKER_FEE_RATE) : exchangeFees?.makerRate ?? import_constants3.FEE_PERCENT;
3080
+ const takerRate = hasConfiguredFees ? Number(config.TAKER_FEE_RATE) : exchangeFees?.takerRate ?? import_constants3.FEE_PERCENT;
3081
+ const fundingEnabled = config.FUNDING_ENABLED !== false && !cacheOnly && typeof connector.getFundingRateHistory === "function";
3082
+ const fundingRates = fundingEnabled ? await loadFundingRates(connector, symbol, startTime, endTime) : [];
3083
+ const requestedLeverage = Math.max(1, finiteOr(config.LEVERAGE, 10));
3084
+ const venueMaxLeverage = Number(instrument?.venueMetadata?.maxLeverage);
3085
+ const maxAllowed = Number.isFinite(venueMaxLeverage) ? venueMaxLeverage : null;
3086
+ const effectiveLeverage = maxAllowed == null ? requestedLeverage : Math.min(requestedLeverage, maxAllowed);
3087
+ const feeSource = hasConfiguredFees ? "config" : exchangeFees?.source ?? "fallback";
3088
+ const fundingSource = !fundingEnabled ? cacheOnly ? "fallback" : "disabled" : fundingRates.length ? "historical" : "unavailable";
3089
+ const usesFallback = feeSource === "fallback" || fundingEnabled && fundingSource === "unavailable" || config.SLIPPAGE_BASE_BPS == null && config.SLIPPAGE_SPREAD_MULTIPLIER == null && config.SLIPPAGE_MARKET_IMPACT_BPS == null;
3090
+ return {
3091
+ model: {
3092
+ fees: { makerRate, takerRate, source: feeSource },
3093
+ funding: {
3094
+ enabled: fundingEnabled,
3095
+ source: fundingSource,
3096
+ points: fundingRates.length,
3097
+ fromTimestamp: fundingRates[0]?.timestamp ?? null,
3098
+ toTimestamp: fundingRates.at(-1)?.timestamp ?? null
3099
+ },
3100
+ slippage: {
3101
+ baseBps: finiteOr(config.SLIPPAGE_BASE_BPS, import_constants3.BACKTEST_BASE_SLIPPAGE_BPS),
3102
+ spreadMultiplier: finiteOr(
3103
+ config.SLIPPAGE_SPREAD_MULTIPLIER,
3104
+ import_constants3.BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER
3105
+ ),
3106
+ marketImpactBps: finiteOr(
3107
+ config.SLIPPAGE_MARKET_IMPACT_BPS,
3108
+ import_constants3.BACKTEST_MARKET_IMPACT_BPS
3109
+ ),
3110
+ delayRiskMultiplier: finiteOr(
3111
+ config.SLIPPAGE_DELAY_RISK_MULTIPLIER,
3112
+ import_constants3.BACKTEST_DELAY_RISK_MULTIPLIER
3113
+ ),
3114
+ source: config.SLIPPAGE_BASE_BPS != null || config.SLIPPAGE_SPREAD_MULTIPLIER != null || config.SLIPPAGE_MARKET_IMPACT_BPS != null ? "config" : "fallback"
3115
+ },
3116
+ leverage: {
3117
+ requested: requestedLeverage,
3118
+ effective: effectiveLeverage,
3119
+ maxAllowed
3120
+ },
3121
+ quality: usesFallback ? "fallback" : fundingEnabled && fundingRates.length ? "full" : "partial",
3122
+ capturedAt: Date.now()
3123
+ },
3124
+ fundingRates
3125
+ };
3126
+ };
3127
+
1199
3128
  // src/testing.ts
3129
+ var isBacktestEntryDelayControlCode = (value) => typeof value === "string" && value.startsWith("BACKTEST_ENTRY_DELAY_");
3130
+ var CLOSED_RESULT_FLUSH_INTERVAL = 500;
3131
+ var DEFAULT_STRATEGY_CANDLE_TIMEOUT_MS = 6e4;
3132
+ var resolvePositiveInt = (value, fallback) => {
3133
+ const parsed = parseInt(String(value ?? ""), 10);
3134
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
3135
+ };
3136
+ var getStrategyCandleTimeoutMs = () => resolvePositiveInt(
3137
+ process.env.BACKTEST_STRATEGY_CANDLE_TIMEOUT_MS,
3138
+ DEFAULT_STRATEGY_CANDLE_TIMEOUT_MS
3139
+ );
3140
+ var getEffectiveTimeoutMs = (baseTimeoutMs, stageTimeoutMs) => {
3141
+ const base = baseTimeoutMs && baseTimeoutMs > 0 ? Math.trunc(baseTimeoutMs) : null;
3142
+ const stage = stageTimeoutMs && stageTimeoutMs > 0 ? Math.trunc(stageTimeoutMs) : null;
3143
+ if (base == null) return stage;
3144
+ if (stage == null) return base;
3145
+ return Math.min(base, stage);
3146
+ };
3147
+ var buildCandleByTimestamp = (candles) => new Map(
3148
+ (candles ?? []).filter((candle) => typeof candle?.timestamp === "number").map((candle) => [candle.timestamp, candle])
3149
+ );
3150
+ var buildBacktestDatasetMetadata = ({
3151
+ backtestRunId,
3152
+ backtestTestKey,
3153
+ chunkId
3154
+ }) => {
3155
+ if (!backtestRunId || !backtestTestKey || !chunkId) {
3156
+ return {};
3157
+ }
3158
+ return {
3159
+ backtestRunId,
3160
+ backtestTestKey,
3161
+ backtestChunkId: chunkId
3162
+ };
3163
+ };
3164
+ var cloneAiPayloadSignal = (signal) => {
3165
+ const cloneValue = (value) => {
3166
+ if (value == null) {
3167
+ return value;
3168
+ }
3169
+ if (typeof structuredClone === "function") {
3170
+ return structuredClone(value);
3171
+ }
3172
+ return JSON.parse(JSON.stringify(value));
3173
+ };
3174
+ return {
3175
+ ...signal,
3176
+ figures: cloneValue(signal.figures),
3177
+ indicators: cloneValue(signal.indicators),
3178
+ additionalIndicators: cloneValue(signal.additionalIndicators)
3179
+ };
3180
+ };
3181
+ var buildReplaySignalEvaluationRecord = ({
3182
+ signal,
3183
+ testId,
3184
+ userName,
3185
+ strategyName,
3186
+ symbol,
3187
+ interval,
3188
+ candle
3189
+ }) => {
3190
+ if (!signal || typeof signal === "string") {
3191
+ return {
3192
+ evaluationId: `${testId}:${strategyName}:${symbol}:${candle.timestamp}`,
3193
+ userName,
3194
+ strategy: strategyName,
3195
+ symbol,
3196
+ interval,
3197
+ timestamp: candle.timestamp,
3198
+ evaluatedAt: candle.timestamp,
3199
+ status: "skip",
3200
+ reason: typeof signal === "string" && signal.trim() ? signal : "NO_SIGNAL"
3201
+ };
3202
+ }
3203
+ const signalTimestamp = typeof signal.timestamp === "number" && Number.isFinite(signal.timestamp) ? signal.timestamp : candle.timestamp;
3204
+ return {
3205
+ evaluationId: `${signal.signalId || testId}:${strategyName}:${symbol}:${signalTimestamp}`,
3206
+ userName,
3207
+ strategy: signal.strategy || strategyName,
3208
+ symbol: signal.symbol || symbol,
3209
+ interval: signal.interval || interval,
3210
+ timestamp: signalTimestamp,
3211
+ evaluatedAt: candle.timestamp,
3212
+ status: "signal",
3213
+ reason: signal.orderSkipReason || signal.orderStatus,
3214
+ signalId: signal.signalId,
3215
+ direction: signal.direction,
3216
+ orderStatus: signal.orderStatus,
3217
+ orderSkipReason: signal.orderSkipReason,
3218
+ aiAnalysis: signal.aiAnalysis ?? null,
3219
+ ml: signal.ml
3220
+ };
3221
+ };
1200
3222
  var createTestingKlineCacheState = () => ({
1201
3223
  coinKlineCache: /* @__PURE__ */ new Map(),
1202
3224
  btcKlineCache: /* @__PURE__ */ new Map(),
3225
+ ethKlineCache: /* @__PURE__ */ new Map(),
1203
3226
  btcBinanceKlineCache: /* @__PURE__ */ new Map(),
1204
3227
  btcCoinbaseKlineCache: /* @__PURE__ */ new Map(),
1205
3228
  preparedDataCache: /* @__PURE__ */ new Map(),
@@ -1226,11 +3249,15 @@ var getKlineCacheKey = (params) => {
1226
3249
  preloadStart,
1227
3250
  end,
1228
3251
  interval,
1229
- cacheOnly
3252
+ cacheOnly,
3253
+ universe,
3254
+ accountId
1230
3255
  } = params;
1231
3256
  return [
1232
3257
  userName,
1233
3258
  connectorName,
3259
+ universe ?? "crypto",
3260
+ accountId ?? "default",
1234
3261
  symbol,
1235
3262
  preloadStart,
1236
3263
  end,
@@ -1248,21 +3275,47 @@ var getPreparedDataCacheKey = (params) => {
1248
3275
  end,
1249
3276
  interval,
1250
3277
  btcBinanceConnectorName,
1251
- btcCoinbaseConnectorName
3278
+ btcCoinbaseConnectorName,
3279
+ backtestExecutionInterval,
3280
+ universe,
3281
+ accountId
1252
3282
  } = params;
1253
3283
  return [
1254
3284
  userName,
1255
3285
  connectorName,
3286
+ universe ?? "crypto",
3287
+ accountId ?? "default",
1256
3288
  symbol,
1257
3289
  preloadStart,
1258
3290
  start,
1259
3291
  end,
1260
3292
  interval,
3293
+ backtestExecutionInterval,
1261
3294
  btcBinanceConnectorName,
1262
3295
  btcCoinbaseConnectorName
1263
3296
  ].join(":");
1264
3297
  };
1265
- var getConnectorCacheKey = (params) => [params.userName, params.connectorName].join(":");
3298
+ var getConnectorCacheKey = (params) => [
3299
+ params.userName,
3300
+ params.connectorName,
3301
+ params.universe ?? "crypto",
3302
+ params.accountId ?? "default"
3303
+ ].join(":");
3304
+ var BACKTEST_INTERVAL = "15";
3305
+ var resolveBacktestExecutionInterval = (interval) => {
3306
+ const normalized = String(interval);
3307
+ if (normalized === "15") {
3308
+ return import_constants4.BACKTEST_EXECUTION_INTERVAL;
3309
+ }
3310
+ if (normalized === "60") {
3311
+ return "15";
3312
+ }
3313
+ return null;
3314
+ };
3315
+ var resolveIntervalMs = (interval) => {
3316
+ const intervalMinutes = Number(interval);
3317
+ return Number.isFinite(intervalMinutes) && intervalMinutes > 0 ? intervalMinutes * 6e4 : Number(BACKTEST_INTERVAL) * 6e4;
3318
+ };
1266
3319
  var splitCandlesForTesting = (candles, start, preloadStart) => {
1267
3320
  const prevData = [];
1268
3321
  const testData = [];
@@ -1276,9 +3329,53 @@ var splitCandlesForTesting = (candles, start, preloadStart) => {
1276
3329
  }
1277
3330
  return { prevData, testData };
1278
3331
  };
3332
+ var getCurrentOpenTimestamp = (interval) => {
3333
+ const intervalMs = resolveIntervalMs(interval);
3334
+ return Math.floor(Date.now() / intervalMs) * intervalMs;
3335
+ };
3336
+ var filterClosedAlignedCandles = (data, btcData, interval) => {
3337
+ const currentOpenTimestamp = getCurrentOpenTimestamp(interval);
3338
+ const closedData = [];
3339
+ const closedBtcData = [];
3340
+ for (let index = 0; index < data.length; index += 1) {
3341
+ const candle = data[index];
3342
+ const btcCandle = btcData[index];
3343
+ if (!candle || !btcCandle || candle.timestamp >= currentOpenTimestamp) {
3344
+ continue;
3345
+ }
3346
+ closedData.push(candle);
3347
+ closedBtcData.push(btcCandle);
3348
+ }
3349
+ return {
3350
+ data: closedData,
3351
+ btcData: closedBtcData
3352
+ };
3353
+ };
3354
+ var shouldLoadBacktestExecutionCandles = (interval) => {
3355
+ const executionInterval = resolveBacktestExecutionInterval(interval);
3356
+ if (!executionInterval) {
3357
+ return false;
3358
+ }
3359
+ const primaryMs = resolveIntervalMs(interval);
3360
+ const executionMs = resolveIntervalMs(executionInterval);
3361
+ return executionMs > 0 && executionMs < primaryMs;
3362
+ };
1279
3363
  var getCachedConnector = async (params) => {
1280
- const { state, projectRoot, userName, connectorName } = params;
1281
- const cacheKey = getConnectorCacheKey({ userName, connectorName });
3364
+ const {
3365
+ state,
3366
+ projectRoot,
3367
+ userName,
3368
+ connectorName,
3369
+ universe,
3370
+ accountId,
3371
+ deploymentId
3372
+ } = params;
3373
+ const cacheKey = getConnectorCacheKey({
3374
+ userName,
3375
+ connectorName,
3376
+ universe,
3377
+ accountId
3378
+ });
1282
3379
  const cachedConnector = state.connectorCache.get(cacheKey);
1283
3380
  if (cachedConnector) {
1284
3381
  return cachedConnector;
@@ -1291,7 +3388,10 @@ var getCachedConnector = async (params) => {
1291
3388
  return void 0;
1292
3389
  }
1293
3390
  const connector = await connectorCreator({
1294
- userName
3391
+ userName,
3392
+ universe,
3393
+ accountId,
3394
+ deploymentId
1295
3395
  });
1296
3396
  state.connectorCache.set(cacheKey, connector);
1297
3397
  return connector;
@@ -1306,115 +3406,74 @@ var resetTestingKlineCache = (cwd) => {
1306
3406
  getTradejsProjectCwd(normalizedCwd)
1307
3407
  );
1308
3408
  };
1309
- var testing = async ({
1310
- userName,
1311
- symbol,
1312
- options: { start, end },
1313
- name,
1314
- testId,
1315
- testSuiteId,
1316
- strategyName,
1317
- strategyConfig,
1318
- connectorName,
1319
- ml = false,
1320
- ai = false,
1321
- chunkId = "single",
1322
- timeoutMs
1323
- }) => {
1324
- if (!start) {
1325
- throw new Error("no start");
1326
- }
1327
- const preloadStart = (0, import_time.getBacktestPreloadStart)(start);
1328
- const startedAt = Date.now();
1329
- const formatTimeoutMessage = (stage) => `Test ${name} (${symbol}) timed out after ${timeoutMs}ms during ${stage}`;
1330
- const getRemainingTimeoutMs = (stage) => {
1331
- if (!timeoutMs || timeoutMs <= 0) {
1332
- return null;
1333
- }
1334
- const remainingMs = timeoutMs - (Date.now() - startedAt);
1335
- if (remainingMs <= 0) {
1336
- throw new Error(formatTimeoutMessage(stage));
1337
- }
1338
- return remainingMs;
1339
- };
1340
- const throwIfTimedOut = (stage) => {
1341
- getRemainingTimeoutMs(stage);
1342
- };
1343
- const withTimeout = async (stage, promise) => {
1344
- const remainingMs = getRemainingTimeoutMs(stage);
1345
- if (remainingMs == null) {
1346
- return promise;
3409
+ var releaseTestingSymbolCache = (params) => {
3410
+ const { cwd, userName, connectorName, symbol } = params;
3411
+ const { state } = getTestingKlineCacheState(cwd);
3412
+ const connectorPrefix = [userName, connectorName].join(":") + ":";
3413
+ for (const cache of [state.coinKlineCache, state.preparedDataCache]) {
3414
+ for (const key of cache.keys()) {
3415
+ if (key.startsWith(connectorPrefix) && key.split(":").includes(symbol)) {
3416
+ cache.delete(key);
3417
+ }
1347
3418
  }
1348
- return await new Promise((resolve, reject) => {
1349
- const timer = setTimeout(() => {
1350
- reject(new Error(formatTimeoutMessage(stage)));
1351
- }, remainingMs);
1352
- promise.then(
1353
- (value) => {
1354
- clearTimeout(timer);
1355
- resolve(value);
1356
- },
1357
- (error) => {
1358
- clearTimeout(timer);
1359
- reject(error);
1360
- }
1361
- );
1362
- });
1363
- };
1364
- const { projectRoot, state } = getTestingKlineCacheState();
1365
- const connector = await withTimeout(
1366
- "connector init",
1367
- getCachedConnector({
1368
- state,
1369
- projectRoot,
1370
- userName,
1371
- connectorName
1372
- })
1373
- );
1374
- if (!connector) {
1375
- throw new Error(`Unknown connector: ${connectorName}`);
1376
- }
1377
- const strategyCreator = await withTimeout(
1378
- "strategy lookup",
1379
- getStrategyCreator(strategyName, projectRoot)
1380
- );
1381
- if (!strategyCreator) {
1382
- throw new Error(`Unknown strategy: ${strategyName}`);
1383
- }
1384
- const binanceConnector = await withTimeout(
1385
- "binance connector init",
1386
- getCachedConnector({
1387
- state,
1388
- projectRoot,
1389
- userName,
1390
- connectorName: BUILTIN_CONNECTOR_NAMES.Binance
1391
- })
1392
- );
1393
- const coinbaseConnector = await withTimeout(
1394
- "coinbase connector init",
1395
- getCachedConnector({
1396
- state,
1397
- projectRoot,
1398
- userName,
1399
- connectorName: BUILTIN_CONNECTOR_NAMES.Coinbase
1400
- })
1401
- );
1402
- if (!binanceConnector || !coinbaseConnector) {
1403
- import_logger5.logger.warn(
1404
- "Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
1405
- connectorName
1406
- );
1407
3419
  }
1408
- const interval = "15";
1409
- const cacheOnly = true;
1410
- const coinCacheKey = getKlineCacheKey({
3420
+ };
3421
+ var prepareTestingData = async (params) => {
3422
+ const {
3423
+ state,
3424
+ projectRoot,
1411
3425
  userName,
1412
3426
  connectorName,
1413
3427
  symbol,
1414
3428
  preloadStart,
3429
+ start,
1415
3430
  end,
1416
3431
  interval,
1417
- cacheOnly
3432
+ universe = "crypto",
3433
+ accountId,
3434
+ deploymentId
3435
+ } = params;
3436
+ const binanceConnector = universe === "crypto" ? await getCachedConnector({
3437
+ state,
3438
+ projectRoot,
3439
+ userName,
3440
+ connectorName: BUILTIN_CONNECTOR_NAMES.Binance
3441
+ }) : void 0;
3442
+ const coinbaseConnector = universe === "crypto" ? await getCachedConnector({
3443
+ state,
3444
+ projectRoot,
3445
+ userName,
3446
+ connectorName: BUILTIN_CONNECTOR_NAMES.Coinbase
3447
+ }) : void 0;
3448
+ const cacheOnly = true;
3449
+ const connector = await getCachedConnector({
3450
+ state,
3451
+ projectRoot,
3452
+ userName,
3453
+ connectorName,
3454
+ universe,
3455
+ accountId,
3456
+ deploymentId
3457
+ });
3458
+ if (!connector) {
3459
+ throw new Error(`Unknown connector: ${connectorName}`);
3460
+ }
3461
+ if (!binanceConnector || !coinbaseConnector) {
3462
+ import_logger7.logger.warn(
3463
+ "Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
3464
+ connectorName
3465
+ );
3466
+ }
3467
+ const coinCacheKey = getKlineCacheKey({
3468
+ userName,
3469
+ connectorName,
3470
+ symbol,
3471
+ preloadStart,
3472
+ end,
3473
+ interval,
3474
+ cacheOnly,
3475
+ universe,
3476
+ accountId
1418
3477
  });
1419
3478
  const btcCacheKey = getKlineCacheKey({
1420
3479
  userName,
@@ -1423,12 +3482,23 @@ var testing = async ({
1423
3482
  preloadStart,
1424
3483
  end,
1425
3484
  interval,
1426
- cacheOnly
3485
+ cacheOnly,
3486
+ universe,
3487
+ accountId
3488
+ });
3489
+ const ethCacheKey = getKlineCacheKey({
3490
+ userName,
3491
+ connectorName,
3492
+ symbol: "ETHUSDT",
3493
+ preloadStart,
3494
+ end,
3495
+ interval,
3496
+ cacheOnly,
3497
+ universe,
3498
+ accountId
1427
3499
  });
1428
3500
  const btcBinanceConnectorName = binanceConnector ? BUILTIN_CONNECTOR_NAMES.Binance : connectorName;
1429
3501
  const btcCoinbaseConnectorName = coinbaseConnector ? BUILTIN_CONNECTOR_NAMES.Coinbase : connectorName;
1430
- const cachedCoinData = state.coinKlineCache.get(coinCacheKey);
1431
- const cachedBtcData = state.btcKlineCache.get(btcCacheKey);
1432
3502
  const btcBinanceCacheKey = getKlineCacheKey({
1433
3503
  userName,
1434
3504
  connectorName: btcBinanceConnectorName,
@@ -1436,7 +3506,8 @@ var testing = async ({
1436
3506
  preloadStart,
1437
3507
  end,
1438
3508
  interval,
1439
- cacheOnly
3509
+ cacheOnly,
3510
+ universe: "crypto"
1440
3511
  });
1441
3512
  const btcCoinbaseCacheKey = getKlineCacheKey({
1442
3513
  userName,
@@ -1445,10 +3516,41 @@ var testing = async ({
1445
3516
  preloadStart,
1446
3517
  end,
1447
3518
  interval,
1448
- cacheOnly
3519
+ cacheOnly,
3520
+ universe: "crypto"
1449
3521
  });
3522
+ const cachedCoinData = state.coinKlineCache.get(coinCacheKey);
3523
+ const cachedBtcData = state.btcKlineCache.get(btcCacheKey);
3524
+ const cachedEthData = state.ethKlineCache.get(ethCacheKey);
1450
3525
  const cachedBtcBinanceData = state.btcBinanceKlineCache.get(btcBinanceCacheKey);
1451
3526
  const cachedBtcCoinbaseData = state.btcCoinbaseKlineCache.get(btcCoinbaseCacheKey);
3527
+ const backtestExecutionInterval = resolveBacktestExecutionInterval(interval);
3528
+ const backtestExecutionCacheInterval = backtestExecutionInterval ?? interval;
3529
+ const shouldLoadExecutionCandles = backtestExecutionInterval != null && shouldLoadBacktestExecutionCandles(interval);
3530
+ const executionCoinCacheKey = getKlineCacheKey({
3531
+ userName,
3532
+ connectorName,
3533
+ symbol,
3534
+ preloadStart,
3535
+ end,
3536
+ interval: backtestExecutionCacheInterval,
3537
+ cacheOnly,
3538
+ universe,
3539
+ accountId
3540
+ });
3541
+ const executionBtcCacheKey = getKlineCacheKey({
3542
+ userName,
3543
+ connectorName,
3544
+ symbol: "BTCUSDT",
3545
+ preloadStart,
3546
+ end,
3547
+ interval: backtestExecutionCacheInterval,
3548
+ cacheOnly,
3549
+ universe,
3550
+ accountId
3551
+ });
3552
+ const cachedExecutionCoinData = shouldLoadExecutionCandles ? state.coinKlineCache.get(executionCoinCacheKey) : void 0;
3553
+ const cachedExecutionBtcData = shouldLoadExecutionCandles ? state.btcKlineCache.get(executionBtcCacheKey) : void 0;
1452
3554
  const preparedDataCacheKey = getPreparedDataCacheKey({
1453
3555
  userName,
1454
3556
  connectorName,
@@ -1457,118 +3559,407 @@ var testing = async ({
1457
3559
  start,
1458
3560
  end,
1459
3561
  interval,
3562
+ backtestExecutionInterval: backtestExecutionCacheInterval,
1460
3563
  btcBinanceConnectorName,
1461
- btcCoinbaseConnectorName
3564
+ btcCoinbaseConnectorName,
3565
+ universe,
3566
+ accountId
1462
3567
  });
1463
- let preparedData = state.preparedDataCache.get(preparedDataCacheKey);
1464
- if (!preparedData) {
1465
- const [data, btcData, btcBinanceData, btcCoinbaseData] = await withTimeout(
1466
- "kline preload",
1467
- Promise.all([
1468
- cachedCoinData ? Promise.resolve(cachedCoinData) : connector.kline({
1469
- symbol,
1470
- start: preloadStart,
1471
- end,
1472
- interval,
1473
- silent: true,
1474
- cacheOnly
1475
- }),
1476
- cachedBtcData ? Promise.resolve(cachedBtcData) : connector.kline({
1477
- symbol: "BTCUSDT",
1478
- start: preloadStart,
1479
- end,
1480
- interval,
1481
- silent: true,
1482
- cacheOnly
1483
- }),
1484
- cachedBtcBinanceData ? Promise.resolve(cachedBtcBinanceData) : (binanceConnector ?? connector).kline({
1485
- symbol: "BTCUSDT",
1486
- start: preloadStart,
1487
- end,
1488
- interval,
1489
- silent: true,
1490
- cacheOnly
1491
- }),
1492
- cachedBtcCoinbaseData ? Promise.resolve(cachedBtcCoinbaseData) : (coinbaseConnector ?? connector).kline({
1493
- symbol: "BTCUSDT",
1494
- start: preloadStart,
1495
- end,
1496
- interval,
1497
- silent: true,
1498
- cacheOnly
1499
- })
1500
- ])
1501
- );
1502
- if (!cachedCoinData) {
1503
- state.coinKlineCache.set(coinCacheKey, data);
1504
- }
1505
- if (!cachedBtcData) {
1506
- state.btcKlineCache.set(btcCacheKey, btcData);
3568
+ const cachedPreparedData = state.preparedDataCache.get(preparedDataCacheKey);
3569
+ if (cachedPreparedData) {
3570
+ return cachedPreparedData;
3571
+ }
3572
+ const btcDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedBtcData ? Promise.resolve(cachedBtcData) : connector.kline({
3573
+ symbol: "BTCUSDT",
3574
+ start: preloadStart,
3575
+ end,
3576
+ interval,
3577
+ silent: true,
3578
+ cacheOnly
3579
+ });
3580
+ const ethDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedEthData ? Promise.resolve(cachedEthData) : connector.kline({
3581
+ symbol: "ETHUSDT",
3582
+ start: preloadStart,
3583
+ end,
3584
+ interval,
3585
+ silent: true,
3586
+ cacheOnly
3587
+ });
3588
+ const btcBinanceDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedBtcBinanceData ? Promise.resolve(cachedBtcBinanceData) : btcBinanceCacheKey === btcCacheKey ? btcDataPromise : (binanceConnector ?? connector).kline({
3589
+ symbol: "BTCUSDT",
3590
+ start: preloadStart,
3591
+ end,
3592
+ interval,
3593
+ silent: true,
3594
+ cacheOnly
3595
+ });
3596
+ const btcCoinbaseDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedBtcCoinbaseData ? Promise.resolve(cachedBtcCoinbaseData) : btcCoinbaseCacheKey === btcCacheKey ? btcDataPromise : (coinbaseConnector ?? connector).kline({
3597
+ symbol: "BTCUSDT",
3598
+ start: preloadStart,
3599
+ end,
3600
+ interval,
3601
+ silent: true,
3602
+ cacheOnly
3603
+ });
3604
+ const executionDataPromise = !shouldLoadExecutionCandles ? Promise.resolve([]) : cachedExecutionCoinData ? Promise.resolve(cachedExecutionCoinData) : connector.kline({
3605
+ symbol,
3606
+ start: preloadStart,
3607
+ end,
3608
+ interval: backtestExecutionInterval,
3609
+ silent: true,
3610
+ cacheOnly
3611
+ });
3612
+ const executionBtcDataPromise = universe === "tradfi" || !shouldLoadExecutionCandles ? Promise.resolve([]) : cachedExecutionBtcData ? Promise.resolve(cachedExecutionBtcData) : connector.kline({
3613
+ symbol: "BTCUSDT",
3614
+ start: preloadStart,
3615
+ end,
3616
+ interval: backtestExecutionInterval,
3617
+ silent: true,
3618
+ cacheOnly
3619
+ });
3620
+ const [
3621
+ dataRaw,
3622
+ btcDataRaw,
3623
+ ethDataRaw,
3624
+ btcBinanceDataRaw,
3625
+ btcCoinbaseDataRaw,
3626
+ executionDataRaw,
3627
+ executionBtcDataRaw
3628
+ ] = await Promise.all([
3629
+ cachedCoinData ? Promise.resolve(cachedCoinData) : connector.kline({
3630
+ symbol,
3631
+ start: preloadStart,
3632
+ end,
3633
+ interval,
3634
+ silent: true,
3635
+ cacheOnly
3636
+ }),
3637
+ btcDataPromise,
3638
+ ethDataPromise,
3639
+ btcBinanceDataPromise,
3640
+ btcCoinbaseDataPromise,
3641
+ executionDataPromise,
3642
+ executionBtcDataPromise
3643
+ ]);
3644
+ if (!cachedCoinData) {
3645
+ state.coinKlineCache.set(coinCacheKey, dataRaw);
3646
+ }
3647
+ if (!cachedBtcData) {
3648
+ state.btcKlineCache.set(btcCacheKey, btcDataRaw);
3649
+ }
3650
+ if (!cachedEthData) {
3651
+ state.ethKlineCache.set(ethCacheKey, ethDataRaw);
3652
+ }
3653
+ if (!cachedBtcBinanceData) {
3654
+ state.btcBinanceKlineCache.set(btcBinanceCacheKey, btcBinanceDataRaw);
3655
+ }
3656
+ if (!cachedBtcCoinbaseData) {
3657
+ state.btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseDataRaw);
3658
+ }
3659
+ if (shouldLoadExecutionCandles && !cachedExecutionCoinData) {
3660
+ state.coinKlineCache.set(executionCoinCacheKey, executionDataRaw);
3661
+ }
3662
+ if (shouldLoadExecutionCandles && !cachedExecutionBtcData) {
3663
+ state.btcKlineCache.set(executionBtcCacheKey, executionBtcDataRaw);
3664
+ }
3665
+ const aligned = universe === "tradfi" ? { alignedCoinCandles: dataRaw, alignedBtcCandles: dataRaw } : (0, import_indicators3.alignSortedCandlesByTimestamp)(dataRaw, btcDataRaw);
3666
+ const { data, btcData } = filterClosedAlignedCandles(
3667
+ aligned.alignedCoinCandles,
3668
+ aligned.alignedBtcCandles,
3669
+ interval
3670
+ );
3671
+ const btcBinanceData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, btcBinanceDataRaw).alignedBtcCandles;
3672
+ const btcCoinbaseData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, btcCoinbaseDataRaw).alignedBtcCandles;
3673
+ const ethData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, ethDataRaw).alignedBtcCandles;
3674
+ const alignedExecution = shouldLoadExecutionCandles ? universe === "tradfi" ? {
3675
+ alignedCoinCandles: executionDataRaw,
3676
+ alignedBtcCandles: executionDataRaw
3677
+ } : (0, import_indicators3.alignSortedCandlesByTimestamp)(executionDataRaw, executionBtcDataRaw) : { alignedCoinCandles: [], alignedBtcCandles: [] };
3678
+ const { data: backtestExecutionData, btcData: backtestExecutionBtcData } = filterClosedAlignedCandles(
3679
+ alignedExecution.alignedCoinCandles,
3680
+ alignedExecution.alignedBtcCandles,
3681
+ backtestExecutionCacheInterval
3682
+ );
3683
+ const backtestExecutionDataByTimestamp = buildCandleByTimestamp(
3684
+ backtestExecutionData
3685
+ );
3686
+ const backtestExecutionBtcDataByTimestamp = buildCandleByTimestamp(
3687
+ backtestExecutionBtcData
3688
+ );
3689
+ const { prevData, testData } = splitCandlesForTesting(
3690
+ data,
3691
+ start,
3692
+ preloadStart
3693
+ );
3694
+ const { prevData: btcPrevData, testData: btcTestData } = splitCandlesForTesting(btcData, start, preloadStart);
3695
+ const { prevData: ethPrevData, testData: ethTestData } = splitCandlesForTesting(ethData, start, preloadStart);
3696
+ const preparedData = {
3697
+ data,
3698
+ btcData,
3699
+ ethData,
3700
+ prevData,
3701
+ btcPrevData,
3702
+ ethPrevData,
3703
+ testData,
3704
+ btcTestData,
3705
+ ethTestData,
3706
+ btcBinanceData,
3707
+ btcCoinbaseData,
3708
+ backtestExecutionInterval: backtestExecutionCacheInterval,
3709
+ backtestExecutionData,
3710
+ backtestExecutionBtcData,
3711
+ backtestExecutionDataByTimestamp,
3712
+ backtestExecutionBtcDataByTimestamp
3713
+ };
3714
+ state.preparedDataCache.set(preparedDataCacheKey, preparedData);
3715
+ return preparedData;
3716
+ };
3717
+ var canRunTestsInSharedCandleLoop = (tests) => {
3718
+ if (tests.length <= 1) {
3719
+ return false;
3720
+ }
3721
+ const first = tests[0];
3722
+ const firstStart = first.options?.start;
3723
+ const firstEnd = first.options?.end;
3724
+ return tests.every(
3725
+ (test) => test.userName === first.userName && test.connectorName === first.connectorName && (test.universe ?? "crypto") === (first.universe ?? "crypto") && (test.accountId ?? null) === (first.accountId ?? null) && (test.deploymentId ?? null) === (first.deploymentId ?? null) && test.symbol === first.symbol && test.strategyName === first.strategyName && test.options?.start === firstStart && test.options?.end === firstEnd && Boolean(test.ml) === Boolean(first.ml) && Boolean(test.ai) === Boolean(first.ai) && Boolean(test.fast) === Boolean(first.fast) && Boolean(test.collectReplaySignalEvaluations) === Boolean(first.collectReplaySignalEvaluations) && (test.interval ?? BACKTEST_INTERVAL) === (first.interval ?? BACKTEST_INTERVAL) && (test.timeoutMs ?? null) === (first.timeoutMs ?? null)
3726
+ );
3727
+ };
3728
+ var testing = async ({
3729
+ userName,
3730
+ symbol,
3731
+ options: { start, end },
3732
+ name,
3733
+ testId,
3734
+ testSuiteId,
3735
+ configId,
3736
+ strategyName,
3737
+ strategyConfig,
3738
+ connectorName,
3739
+ universe = "crypto",
3740
+ assetClass,
3741
+ instrument: requestedInstrument,
3742
+ accountId,
3743
+ deploymentId,
3744
+ policyProfileId,
3745
+ interval = BACKTEST_INTERVAL,
3746
+ ml = false,
3747
+ ai = false,
3748
+ fast = false,
3749
+ collectReplaySignalEvaluations = false,
3750
+ chunkId = "single",
3751
+ backtestRunId,
3752
+ backtestTestKey,
3753
+ timeoutMs
3754
+ }) => {
3755
+ if (!start) {
3756
+ throw new Error("no start");
3757
+ }
3758
+ const preloadStart = (0, import_time.getBacktestPreloadStart)(start);
3759
+ const startedAt = Date.now();
3760
+ let activeStageStartedAt = startedAt;
3761
+ let lastProgressSentAt = 0;
3762
+ let lastProgressSignature = "";
3763
+ let currentCandleIndex = 0;
3764
+ let totalCandles = 0;
3765
+ const strategyCandleTimeoutMs = getStrategyCandleTimeoutMs();
3766
+ const formatTimeoutMessage = (stage, stageTimeoutMs) => `Test ${name} (${symbol}) timed out after ${stageTimeoutMs}ms during ${stage}`;
3767
+ const emitProgress = (stage, options = {}) => {
3768
+ const now = Date.now();
3769
+ const candleIndex = typeof options.candleIndex === "number" ? options.candleIndex : currentCandleIndex;
3770
+ const candleTotal = typeof options.candleTotal === "number" ? options.candleTotal : totalCandles;
3771
+ const signature = [
3772
+ stage,
3773
+ candleIndex,
3774
+ candleTotal,
3775
+ Math.floor((now - activeStageStartedAt) / 5e3)
3776
+ ].join(":");
3777
+ if (!options.force) {
3778
+ if (signature === lastProgressSignature) {
3779
+ return;
3780
+ }
3781
+ if (now - lastProgressSentAt < 4e3) {
3782
+ return;
3783
+ }
1507
3784
  }
1508
- if (!cachedBtcBinanceData) {
1509
- state.btcBinanceKlineCache.set(btcBinanceCacheKey, btcBinanceData);
3785
+ lastProgressSentAt = now;
3786
+ lastProgressSignature = signature;
3787
+ process.send?.({
3788
+ progress: true,
3789
+ testName: name,
3790
+ symbol,
3791
+ strategyName,
3792
+ stage,
3793
+ candleIndex,
3794
+ candleTotal,
3795
+ elapsedMs: now - startedAt,
3796
+ stageElapsedMs: now - activeStageStartedAt
3797
+ });
3798
+ };
3799
+ const getStageTimeoutMs = () => {
3800
+ if (!timeoutMs || timeoutMs <= 0) {
3801
+ return null;
1510
3802
  }
1511
- if (!cachedBtcCoinbaseData) {
1512
- state.btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseData);
3803
+ return timeoutMs;
3804
+ };
3805
+ const throwIfTimedOut = (stage) => {
3806
+ if (getStageTimeoutMs() == null) {
3807
+ return;
1513
3808
  }
1514
- const { prevData: prevDataRaw, testData: testDataRaw } = splitCandlesForTesting(data, start, preloadStart);
1515
- const { prevData: btcPrevDataRaw, testData: btcTestDataRaw } = splitCandlesForTesting(btcData, start, preloadStart);
1516
- const { prevData: btcBinancePrevDataRaw } = splitCandlesForTesting(
1517
- btcBinanceData,
1518
- start,
1519
- preloadStart
1520
- );
1521
- const { prevData: btcCoinbasePrevDataRaw } = splitCandlesForTesting(
1522
- btcCoinbaseData,
1523
- start,
1524
- preloadStart
3809
+ emitProgress(stage);
3810
+ };
3811
+ const withTimeout = async (stage, promise, stageTimeoutOverrideMs = null) => {
3812
+ const stageTimeoutMs = getEffectiveTimeoutMs(
3813
+ getStageTimeoutMs() ?? void 0,
3814
+ stageTimeoutOverrideMs
1525
3815
  );
1526
- const { alignedCoinCandles: prevData2, alignedBtcCandles: btcPrevData2 } = (0, import_indicators3.alignSortedCandlesByTimestamp)(prevDataRaw, btcPrevDataRaw);
1527
- const { alignedCoinCandles: testData2, alignedBtcCandles: btcTestData2 } = (0, import_indicators3.alignSortedCandlesByTimestamp)(testDataRaw, btcTestDataRaw);
1528
- const { alignedBtcCandles: btcBinancePrevData2 } = (0, import_indicators3.alignSortedCandlesByTimestamp)(prevDataRaw, btcBinancePrevDataRaw);
1529
- const { alignedBtcCandles: btcCoinbasePrevData2 } = (0, import_indicators3.alignSortedCandlesByTimestamp)(prevDataRaw, btcCoinbasePrevDataRaw);
1530
- preparedData = {
1531
- prevData: prevData2,
1532
- btcPrevData: btcPrevData2,
1533
- testData: testData2,
1534
- btcTestData: btcTestData2,
1535
- btcBinancePrevData: btcBinancePrevData2,
1536
- btcCoinbasePrevData: btcCoinbasePrevData2
1537
- };
1538
- state.preparedDataCache.set(preparedDataCacheKey, preparedData);
3816
+ if (stageTimeoutMs == null) {
3817
+ return promise;
3818
+ }
3819
+ activeStageStartedAt = Date.now();
3820
+ emitProgress(stage, { force: true });
3821
+ return await new Promise((resolve, reject) => {
3822
+ const heartbeat = setInterval(() => {
3823
+ emitProgress(stage);
3824
+ }, 5e3);
3825
+ const timer = setTimeout(() => {
3826
+ clearInterval(heartbeat);
3827
+ reject(new Error(formatTimeoutMessage(stage, stageTimeoutMs)));
3828
+ }, stageTimeoutMs);
3829
+ promise.then(
3830
+ (value) => {
3831
+ clearInterval(heartbeat);
3832
+ clearTimeout(timer);
3833
+ resolve(value);
3834
+ },
3835
+ (error) => {
3836
+ clearInterval(heartbeat);
3837
+ clearTimeout(timer);
3838
+ reject(error);
3839
+ }
3840
+ );
3841
+ });
3842
+ };
3843
+ const runStage = (stage, fn) => {
3844
+ if (getStageTimeoutMs() == null) {
3845
+ return fn();
3846
+ }
3847
+ return withTimeout(stage, fn());
3848
+ };
3849
+ const runStrategyCandleStage = (stage, fn) => withTimeout(stage, fn(), strategyCandleTimeoutMs);
3850
+ const { projectRoot, state } = getTestingKlineCacheState();
3851
+ const connector = await withTimeout(
3852
+ "connector init",
3853
+ getCachedConnector({
3854
+ state,
3855
+ projectRoot,
3856
+ userName,
3857
+ connectorName,
3858
+ universe,
3859
+ accountId,
3860
+ deploymentId
3861
+ })
3862
+ );
3863
+ if (!connector) {
3864
+ throw new Error(`Unknown connector: ${connectorName}`);
3865
+ }
3866
+ const strategyCreator = await withTimeout(
3867
+ "strategy lookup",
3868
+ getStrategyCreator(strategyName, projectRoot)
3869
+ );
3870
+ if (!strategyCreator) {
3871
+ throw new Error(`Unknown strategy: ${strategyName}`);
1539
3872
  }
3873
+ const preparedData = await withTimeout(
3874
+ "kline preload",
3875
+ prepareTestingData({
3876
+ state,
3877
+ projectRoot,
3878
+ userName,
3879
+ connectorName,
3880
+ symbol,
3881
+ preloadStart,
3882
+ start,
3883
+ end,
3884
+ interval,
3885
+ universe,
3886
+ accountId,
3887
+ deploymentId
3888
+ })
3889
+ );
1540
3890
  if (!preparedData) {
1541
3891
  throw new Error("Prepared backtest data not available");
1542
3892
  }
1543
3893
  const {
1544
3894
  prevData,
1545
3895
  btcPrevData,
3896
+ ethPrevData,
3897
+ ethTestData,
1546
3898
  testData,
1547
3899
  btcTestData,
1548
- btcBinancePrevData,
1549
- btcCoinbasePrevData
3900
+ btcBinanceData,
3901
+ btcCoinbaseData,
3902
+ backtestExecutionInterval,
3903
+ backtestExecutionData,
3904
+ backtestExecutionBtcData,
3905
+ backtestExecutionDataByTimestamp,
3906
+ backtestExecutionBtcDataByTimestamp
1550
3907
  } = preparedData;
3908
+ const runtimePrevData = prevData.slice();
3909
+ const runtimeBtcPrevData = btcPrevData.slice();
3910
+ const runtimeEthData = [...ethPrevData, ...ethTestData];
3911
+ totalCandles = testData.length;
3912
+ const instrument = requestedInstrument;
3913
+ const { model: executionCostModel, fundingRates } = await resolveExecutionCosts({
3914
+ connector,
3915
+ symbol,
3916
+ config: strategyConfig,
3917
+ startTime: start,
3918
+ endTime: end,
3919
+ instrument
3920
+ });
1551
3921
  const testConnector = createTestConnector(connector, {
1552
3922
  userName,
1553
3923
  mlEnabled: ml,
1554
- aiEnabled: ai
3924
+ aiEnabled: ai,
3925
+ fastMode: fast,
3926
+ executionCostModel,
3927
+ fundingRates
1555
3928
  });
1556
3929
  const strategy = await withTimeout(
1557
3930
  "strategy init",
1558
3931
  strategyCreator({
1559
3932
  userName,
1560
- config: strategyConfig,
3933
+ connectorName,
3934
+ universe,
3935
+ assetClass: assetClass ?? instrument?.assetClass,
3936
+ instrument,
3937
+ accountId,
3938
+ deploymentId,
3939
+ policyProfileId,
3940
+ config: {
3941
+ ...strategyConfig,
3942
+ INTERVAL: interval
3943
+ },
1561
3944
  symbol,
1562
- data: prevData,
1563
- btcData: btcPrevData,
1564
- btcBinanceData: btcBinancePrevData,
1565
- btcCoinbaseData: btcCoinbasePrevData,
3945
+ data: runtimePrevData,
3946
+ btcData: runtimeBtcPrevData,
3947
+ ethData: runtimeEthData,
3948
+ btcBinanceData,
3949
+ btcCoinbaseData,
3950
+ backtestExecutionMarketData: {
3951
+ interval: backtestExecutionInterval,
3952
+ data: backtestExecutionData,
3953
+ btcData: backtestExecutionBtcData,
3954
+ dataByTimestamp: backtestExecutionDataByTimestamp,
3955
+ btcDataByTimestamp: backtestExecutionBtcDataByTimestamp
3956
+ },
1566
3957
  connector: testConnector
1567
3958
  })
1568
3959
  );
1569
3960
  const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
1570
3961
  const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
1571
- const replaySignalEvaluations = [];
3962
+ const replaySignalEvaluations = collectReplaySignalEvaluations ? [] : null;
1572
3963
  const flushClosedResultsBatch = async () => {
1573
3964
  if (!ml && !ai) return;
1574
3965
  const batch = await testConnector.drainMlResultsBatch();
@@ -1580,7 +3971,14 @@ var testing = async ({
1580
3971
  const fullRow = (0, import_ml2.buildMlTrainingRow)(payload, {
1581
3972
  profit: resultRecord.profit
1582
3973
  });
1583
- const row = (0, import_ml2.trimMlTrainingRowWindows)(fullRow, 5);
3974
+ const row = {
3975
+ ...(0, import_ml2.trimMlTrainingRowWindows)(fullRow, 5),
3976
+ ...buildBacktestDatasetMetadata({
3977
+ backtestRunId,
3978
+ backtestTestKey,
3979
+ chunkId
3980
+ })
3981
+ };
1584
3982
  await (0, import_ml2.appendMlDatasetRow)({
1585
3983
  strategyName,
1586
3984
  chunkId,
@@ -1590,62 +3988,54 @@ var testing = async ({
1590
3988
  const aiRowBase = pendingAiRowBySignalId.get(resultRecord.signalId);
1591
3989
  if (aiRowBase) {
1592
3990
  pendingAiRowBySignalId.delete(resultRecord.signalId);
3991
+ const { signal: aiSignal, ...rowBase } = aiRowBase;
1593
3992
  await (0, import_ai2.appendAiDatasetRow)({
1594
3993
  strategyName,
1595
3994
  chunkId,
1596
3995
  row: {
1597
- ...aiRowBase,
1598
- profit: resultRecord.profit
3996
+ ...rowBase,
3997
+ payload: buildAiPayload(aiSignal),
3998
+ profit: resultRecord.profit,
3999
+ tradeResult: resultRecord.tradeResult
1599
4000
  }
1600
4001
  });
1601
4002
  }
1602
4003
  }
1603
4004
  };
1604
- for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
1605
- if (candleIndex % 25 === 0) {
1606
- throwIfTimedOut("candle loop");
4005
+ const processSignal = async (signal, candle) => {
4006
+ if (isBacktestEntryDelayControlCode(signal)) {
4007
+ return;
1607
4008
  }
1608
- const candle = testData[candleIndex];
1609
- const btcCandle = btcTestData[candleIndex];
1610
- await withTimeout("stop-loss check", testConnector.checkSl(candle));
1611
- await withTimeout("take-profit check", testConnector.checkTp(candle));
1612
- const signal = await withTimeout(
1613
- "strategy signal",
1614
- strategy(candle, btcCandle)
1615
- );
1616
- if (!signal || typeof signal === "string") {
1617
- replaySignalEvaluations.push({
1618
- evaluationId: `${testId}:${strategyName}:${symbol}:${candle.timestamp}`,
1619
- userName,
1620
- strategy: strategyName,
1621
- symbol,
1622
- interval,
1623
- timestamp: candle.timestamp,
1624
- evaluatedAt: candle.timestamp,
1625
- status: "skip",
1626
- reason: typeof signal === "string" && signal.trim() ? signal : "NO_SIGNAL"
1627
- });
1628
- } else {
1629
- replaySignalEvaluations.push({
1630
- evaluationId: `${signal.signalId || testId}:${strategyName}:${symbol}:${signal.timestamp || candle.timestamp}`,
1631
- userName,
1632
- strategy: signal.strategy || strategyName,
1633
- symbol: signal.symbol || symbol,
1634
- interval: signal.interval || interval,
1635
- timestamp: typeof signal.timestamp === "number" && Number.isFinite(signal.timestamp) ? signal.timestamp : candle.timestamp,
1636
- evaluatedAt: candle.timestamp,
1637
- status: "signal",
1638
- reason: signal.orderSkipReason || signal.orderStatus,
1639
- signalId: signal.signalId,
1640
- direction: signal.direction,
1641
- orderStatus: signal.orderStatus,
1642
- orderSkipReason: signal.orderSkipReason,
1643
- aiAnalysis: signal.aiAnalysis ?? null,
1644
- ml: signal.ml
1645
- });
4009
+ if (replaySignalEvaluations) {
4010
+ replaySignalEvaluations.push(
4011
+ buildReplaySignalEvaluationRecord({
4012
+ signal,
4013
+ testId,
4014
+ userName,
4015
+ strategyName,
4016
+ symbol,
4017
+ interval,
4018
+ candle
4019
+ })
4020
+ );
1646
4021
  }
1647
4022
  const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (ml || ai);
1648
4023
  if (shouldCapturePayload) {
4024
+ await withTimeout(
4025
+ "binance market context",
4026
+ enrichSignalWithBinanceMarketContext({
4027
+ signal,
4028
+ env: "BACKTEST"
4029
+ })
4030
+ );
4031
+ await withTimeout(
4032
+ "coinmarketcap context",
4033
+ enrichSignalWithCoinMarketCapContext({
4034
+ signal,
4035
+ env: "BACKTEST",
4036
+ enabled: Boolean(ml || ai)
4037
+ })
4038
+ );
1649
4039
  await withTimeout(
1650
4040
  "derivatives context",
1651
4041
  enrichSignalWithDerivativesContext({
@@ -1662,6 +4052,7 @@ var testing = async ({
1662
4052
  testId,
1663
4053
  testSuiteId,
1664
4054
  testName: name,
4055
+ configId,
1665
4056
  symbol,
1666
4057
  strategyName,
1667
4058
  strategyConfig,
@@ -1677,25 +4068,517 @@ var testing = async ({
1677
4068
  symbol: signal.symbol || symbol,
1678
4069
  direction: signal.direction,
1679
4070
  timestamp: signal.timestamp,
1680
- payload: buildAiPayload(signal),
4071
+ signal: cloneAiPayloadSignal(signal),
1681
4072
  testId,
1682
4073
  testSuiteId,
1683
4074
  testName: name,
1684
- connectorName
4075
+ configId,
4076
+ connectorName,
4077
+ ...buildBacktestDatasetMetadata({
4078
+ backtestRunId,
4079
+ backtestTestKey,
4080
+ chunkId
4081
+ })
1685
4082
  });
1686
4083
  }
4084
+ };
4085
+ for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
4086
+ if (candleIndex % 25 === 0) {
4087
+ throwIfTimedOut("candle loop");
4088
+ }
4089
+ currentCandleIndex = candleIndex + 1;
4090
+ emitProgress("candle loop", {
4091
+ force: candleIndex === 0 || currentCandleIndex === totalCandles
4092
+ });
4093
+ const candle = testData[candleIndex];
4094
+ const btcCandle = btcTestData[candleIndex];
4095
+ const delayedSignal = await runStrategyCandleStage(
4096
+ "delayed entry",
4097
+ async () => strategy.__tradejsFlushBacktestDelayedEntry?.(candle, btcCandle)
4098
+ );
4099
+ if (delayedSignal && typeof delayedSignal !== "string") {
4100
+ await processSignal(delayedSignal, candle);
4101
+ }
4102
+ await runStage("exit checks", () => testConnector.checkExits(candle));
4103
+ const signal = await runStrategyCandleStage(
4104
+ "strategy signal",
4105
+ () => strategy(candle, btcCandle)
4106
+ );
4107
+ await processSignal(signal, candle);
4108
+ if ((candleIndex + 1) % CLOSED_RESULT_FLUSH_INTERVAL === 0) {
4109
+ await withTimeout("flush closed results", flushClosedResultsBatch());
4110
+ }
1687
4111
  }
1688
4112
  await withTimeout("flush closed results", flushClosedResultsBatch());
1689
4113
  const result = await withTimeout("collect result", testConnector.getResult());
1690
- return {
4114
+ return replaySignalEvaluations ? {
1691
4115
  ...result,
1692
4116
  inlineReplaySignalEvaluations: replaySignalEvaluations
4117
+ } : result;
4118
+ };
4119
+ var testingGroupInSharedCandleLoop = async (tests) => {
4120
+ if (!canRunTestsInSharedCandleLoop(tests)) {
4121
+ const results = [];
4122
+ for (const test of tests) {
4123
+ const result = await testing(test);
4124
+ if (result) {
4125
+ results.push({ test, result });
4126
+ }
4127
+ }
4128
+ return results;
4129
+ }
4130
+ const first = tests[0];
4131
+ const {
4132
+ userName,
4133
+ symbol,
4134
+ options: { start, end },
4135
+ strategyName,
4136
+ connectorName,
4137
+ universe = "crypto",
4138
+ accountId,
4139
+ deploymentId,
4140
+ interval = BACKTEST_INTERVAL,
4141
+ ml = false,
4142
+ ai = false,
4143
+ fast = false,
4144
+ collectReplaySignalEvaluations = false,
4145
+ chunkId = "single",
4146
+ timeoutMs
4147
+ } = first;
4148
+ if (!start) {
4149
+ throw new Error("no start");
4150
+ }
4151
+ const preloadStart = (0, import_time.getBacktestPreloadStart)(start);
4152
+ const startedAt = Date.now();
4153
+ let activeStageStartedAt = startedAt;
4154
+ let lastProgressSentAt = 0;
4155
+ let lastProgressSignature = "";
4156
+ let currentCandleIndex = 0;
4157
+ let totalCandles = 0;
4158
+ const strategyCandleTimeoutMs = getStrategyCandleTimeoutMs();
4159
+ const formatTimeoutMessage = (stage, stageTimeoutMs) => `Test group ${strategyName}/${symbol} timed out after ${stageTimeoutMs}ms during ${stage}`;
4160
+ const emitProgress = (stage, options = {}) => {
4161
+ const now = Date.now();
4162
+ const candleIndex = typeof options.candleIndex === "number" ? options.candleIndex : currentCandleIndex;
4163
+ const candleTotal = typeof options.candleTotal === "number" ? options.candleTotal : totalCandles;
4164
+ const signature = [
4165
+ stage,
4166
+ candleIndex,
4167
+ candleTotal,
4168
+ Math.floor((now - activeStageStartedAt) / 5e3)
4169
+ ].join(":");
4170
+ if (!options.force) {
4171
+ if (signature === lastProgressSignature) {
4172
+ return;
4173
+ }
4174
+ if (now - lastProgressSentAt < 4e3) {
4175
+ return;
4176
+ }
4177
+ }
4178
+ lastProgressSentAt = now;
4179
+ lastProgressSignature = signature;
4180
+ process.send?.({
4181
+ progress: true,
4182
+ testName: first.name,
4183
+ symbol,
4184
+ strategyName,
4185
+ stage,
4186
+ candleIndex,
4187
+ candleTotal,
4188
+ elapsedMs: now - startedAt,
4189
+ stageElapsedMs: now - activeStageStartedAt
4190
+ });
4191
+ };
4192
+ const getStageTimeoutMs = () => {
4193
+ if (!timeoutMs || timeoutMs <= 0) {
4194
+ return null;
4195
+ }
4196
+ return timeoutMs;
4197
+ };
4198
+ const throwIfTimedOut = (stage) => {
4199
+ if (getStageTimeoutMs() == null) {
4200
+ return;
4201
+ }
4202
+ emitProgress(stage);
1693
4203
  };
4204
+ const withTimeout = async (stage, promise, stageTimeoutOverrideMs = null) => {
4205
+ const stageTimeoutMs = getEffectiveTimeoutMs(
4206
+ getStageTimeoutMs() ?? void 0,
4207
+ stageTimeoutOverrideMs
4208
+ );
4209
+ if (stageTimeoutMs == null) {
4210
+ return promise;
4211
+ }
4212
+ activeStageStartedAt = Date.now();
4213
+ emitProgress(stage, { force: true });
4214
+ return await new Promise((resolve, reject) => {
4215
+ const heartbeat = setInterval(() => {
4216
+ emitProgress(stage);
4217
+ }, 5e3);
4218
+ const timer = setTimeout(() => {
4219
+ clearInterval(heartbeat);
4220
+ reject(new Error(formatTimeoutMessage(stage, stageTimeoutMs)));
4221
+ }, stageTimeoutMs);
4222
+ promise.then(
4223
+ (value) => {
4224
+ clearInterval(heartbeat);
4225
+ clearTimeout(timer);
4226
+ resolve(value);
4227
+ },
4228
+ (error) => {
4229
+ clearInterval(heartbeat);
4230
+ clearTimeout(timer);
4231
+ reject(error);
4232
+ }
4233
+ );
4234
+ });
4235
+ };
4236
+ const runStage = (stage, fn) => {
4237
+ if (getStageTimeoutMs() == null) {
4238
+ return fn();
4239
+ }
4240
+ return withTimeout(stage, fn());
4241
+ };
4242
+ const runStrategyCandleStage = (stage, fn) => withTimeout(stage, fn(), strategyCandleTimeoutMs);
4243
+ const { projectRoot, state } = getTestingKlineCacheState();
4244
+ const connector = await withTimeout(
4245
+ "connector init",
4246
+ getCachedConnector({
4247
+ state,
4248
+ projectRoot,
4249
+ userName,
4250
+ connectorName,
4251
+ universe,
4252
+ accountId,
4253
+ deploymentId
4254
+ })
4255
+ );
4256
+ if (!connector) {
4257
+ throw new Error(`Unknown connector: ${connectorName}`);
4258
+ }
4259
+ const strategyCreator = await withTimeout(
4260
+ "strategy lookup",
4261
+ getStrategyCreator(strategyName, projectRoot)
4262
+ );
4263
+ if (!strategyCreator) {
4264
+ throw new Error(`Unknown strategy: ${strategyName}`);
4265
+ }
4266
+ const preparedData = await withTimeout(
4267
+ "kline preload",
4268
+ prepareTestingData({
4269
+ state,
4270
+ projectRoot,
4271
+ userName,
4272
+ connectorName,
4273
+ symbol,
4274
+ preloadStart,
4275
+ start,
4276
+ end,
4277
+ interval,
4278
+ universe,
4279
+ accountId,
4280
+ deploymentId
4281
+ })
4282
+ );
4283
+ if (!preparedData) {
4284
+ throw new Error("Prepared backtest data not available");
4285
+ }
4286
+ const {
4287
+ prevData,
4288
+ btcPrevData,
4289
+ ethPrevData,
4290
+ ethTestData,
4291
+ testData,
4292
+ btcTestData,
4293
+ btcBinanceData,
4294
+ btcCoinbaseData,
4295
+ backtestExecutionInterval,
4296
+ backtestExecutionData,
4297
+ backtestExecutionBtcData,
4298
+ backtestExecutionDataByTimestamp,
4299
+ backtestExecutionBtcDataByTimestamp
4300
+ } = preparedData;
4301
+ totalCandles = testData.length;
4302
+ const sharedIndicatorsReplayKey = [
4303
+ "shared",
4304
+ userName,
4305
+ connectorName,
4306
+ strategyName,
4307
+ symbol,
4308
+ interval,
4309
+ start,
4310
+ end,
4311
+ chunkId
4312
+ ].join(":");
4313
+ const runners = [];
4314
+ try {
4315
+ for (const test of tests) {
4316
+ const instrument = test.instrument;
4317
+ const { model: executionCostModel, fundingRates } = await resolveExecutionCosts({
4318
+ connector,
4319
+ symbol: test.symbol,
4320
+ config: test.strategyConfig,
4321
+ startTime: start,
4322
+ endTime: end,
4323
+ instrument
4324
+ });
4325
+ const testConnector = createTestConnector(connector, {
4326
+ userName: test.userName,
4327
+ mlEnabled: test.ml,
4328
+ aiEnabled: test.ai,
4329
+ fastMode: test.fast,
4330
+ executionCostModel,
4331
+ fundingRates
4332
+ });
4333
+ const strategy = await withTimeout(
4334
+ "strategy init",
4335
+ strategyCreator({
4336
+ userName: test.userName,
4337
+ connectorName: test.connectorName,
4338
+ universe: test.universe ?? universe,
4339
+ assetClass: test.assetClass ?? instrument?.assetClass,
4340
+ instrument,
4341
+ accountId: test.accountId ?? accountId,
4342
+ deploymentId: test.deploymentId ?? deploymentId,
4343
+ policyProfileId: test.policyProfileId,
4344
+ config: {
4345
+ ...test.strategyConfig,
4346
+ INTERVAL: test.interval ?? interval
4347
+ },
4348
+ symbol: test.symbol,
4349
+ data: prevData.slice(),
4350
+ btcData: btcPrevData.slice(),
4351
+ ethData: [...ethPrevData, ...ethTestData],
4352
+ btcBinanceData,
4353
+ btcCoinbaseData,
4354
+ backtestExecutionMarketData: {
4355
+ interval: backtestExecutionInterval,
4356
+ data: backtestExecutionData,
4357
+ btcData: backtestExecutionBtcData,
4358
+ dataByTimestamp: backtestExecutionDataByTimestamp,
4359
+ btcDataByTimestamp: backtestExecutionBtcDataByTimestamp
4360
+ },
4361
+ connector: testConnector,
4362
+ sharedIndicatorsReplayKey
4363
+ })
4364
+ );
4365
+ runners.push({
4366
+ test,
4367
+ strategy,
4368
+ testConnector,
4369
+ pendingMlPayloadBySignalId: /* @__PURE__ */ new Map(),
4370
+ pendingAiRowBySignalId: /* @__PURE__ */ new Map(),
4371
+ replaySignalEvaluations: collectReplaySignalEvaluations ? [] : null
4372
+ });
4373
+ }
4374
+ const flushClosedResultsBatch = async (runner) => {
4375
+ if (!runner.test.ml && !runner.test.ai) return;
4376
+ const batch = await runner.testConnector.drainMlResultsBatch();
4377
+ if (!batch.length) return;
4378
+ for (const resultRecord of batch) {
4379
+ const payload = runner.pendingMlPayloadBySignalId.get(
4380
+ resultRecord.signalId
4381
+ );
4382
+ if (payload) {
4383
+ runner.pendingMlPayloadBySignalId.delete(resultRecord.signalId);
4384
+ const fullRow = (0, import_ml2.buildMlTrainingRow)(payload, {
4385
+ profit: resultRecord.profit
4386
+ });
4387
+ const resolvedChunkId = runner.test.chunkId ?? "single";
4388
+ const row = {
4389
+ ...(0, import_ml2.trimMlTrainingRowWindows)(fullRow, 5),
4390
+ ...buildBacktestDatasetMetadata({
4391
+ backtestRunId: runner.test.backtestRunId,
4392
+ backtestTestKey: runner.test.backtestTestKey,
4393
+ chunkId: resolvedChunkId
4394
+ })
4395
+ };
4396
+ await (0, import_ml2.appendMlDatasetRow)({
4397
+ strategyName: runner.test.strategyName,
4398
+ chunkId: resolvedChunkId,
4399
+ row
4400
+ });
4401
+ }
4402
+ const aiRowBase = runner.pendingAiRowBySignalId.get(
4403
+ resultRecord.signalId
4404
+ );
4405
+ if (aiRowBase) {
4406
+ runner.pendingAiRowBySignalId.delete(resultRecord.signalId);
4407
+ const { signal: aiSignal, ...rowBase } = aiRowBase;
4408
+ const resolvedChunkId = runner.test.chunkId ?? "single";
4409
+ await (0, import_ai2.appendAiDatasetRow)({
4410
+ strategyName: runner.test.strategyName,
4411
+ chunkId: resolvedChunkId,
4412
+ row: {
4413
+ ...rowBase,
4414
+ payload: buildAiPayload(aiSignal),
4415
+ profit: resultRecord.profit,
4416
+ tradeResult: resultRecord.tradeResult
4417
+ }
4418
+ });
4419
+ }
4420
+ }
4421
+ };
4422
+ const processRunnerSignal = async (runner, signal, candle) => {
4423
+ if (isBacktestEntryDelayControlCode(signal)) {
4424
+ return;
4425
+ }
4426
+ const { test } = runner;
4427
+ if (runner.replaySignalEvaluations) {
4428
+ runner.replaySignalEvaluations.push(
4429
+ buildReplaySignalEvaluationRecord({
4430
+ signal,
4431
+ testId: test.testId,
4432
+ userName: test.userName,
4433
+ strategyName: test.strategyName,
4434
+ symbol: test.symbol,
4435
+ interval: test.interval ?? interval,
4436
+ candle
4437
+ })
4438
+ );
4439
+ }
4440
+ const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (test.ml || test.ai);
4441
+ if (shouldCapturePayload) {
4442
+ await withTimeout(
4443
+ "binance market context",
4444
+ enrichSignalWithBinanceMarketContext({
4445
+ signal,
4446
+ env: "BACKTEST"
4447
+ })
4448
+ );
4449
+ await withTimeout(
4450
+ "coinmarketcap context",
4451
+ enrichSignalWithCoinMarketCapContext({
4452
+ signal,
4453
+ env: "BACKTEST",
4454
+ enabled: Boolean(test.ml || test.ai)
4455
+ })
4456
+ );
4457
+ await withTimeout(
4458
+ "derivatives context",
4459
+ enrichSignalWithDerivativesContext({
4460
+ signal,
4461
+ env: "BACKTEST"
4462
+ })
4463
+ );
4464
+ }
4465
+ if (test.ml && signal && typeof signal !== "string" && signal.signalId) {
4466
+ const payload = buildMlPayload({
4467
+ signal,
4468
+ context: {
4469
+ userName: test.userName,
4470
+ testId: test.testId,
4471
+ testSuiteId: test.testSuiteId,
4472
+ testName: test.name,
4473
+ configId: test.configId,
4474
+ symbol: test.symbol,
4475
+ strategyName: test.strategyName,
4476
+ strategyConfig: test.strategyConfig,
4477
+ connectorName: test.connectorName
4478
+ }
4479
+ });
4480
+ runner.pendingMlPayloadBySignalId.set(signal.signalId, payload);
4481
+ }
4482
+ if (test.ai && signal && typeof signal !== "string" && signal.signalId) {
4483
+ runner.pendingAiRowBySignalId.set(signal.signalId, {
4484
+ signalId: signal.signalId,
4485
+ strategyName: signal.strategy || test.strategyName,
4486
+ symbol: signal.symbol || test.symbol,
4487
+ direction: signal.direction,
4488
+ timestamp: signal.timestamp,
4489
+ signal: cloneAiPayloadSignal(signal),
4490
+ testId: test.testId,
4491
+ testSuiteId: test.testSuiteId,
4492
+ testName: test.name,
4493
+ configId: test.configId,
4494
+ connectorName: test.connectorName,
4495
+ ...buildBacktestDatasetMetadata({
4496
+ backtestRunId: test.backtestRunId,
4497
+ backtestTestKey: test.backtestTestKey,
4498
+ chunkId: test.chunkId ?? "single"
4499
+ })
4500
+ });
4501
+ }
4502
+ };
4503
+ for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
4504
+ if (candleIndex % 25 === 0) {
4505
+ throwIfTimedOut("candle loop");
4506
+ }
4507
+ currentCandleIndex = candleIndex + 1;
4508
+ emitProgress("candle loop", {
4509
+ force: candleIndex === 0 || currentCandleIndex === totalCandles
4510
+ });
4511
+ const candle = testData[candleIndex];
4512
+ const btcCandle = btcTestData[candleIndex];
4513
+ const detectorNoSignalByKey = /* @__PURE__ */ new Map();
4514
+ for (const runner of runners) {
4515
+ const { test, testConnector, strategy } = runner;
4516
+ const delayedSignal = await runStrategyCandleStage(
4517
+ "delayed entry",
4518
+ async () => strategy.__tradejsFlushBacktestDelayedEntry?.(candle, btcCandle)
4519
+ );
4520
+ if (delayedSignal && typeof delayedSignal !== "string") {
4521
+ await processRunnerSignal(runner, delayedSignal, candle);
4522
+ }
4523
+ await runStage("exit checks", () => testConnector.checkExits(candle));
4524
+ const detectorFanoutKey = strategy.detectorFanoutKey;
4525
+ const detectorSkipCode = detectorFanoutKey ? detectorNoSignalByKey.get(detectorFanoutKey) : void 0;
4526
+ const signal = await runStrategyCandleStage(
4527
+ detectorSkipCode ? "strategy detector skip" : "strategy signal",
4528
+ () => detectorSkipCode && strategy.canFastAdvanceDetectorNoSignal && strategy.advanceDetectorNoSignal ? strategy.advanceDetectorNoSignal(
4529
+ candle,
4530
+ btcCandle,
4531
+ detectorSkipCode
4532
+ ) : detectorSkipCode && strategy.skipDetectorNoSignal ? strategy.skipDetectorNoSignal(
4533
+ candle,
4534
+ btcCandle,
4535
+ detectorSkipCode
4536
+ ) : strategy(candle, btcCandle)
4537
+ );
4538
+ if (detectorFanoutKey && strategy.detectorNoSignalSkipReason && typeof signal === "string" && signal === strategy.detectorNoSignalSkipReason) {
4539
+ detectorNoSignalByKey.set(detectorFanoutKey, signal);
4540
+ }
4541
+ await processRunnerSignal(runner, signal, candle);
4542
+ }
4543
+ if ((candleIndex + 1) % CLOSED_RESULT_FLUSH_INTERVAL === 0) {
4544
+ await withTimeout(
4545
+ "flush closed results",
4546
+ Promise.all(runners.map((runner) => flushClosedResultsBatch(runner)))
4547
+ );
4548
+ }
4549
+ }
4550
+ const results = [];
4551
+ for (const runner of runners) {
4552
+ await withTimeout(
4553
+ "flush closed results",
4554
+ flushClosedResultsBatch(runner)
4555
+ );
4556
+ const result = await withTimeout(
4557
+ "collect result",
4558
+ runner.testConnector.getResult()
4559
+ );
4560
+ results.push({
4561
+ test: runner.test,
4562
+ result: runner.replaySignalEvaluations ? {
4563
+ ...result,
4564
+ inlineReplaySignalEvaluations: runner.replaySignalEvaluations
4565
+ } : result
4566
+ });
4567
+ }
4568
+ return results;
4569
+ } finally {
4570
+ (0, import_strategies4.releaseStrategyIndicatorsReplayCache)(sharedIndicatorsReplayKey);
4571
+ (0, import_strategies4.releaseStrategyReplayCache)(sharedIndicatorsReplayKey);
4572
+ }
1694
4573
  };
1695
4574
  // Annotate the CommonJS export names for ESM import in node:
1696
4575
  0 && (module.exports = {
4576
+ canRunTestsInSharedCandleLoop,
1697
4577
  createTestConnector,
4578
+ releaseTestingSymbolCache,
1698
4579
  resetTestingKlineCache,
4580
+ resolveExecutionCosts,
1699
4581
  testing,
4582
+ testingGroupInSharedCandleLoop,
1700
4583
  ...require("@tradejs/core/backtest")
1701
4584
  });