@tradejs/node 1.0.9 → 1.0.11

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,56 @@ 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
+ if (!import_constants4.BACKTEST_LOWER_TIMEFRAME_EXECUTION_ENABLED) {
3356
+ return false;
3357
+ }
3358
+ const executionInterval = resolveBacktestExecutionInterval(interval);
3359
+ if (!executionInterval) {
3360
+ return false;
3361
+ }
3362
+ const primaryMs = resolveIntervalMs(interval);
3363
+ const executionMs = resolveIntervalMs(executionInterval);
3364
+ return executionMs > 0 && executionMs < primaryMs;
3365
+ };
1279
3366
  var getCachedConnector = async (params) => {
1280
- const { state, projectRoot, userName, connectorName } = params;
1281
- const cacheKey = getConnectorCacheKey({ userName, connectorName });
3367
+ const {
3368
+ state,
3369
+ projectRoot,
3370
+ userName,
3371
+ connectorName,
3372
+ universe,
3373
+ accountId,
3374
+ deploymentId
3375
+ } = params;
3376
+ const cacheKey = getConnectorCacheKey({
3377
+ userName,
3378
+ connectorName,
3379
+ universe,
3380
+ accountId
3381
+ });
1282
3382
  const cachedConnector = state.connectorCache.get(cacheKey);
1283
3383
  if (cachedConnector) {
1284
3384
  return cachedConnector;
@@ -1291,7 +3391,10 @@ var getCachedConnector = async (params) => {
1291
3391
  return void 0;
1292
3392
  }
1293
3393
  const connector = await connectorCreator({
1294
- userName
3394
+ userName,
3395
+ universe,
3396
+ accountId,
3397
+ deploymentId
1295
3398
  });
1296
3399
  state.connectorCache.set(cacheKey, connector);
1297
3400
  return connector;
@@ -1306,107 +3409,64 @@ var resetTestingKlineCache = (cwd) => {
1306
3409
  getTradejsProjectCwd(normalizedCwd)
1307
3410
  );
1308
3411
  };
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;
3412
+ var releaseTestingSymbolCache = (params) => {
3413
+ const { cwd, userName, connectorName, symbol } = params;
3414
+ const { state } = getTestingKlineCacheState(cwd);
3415
+ const connectorPrefix = [userName, connectorName].join(":") + ":";
3416
+ for (const cache of [state.coinKlineCache, state.preparedDataCache]) {
3417
+ for (const key of cache.keys()) {
3418
+ if (key.startsWith(connectorPrefix) && key.split(":").includes(symbol)) {
3419
+ cache.delete(key);
3420
+ }
1347
3421
  }
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
- );
3422
+ }
3423
+ };
3424
+ var prepareTestingData = async (params) => {
3425
+ const {
3426
+ state,
3427
+ projectRoot,
3428
+ userName,
3429
+ connectorName,
3430
+ symbol,
3431
+ preloadStart,
3432
+ start,
3433
+ end,
3434
+ interval,
3435
+ universe = "crypto",
3436
+ accountId,
3437
+ deploymentId
3438
+ } = params;
3439
+ const binanceConnector = universe === "crypto" ? await getCachedConnector({
3440
+ state,
3441
+ projectRoot,
3442
+ userName,
3443
+ connectorName: BUILTIN_CONNECTOR_NAMES.Binance
3444
+ }) : void 0;
3445
+ const coinbaseConnector = universe === "crypto" ? await getCachedConnector({
3446
+ state,
3447
+ projectRoot,
3448
+ userName,
3449
+ connectorName: BUILTIN_CONNECTOR_NAMES.Coinbase
3450
+ }) : void 0;
3451
+ const cacheOnly = true;
3452
+ const connector = await getCachedConnector({
3453
+ state,
3454
+ projectRoot,
3455
+ userName,
3456
+ connectorName,
3457
+ universe,
3458
+ accountId,
3459
+ deploymentId
3460
+ });
1374
3461
  if (!connector) {
1375
3462
  throw new Error(`Unknown connector: ${connectorName}`);
1376
3463
  }
1377
- const strategyCreator = await withTimeout(
1378
- "strategy lookup",
1379
- getStrategyCreator(strategyName, projectRoot)
1380
- );
1381
- if (!strategyCreator) {
1382
- throw new Error(`Unknown strategy: ${strategyName}`);
3464
+ if (!binanceConnector || !coinbaseConnector) {
3465
+ import_logger7.logger.warn(
3466
+ "Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
3467
+ connectorName
3468
+ );
1383
3469
  }
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
- }
1408
- const interval = "15";
1409
- const cacheOnly = true;
1410
3470
  const coinCacheKey = getKlineCacheKey({
1411
3471
  userName,
1412
3472
  connectorName,
@@ -1414,7 +3474,9 @@ var testing = async ({
1414
3474
  preloadStart,
1415
3475
  end,
1416
3476
  interval,
1417
- cacheOnly
3477
+ cacheOnly,
3478
+ universe,
3479
+ accountId
1418
3480
  });
1419
3481
  const btcCacheKey = getKlineCacheKey({
1420
3482
  userName,
@@ -1423,12 +3485,23 @@ var testing = async ({
1423
3485
  preloadStart,
1424
3486
  end,
1425
3487
  interval,
1426
- cacheOnly
3488
+ cacheOnly,
3489
+ universe,
3490
+ accountId
3491
+ });
3492
+ const ethCacheKey = getKlineCacheKey({
3493
+ userName,
3494
+ connectorName,
3495
+ symbol: "ETHUSDT",
3496
+ preloadStart,
3497
+ end,
3498
+ interval,
3499
+ cacheOnly,
3500
+ universe,
3501
+ accountId
1427
3502
  });
1428
3503
  const btcBinanceConnectorName = binanceConnector ? BUILTIN_CONNECTOR_NAMES.Binance : connectorName;
1429
3504
  const btcCoinbaseConnectorName = coinbaseConnector ? BUILTIN_CONNECTOR_NAMES.Coinbase : connectorName;
1430
- const cachedCoinData = state.coinKlineCache.get(coinCacheKey);
1431
- const cachedBtcData = state.btcKlineCache.get(btcCacheKey);
1432
3505
  const btcBinanceCacheKey = getKlineCacheKey({
1433
3506
  userName,
1434
3507
  connectorName: btcBinanceConnectorName,
@@ -1436,7 +3509,8 @@ var testing = async ({
1436
3509
  preloadStart,
1437
3510
  end,
1438
3511
  interval,
1439
- cacheOnly
3512
+ cacheOnly,
3513
+ universe: "crypto"
1440
3514
  });
1441
3515
  const btcCoinbaseCacheKey = getKlineCacheKey({
1442
3516
  userName,
@@ -1445,10 +3519,41 @@ var testing = async ({
1445
3519
  preloadStart,
1446
3520
  end,
1447
3521
  interval,
1448
- cacheOnly
3522
+ cacheOnly,
3523
+ universe: "crypto"
1449
3524
  });
3525
+ const cachedCoinData = state.coinKlineCache.get(coinCacheKey);
3526
+ const cachedBtcData = state.btcKlineCache.get(btcCacheKey);
3527
+ const cachedEthData = state.ethKlineCache.get(ethCacheKey);
1450
3528
  const cachedBtcBinanceData = state.btcBinanceKlineCache.get(btcBinanceCacheKey);
1451
3529
  const cachedBtcCoinbaseData = state.btcCoinbaseKlineCache.get(btcCoinbaseCacheKey);
3530
+ const backtestExecutionInterval = resolveBacktestExecutionInterval(interval);
3531
+ const backtestExecutionCacheInterval = backtestExecutionInterval ?? interval;
3532
+ const shouldLoadExecutionCandles = backtestExecutionInterval != null && shouldLoadBacktestExecutionCandles(interval);
3533
+ const executionCoinCacheKey = getKlineCacheKey({
3534
+ userName,
3535
+ connectorName,
3536
+ symbol,
3537
+ preloadStart,
3538
+ end,
3539
+ interval: backtestExecutionCacheInterval,
3540
+ cacheOnly,
3541
+ universe,
3542
+ accountId
3543
+ });
3544
+ const executionBtcCacheKey = getKlineCacheKey({
3545
+ userName,
3546
+ connectorName,
3547
+ symbol: "BTCUSDT",
3548
+ preloadStart,
3549
+ end,
3550
+ interval: backtestExecutionCacheInterval,
3551
+ cacheOnly,
3552
+ universe,
3553
+ accountId
3554
+ });
3555
+ const cachedExecutionCoinData = shouldLoadExecutionCandles ? state.coinKlineCache.get(executionCoinCacheKey) : void 0;
3556
+ const cachedExecutionBtcData = shouldLoadExecutionCandles ? state.btcKlineCache.get(executionBtcCacheKey) : void 0;
1452
3557
  const preparedDataCacheKey = getPreparedDataCacheKey({
1453
3558
  userName,
1454
3559
  connectorName,
@@ -1457,118 +3562,407 @@ var testing = async ({
1457
3562
  start,
1458
3563
  end,
1459
3564
  interval,
3565
+ backtestExecutionInterval: backtestExecutionCacheInterval,
1460
3566
  btcBinanceConnectorName,
1461
- btcCoinbaseConnectorName
3567
+ btcCoinbaseConnectorName,
3568
+ universe,
3569
+ accountId
1462
3570
  });
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);
3571
+ const cachedPreparedData = state.preparedDataCache.get(preparedDataCacheKey);
3572
+ if (cachedPreparedData) {
3573
+ return cachedPreparedData;
3574
+ }
3575
+ const btcDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedBtcData ? Promise.resolve(cachedBtcData) : connector.kline({
3576
+ symbol: "BTCUSDT",
3577
+ start: preloadStart,
3578
+ end,
3579
+ interval,
3580
+ silent: true,
3581
+ cacheOnly
3582
+ });
3583
+ const ethDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedEthData ? Promise.resolve(cachedEthData) : connector.kline({
3584
+ symbol: "ETHUSDT",
3585
+ start: preloadStart,
3586
+ end,
3587
+ interval,
3588
+ silent: true,
3589
+ cacheOnly
3590
+ });
3591
+ const btcBinanceDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedBtcBinanceData ? Promise.resolve(cachedBtcBinanceData) : btcBinanceCacheKey === btcCacheKey ? btcDataPromise : (binanceConnector ?? connector).kline({
3592
+ symbol: "BTCUSDT",
3593
+ start: preloadStart,
3594
+ end,
3595
+ interval,
3596
+ silent: true,
3597
+ cacheOnly
3598
+ });
3599
+ const btcCoinbaseDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedBtcCoinbaseData ? Promise.resolve(cachedBtcCoinbaseData) : btcCoinbaseCacheKey === btcCacheKey ? btcDataPromise : (coinbaseConnector ?? connector).kline({
3600
+ symbol: "BTCUSDT",
3601
+ start: preloadStart,
3602
+ end,
3603
+ interval,
3604
+ silent: true,
3605
+ cacheOnly
3606
+ });
3607
+ const executionDataPromise = !shouldLoadExecutionCandles ? Promise.resolve([]) : cachedExecutionCoinData ? Promise.resolve(cachedExecutionCoinData) : connector.kline({
3608
+ symbol,
3609
+ start: preloadStart,
3610
+ end,
3611
+ interval: backtestExecutionInterval,
3612
+ silent: true,
3613
+ cacheOnly
3614
+ });
3615
+ const executionBtcDataPromise = universe === "tradfi" || !shouldLoadExecutionCandles ? Promise.resolve([]) : cachedExecutionBtcData ? Promise.resolve(cachedExecutionBtcData) : connector.kline({
3616
+ symbol: "BTCUSDT",
3617
+ start: preloadStart,
3618
+ end,
3619
+ interval: backtestExecutionInterval,
3620
+ silent: true,
3621
+ cacheOnly
3622
+ });
3623
+ const [
3624
+ dataRaw,
3625
+ btcDataRaw,
3626
+ ethDataRaw,
3627
+ btcBinanceDataRaw,
3628
+ btcCoinbaseDataRaw,
3629
+ executionDataRaw,
3630
+ executionBtcDataRaw
3631
+ ] = await Promise.all([
3632
+ cachedCoinData ? Promise.resolve(cachedCoinData) : connector.kline({
3633
+ symbol,
3634
+ start: preloadStart,
3635
+ end,
3636
+ interval,
3637
+ silent: true,
3638
+ cacheOnly
3639
+ }),
3640
+ btcDataPromise,
3641
+ ethDataPromise,
3642
+ btcBinanceDataPromise,
3643
+ btcCoinbaseDataPromise,
3644
+ executionDataPromise,
3645
+ executionBtcDataPromise
3646
+ ]);
3647
+ if (!cachedCoinData) {
3648
+ state.coinKlineCache.set(coinCacheKey, dataRaw);
3649
+ }
3650
+ if (!cachedBtcData) {
3651
+ state.btcKlineCache.set(btcCacheKey, btcDataRaw);
3652
+ }
3653
+ if (!cachedEthData) {
3654
+ state.ethKlineCache.set(ethCacheKey, ethDataRaw);
3655
+ }
3656
+ if (!cachedBtcBinanceData) {
3657
+ state.btcBinanceKlineCache.set(btcBinanceCacheKey, btcBinanceDataRaw);
3658
+ }
3659
+ if (!cachedBtcCoinbaseData) {
3660
+ state.btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseDataRaw);
3661
+ }
3662
+ if (shouldLoadExecutionCandles && !cachedExecutionCoinData) {
3663
+ state.coinKlineCache.set(executionCoinCacheKey, executionDataRaw);
3664
+ }
3665
+ if (shouldLoadExecutionCandles && !cachedExecutionBtcData) {
3666
+ state.btcKlineCache.set(executionBtcCacheKey, executionBtcDataRaw);
3667
+ }
3668
+ const aligned = universe === "tradfi" ? { alignedCoinCandles: dataRaw, alignedBtcCandles: dataRaw } : (0, import_indicators3.alignSortedCandlesByTimestamp)(dataRaw, btcDataRaw);
3669
+ const { data, btcData } = filterClosedAlignedCandles(
3670
+ aligned.alignedCoinCandles,
3671
+ aligned.alignedBtcCandles,
3672
+ interval
3673
+ );
3674
+ const btcBinanceData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, btcBinanceDataRaw).alignedBtcCandles;
3675
+ const btcCoinbaseData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, btcCoinbaseDataRaw).alignedBtcCandles;
3676
+ const ethData = universe === "tradfi" ? [] : (0, import_indicators3.alignSortedCandlesByTimestamp)(data, ethDataRaw).alignedBtcCandles;
3677
+ const alignedExecution = shouldLoadExecutionCandles ? universe === "tradfi" ? {
3678
+ alignedCoinCandles: executionDataRaw,
3679
+ alignedBtcCandles: executionDataRaw
3680
+ } : (0, import_indicators3.alignSortedCandlesByTimestamp)(executionDataRaw, executionBtcDataRaw) : { alignedCoinCandles: [], alignedBtcCandles: [] };
3681
+ const { data: backtestExecutionData, btcData: backtestExecutionBtcData } = filterClosedAlignedCandles(
3682
+ alignedExecution.alignedCoinCandles,
3683
+ alignedExecution.alignedBtcCandles,
3684
+ backtestExecutionCacheInterval
3685
+ );
3686
+ const backtestExecutionDataByTimestamp = buildCandleByTimestamp(
3687
+ backtestExecutionData
3688
+ );
3689
+ const backtestExecutionBtcDataByTimestamp = buildCandleByTimestamp(
3690
+ backtestExecutionBtcData
3691
+ );
3692
+ const { prevData, testData } = splitCandlesForTesting(
3693
+ data,
3694
+ start,
3695
+ preloadStart
3696
+ );
3697
+ const { prevData: btcPrevData, testData: btcTestData } = splitCandlesForTesting(btcData, start, preloadStart);
3698
+ const { prevData: ethPrevData, testData: ethTestData } = splitCandlesForTesting(ethData, start, preloadStart);
3699
+ const preparedData = {
3700
+ data,
3701
+ btcData,
3702
+ ethData,
3703
+ prevData,
3704
+ btcPrevData,
3705
+ ethPrevData,
3706
+ testData,
3707
+ btcTestData,
3708
+ ethTestData,
3709
+ btcBinanceData,
3710
+ btcCoinbaseData,
3711
+ backtestExecutionInterval: backtestExecutionCacheInterval,
3712
+ backtestExecutionData,
3713
+ backtestExecutionBtcData,
3714
+ backtestExecutionDataByTimestamp,
3715
+ backtestExecutionBtcDataByTimestamp
3716
+ };
3717
+ state.preparedDataCache.set(preparedDataCacheKey, preparedData);
3718
+ return preparedData;
3719
+ };
3720
+ var canRunTestsInSharedCandleLoop = (tests) => {
3721
+ if (tests.length <= 1) {
3722
+ return false;
3723
+ }
3724
+ const first = tests[0];
3725
+ const firstStart = first.options?.start;
3726
+ const firstEnd = first.options?.end;
3727
+ return tests.every(
3728
+ (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)
3729
+ );
3730
+ };
3731
+ var testing = async ({
3732
+ userName,
3733
+ symbol,
3734
+ options: { start, end },
3735
+ name,
3736
+ testId,
3737
+ testSuiteId,
3738
+ configId,
3739
+ strategyName,
3740
+ strategyConfig,
3741
+ connectorName,
3742
+ universe = "crypto",
3743
+ assetClass,
3744
+ instrument: requestedInstrument,
3745
+ accountId,
3746
+ deploymentId,
3747
+ policyProfileId,
3748
+ interval = BACKTEST_INTERVAL,
3749
+ ml = false,
3750
+ ai = false,
3751
+ fast = false,
3752
+ collectReplaySignalEvaluations = false,
3753
+ chunkId = "single",
3754
+ backtestRunId,
3755
+ backtestTestKey,
3756
+ timeoutMs
3757
+ }) => {
3758
+ if (!start) {
3759
+ throw new Error("no start");
3760
+ }
3761
+ const preloadStart = (0, import_time.getBacktestPreloadStart)(start);
3762
+ const startedAt = Date.now();
3763
+ let activeStageStartedAt = startedAt;
3764
+ let lastProgressSentAt = 0;
3765
+ let lastProgressSignature = "";
3766
+ let currentCandleIndex = 0;
3767
+ let totalCandles = 0;
3768
+ const strategyCandleTimeoutMs = getStrategyCandleTimeoutMs();
3769
+ const formatTimeoutMessage = (stage, stageTimeoutMs) => `Test ${name} (${symbol}) timed out after ${stageTimeoutMs}ms during ${stage}`;
3770
+ const emitProgress = (stage, options = {}) => {
3771
+ const now = Date.now();
3772
+ const candleIndex = typeof options.candleIndex === "number" ? options.candleIndex : currentCandleIndex;
3773
+ const candleTotal = typeof options.candleTotal === "number" ? options.candleTotal : totalCandles;
3774
+ const signature = [
3775
+ stage,
3776
+ candleIndex,
3777
+ candleTotal,
3778
+ Math.floor((now - activeStageStartedAt) / 5e3)
3779
+ ].join(":");
3780
+ if (!options.force) {
3781
+ if (signature === lastProgressSignature) {
3782
+ return;
3783
+ }
3784
+ if (now - lastProgressSentAt < 4e3) {
3785
+ return;
3786
+ }
1507
3787
  }
1508
- if (!cachedBtcBinanceData) {
1509
- state.btcBinanceKlineCache.set(btcBinanceCacheKey, btcBinanceData);
3788
+ lastProgressSentAt = now;
3789
+ lastProgressSignature = signature;
3790
+ process.send?.({
3791
+ progress: true,
3792
+ testName: name,
3793
+ symbol,
3794
+ strategyName,
3795
+ stage,
3796
+ candleIndex,
3797
+ candleTotal,
3798
+ elapsedMs: now - startedAt,
3799
+ stageElapsedMs: now - activeStageStartedAt
3800
+ });
3801
+ };
3802
+ const getStageTimeoutMs = () => {
3803
+ if (!timeoutMs || timeoutMs <= 0) {
3804
+ return null;
1510
3805
  }
1511
- if (!cachedBtcCoinbaseData) {
1512
- state.btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseData);
3806
+ return timeoutMs;
3807
+ };
3808
+ const throwIfTimedOut = (stage) => {
3809
+ if (getStageTimeoutMs() == null) {
3810
+ return;
1513
3811
  }
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
3812
+ emitProgress(stage);
3813
+ };
3814
+ const withTimeout = async (stage, promise, stageTimeoutOverrideMs = null) => {
3815
+ const stageTimeoutMs = getEffectiveTimeoutMs(
3816
+ getStageTimeoutMs() ?? void 0,
3817
+ stageTimeoutOverrideMs
1525
3818
  );
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);
3819
+ if (stageTimeoutMs == null) {
3820
+ return promise;
3821
+ }
3822
+ activeStageStartedAt = Date.now();
3823
+ emitProgress(stage, { force: true });
3824
+ return await new Promise((resolve, reject) => {
3825
+ const heartbeat = setInterval(() => {
3826
+ emitProgress(stage);
3827
+ }, 5e3);
3828
+ const timer = setTimeout(() => {
3829
+ clearInterval(heartbeat);
3830
+ reject(new Error(formatTimeoutMessage(stage, stageTimeoutMs)));
3831
+ }, stageTimeoutMs);
3832
+ promise.then(
3833
+ (value) => {
3834
+ clearInterval(heartbeat);
3835
+ clearTimeout(timer);
3836
+ resolve(value);
3837
+ },
3838
+ (error) => {
3839
+ clearInterval(heartbeat);
3840
+ clearTimeout(timer);
3841
+ reject(error);
3842
+ }
3843
+ );
3844
+ });
3845
+ };
3846
+ const runStage = (stage, fn) => {
3847
+ if (getStageTimeoutMs() == null) {
3848
+ return fn();
3849
+ }
3850
+ return withTimeout(stage, fn());
3851
+ };
3852
+ const runStrategyCandleStage = (stage, fn) => withTimeout(stage, fn(), strategyCandleTimeoutMs);
3853
+ const { projectRoot, state } = getTestingKlineCacheState();
3854
+ const connector = await withTimeout(
3855
+ "connector init",
3856
+ getCachedConnector({
3857
+ state,
3858
+ projectRoot,
3859
+ userName,
3860
+ connectorName,
3861
+ universe,
3862
+ accountId,
3863
+ deploymentId
3864
+ })
3865
+ );
3866
+ if (!connector) {
3867
+ throw new Error(`Unknown connector: ${connectorName}`);
3868
+ }
3869
+ const strategyCreator = await withTimeout(
3870
+ "strategy lookup",
3871
+ getStrategyCreator(strategyName, projectRoot)
3872
+ );
3873
+ if (!strategyCreator) {
3874
+ throw new Error(`Unknown strategy: ${strategyName}`);
1539
3875
  }
3876
+ const preparedData = await withTimeout(
3877
+ "kline preload",
3878
+ prepareTestingData({
3879
+ state,
3880
+ projectRoot,
3881
+ userName,
3882
+ connectorName,
3883
+ symbol,
3884
+ preloadStart,
3885
+ start,
3886
+ end,
3887
+ interval,
3888
+ universe,
3889
+ accountId,
3890
+ deploymentId
3891
+ })
3892
+ );
1540
3893
  if (!preparedData) {
1541
3894
  throw new Error("Prepared backtest data not available");
1542
3895
  }
1543
3896
  const {
1544
3897
  prevData,
1545
3898
  btcPrevData,
3899
+ ethPrevData,
3900
+ ethTestData,
1546
3901
  testData,
1547
3902
  btcTestData,
1548
- btcBinancePrevData,
1549
- btcCoinbasePrevData
3903
+ btcBinanceData,
3904
+ btcCoinbaseData,
3905
+ backtestExecutionInterval,
3906
+ backtestExecutionData,
3907
+ backtestExecutionBtcData,
3908
+ backtestExecutionDataByTimestamp,
3909
+ backtestExecutionBtcDataByTimestamp
1550
3910
  } = preparedData;
3911
+ const runtimePrevData = prevData.slice();
3912
+ const runtimeBtcPrevData = btcPrevData.slice();
3913
+ const runtimeEthData = [...ethPrevData, ...ethTestData];
3914
+ totalCandles = testData.length;
3915
+ const instrument = requestedInstrument;
3916
+ const { model: executionCostModel, fundingRates } = await resolveExecutionCosts({
3917
+ connector,
3918
+ symbol,
3919
+ config: strategyConfig,
3920
+ startTime: start,
3921
+ endTime: end,
3922
+ instrument
3923
+ });
1551
3924
  const testConnector = createTestConnector(connector, {
1552
3925
  userName,
1553
3926
  mlEnabled: ml,
1554
- aiEnabled: ai
3927
+ aiEnabled: ai,
3928
+ fastMode: fast,
3929
+ executionCostModel,
3930
+ fundingRates
1555
3931
  });
1556
3932
  const strategy = await withTimeout(
1557
3933
  "strategy init",
1558
3934
  strategyCreator({
1559
3935
  userName,
1560
- config: strategyConfig,
3936
+ connectorName,
3937
+ universe,
3938
+ assetClass: assetClass ?? instrument?.assetClass,
3939
+ instrument,
3940
+ accountId,
3941
+ deploymentId,
3942
+ policyProfileId,
3943
+ config: {
3944
+ ...strategyConfig,
3945
+ INTERVAL: interval
3946
+ },
1561
3947
  symbol,
1562
- data: prevData,
1563
- btcData: btcPrevData,
1564
- btcBinanceData: btcBinancePrevData,
1565
- btcCoinbaseData: btcCoinbasePrevData,
3948
+ data: runtimePrevData,
3949
+ btcData: runtimeBtcPrevData,
3950
+ ethData: runtimeEthData,
3951
+ btcBinanceData,
3952
+ btcCoinbaseData,
3953
+ backtestExecutionMarketData: {
3954
+ interval: backtestExecutionInterval,
3955
+ data: backtestExecutionData,
3956
+ btcData: backtestExecutionBtcData,
3957
+ dataByTimestamp: backtestExecutionDataByTimestamp,
3958
+ btcDataByTimestamp: backtestExecutionBtcDataByTimestamp
3959
+ },
1566
3960
  connector: testConnector
1567
3961
  })
1568
3962
  );
1569
3963
  const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
1570
3964
  const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
1571
- const replaySignalEvaluations = [];
3965
+ const replaySignalEvaluations = collectReplaySignalEvaluations ? [] : null;
1572
3966
  const flushClosedResultsBatch = async () => {
1573
3967
  if (!ml && !ai) return;
1574
3968
  const batch = await testConnector.drainMlResultsBatch();
@@ -1580,7 +3974,14 @@ var testing = async ({
1580
3974
  const fullRow = (0, import_ml2.buildMlTrainingRow)(payload, {
1581
3975
  profit: resultRecord.profit
1582
3976
  });
1583
- const row = (0, import_ml2.trimMlTrainingRowWindows)(fullRow, 5);
3977
+ const row = {
3978
+ ...(0, import_ml2.trimMlTrainingRowWindows)(fullRow, 5),
3979
+ ...buildBacktestDatasetMetadata({
3980
+ backtestRunId,
3981
+ backtestTestKey,
3982
+ chunkId
3983
+ })
3984
+ };
1584
3985
  await (0, import_ml2.appendMlDatasetRow)({
1585
3986
  strategyName,
1586
3987
  chunkId,
@@ -1590,62 +3991,54 @@ var testing = async ({
1590
3991
  const aiRowBase = pendingAiRowBySignalId.get(resultRecord.signalId);
1591
3992
  if (aiRowBase) {
1592
3993
  pendingAiRowBySignalId.delete(resultRecord.signalId);
3994
+ const { signal: aiSignal, ...rowBase } = aiRowBase;
1593
3995
  await (0, import_ai2.appendAiDatasetRow)({
1594
3996
  strategyName,
1595
3997
  chunkId,
1596
3998
  row: {
1597
- ...aiRowBase,
1598
- profit: resultRecord.profit
3999
+ ...rowBase,
4000
+ payload: buildAiPayload(aiSignal),
4001
+ profit: resultRecord.profit,
4002
+ tradeResult: resultRecord.tradeResult
1599
4003
  }
1600
4004
  });
1601
4005
  }
1602
4006
  }
1603
4007
  };
1604
- for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
1605
- if (candleIndex % 25 === 0) {
1606
- throwIfTimedOut("candle loop");
4008
+ const processSignal = async (signal, candle) => {
4009
+ if (isBacktestEntryDelayControlCode(signal)) {
4010
+ return;
1607
4011
  }
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
- });
4012
+ if (replaySignalEvaluations) {
4013
+ replaySignalEvaluations.push(
4014
+ buildReplaySignalEvaluationRecord({
4015
+ signal,
4016
+ testId,
4017
+ userName,
4018
+ strategyName,
4019
+ symbol,
4020
+ interval,
4021
+ candle
4022
+ })
4023
+ );
1646
4024
  }
1647
4025
  const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (ml || ai);
1648
4026
  if (shouldCapturePayload) {
4027
+ await withTimeout(
4028
+ "binance market context",
4029
+ enrichSignalWithBinanceMarketContext({
4030
+ signal,
4031
+ env: "BACKTEST"
4032
+ })
4033
+ );
4034
+ await withTimeout(
4035
+ "coinmarketcap context",
4036
+ enrichSignalWithCoinMarketCapContext({
4037
+ signal,
4038
+ env: "BACKTEST",
4039
+ enabled: Boolean(ml || ai)
4040
+ })
4041
+ );
1649
4042
  await withTimeout(
1650
4043
  "derivatives context",
1651
4044
  enrichSignalWithDerivativesContext({
@@ -1662,6 +4055,7 @@ var testing = async ({
1662
4055
  testId,
1663
4056
  testSuiteId,
1664
4057
  testName: name,
4058
+ configId,
1665
4059
  symbol,
1666
4060
  strategyName,
1667
4061
  strategyConfig,
@@ -1677,25 +4071,517 @@ var testing = async ({
1677
4071
  symbol: signal.symbol || symbol,
1678
4072
  direction: signal.direction,
1679
4073
  timestamp: signal.timestamp,
1680
- payload: buildAiPayload(signal),
4074
+ signal: cloneAiPayloadSignal(signal),
1681
4075
  testId,
1682
4076
  testSuiteId,
1683
4077
  testName: name,
1684
- connectorName
4078
+ configId,
4079
+ connectorName,
4080
+ ...buildBacktestDatasetMetadata({
4081
+ backtestRunId,
4082
+ backtestTestKey,
4083
+ chunkId
4084
+ })
1685
4085
  });
1686
4086
  }
4087
+ };
4088
+ for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
4089
+ if (candleIndex % 25 === 0) {
4090
+ throwIfTimedOut("candle loop");
4091
+ }
4092
+ currentCandleIndex = candleIndex + 1;
4093
+ emitProgress("candle loop", {
4094
+ force: candleIndex === 0 || currentCandleIndex === totalCandles
4095
+ });
4096
+ const candle = testData[candleIndex];
4097
+ const btcCandle = btcTestData[candleIndex];
4098
+ const delayedSignal = await runStrategyCandleStage(
4099
+ "delayed entry",
4100
+ async () => strategy.__tradejsFlushBacktestDelayedEntry?.(candle, btcCandle)
4101
+ );
4102
+ if (delayedSignal && typeof delayedSignal !== "string") {
4103
+ await processSignal(delayedSignal, candle);
4104
+ }
4105
+ await runStage("exit checks", () => testConnector.checkExits(candle));
4106
+ const signal = await runStrategyCandleStage(
4107
+ "strategy signal",
4108
+ () => strategy(candle, btcCandle)
4109
+ );
4110
+ await processSignal(signal, candle);
4111
+ if ((candleIndex + 1) % CLOSED_RESULT_FLUSH_INTERVAL === 0) {
4112
+ await withTimeout("flush closed results", flushClosedResultsBatch());
4113
+ }
1687
4114
  }
1688
4115
  await withTimeout("flush closed results", flushClosedResultsBatch());
1689
4116
  const result = await withTimeout("collect result", testConnector.getResult());
1690
- return {
4117
+ return replaySignalEvaluations ? {
1691
4118
  ...result,
1692
4119
  inlineReplaySignalEvaluations: replaySignalEvaluations
4120
+ } : result;
4121
+ };
4122
+ var testingGroupInSharedCandleLoop = async (tests) => {
4123
+ if (!canRunTestsInSharedCandleLoop(tests)) {
4124
+ const results = [];
4125
+ for (const test of tests) {
4126
+ const result = await testing(test);
4127
+ if (result) {
4128
+ results.push({ test, result });
4129
+ }
4130
+ }
4131
+ return results;
4132
+ }
4133
+ const first = tests[0];
4134
+ const {
4135
+ userName,
4136
+ symbol,
4137
+ options: { start, end },
4138
+ strategyName,
4139
+ connectorName,
4140
+ universe = "crypto",
4141
+ accountId,
4142
+ deploymentId,
4143
+ interval = BACKTEST_INTERVAL,
4144
+ ml = false,
4145
+ ai = false,
4146
+ fast = false,
4147
+ collectReplaySignalEvaluations = false,
4148
+ chunkId = "single",
4149
+ timeoutMs
4150
+ } = first;
4151
+ if (!start) {
4152
+ throw new Error("no start");
4153
+ }
4154
+ const preloadStart = (0, import_time.getBacktestPreloadStart)(start);
4155
+ const startedAt = Date.now();
4156
+ let activeStageStartedAt = startedAt;
4157
+ let lastProgressSentAt = 0;
4158
+ let lastProgressSignature = "";
4159
+ let currentCandleIndex = 0;
4160
+ let totalCandles = 0;
4161
+ const strategyCandleTimeoutMs = getStrategyCandleTimeoutMs();
4162
+ const formatTimeoutMessage = (stage, stageTimeoutMs) => `Test group ${strategyName}/${symbol} timed out after ${stageTimeoutMs}ms during ${stage}`;
4163
+ const emitProgress = (stage, options = {}) => {
4164
+ const now = Date.now();
4165
+ const candleIndex = typeof options.candleIndex === "number" ? options.candleIndex : currentCandleIndex;
4166
+ const candleTotal = typeof options.candleTotal === "number" ? options.candleTotal : totalCandles;
4167
+ const signature = [
4168
+ stage,
4169
+ candleIndex,
4170
+ candleTotal,
4171
+ Math.floor((now - activeStageStartedAt) / 5e3)
4172
+ ].join(":");
4173
+ if (!options.force) {
4174
+ if (signature === lastProgressSignature) {
4175
+ return;
4176
+ }
4177
+ if (now - lastProgressSentAt < 4e3) {
4178
+ return;
4179
+ }
4180
+ }
4181
+ lastProgressSentAt = now;
4182
+ lastProgressSignature = signature;
4183
+ process.send?.({
4184
+ progress: true,
4185
+ testName: first.name,
4186
+ symbol,
4187
+ strategyName,
4188
+ stage,
4189
+ candleIndex,
4190
+ candleTotal,
4191
+ elapsedMs: now - startedAt,
4192
+ stageElapsedMs: now - activeStageStartedAt
4193
+ });
4194
+ };
4195
+ const getStageTimeoutMs = () => {
4196
+ if (!timeoutMs || timeoutMs <= 0) {
4197
+ return null;
4198
+ }
4199
+ return timeoutMs;
4200
+ };
4201
+ const throwIfTimedOut = (stage) => {
4202
+ if (getStageTimeoutMs() == null) {
4203
+ return;
4204
+ }
4205
+ emitProgress(stage);
4206
+ };
4207
+ const withTimeout = async (stage, promise, stageTimeoutOverrideMs = null) => {
4208
+ const stageTimeoutMs = getEffectiveTimeoutMs(
4209
+ getStageTimeoutMs() ?? void 0,
4210
+ stageTimeoutOverrideMs
4211
+ );
4212
+ if (stageTimeoutMs == null) {
4213
+ return promise;
4214
+ }
4215
+ activeStageStartedAt = Date.now();
4216
+ emitProgress(stage, { force: true });
4217
+ return await new Promise((resolve, reject) => {
4218
+ const heartbeat = setInterval(() => {
4219
+ emitProgress(stage);
4220
+ }, 5e3);
4221
+ const timer = setTimeout(() => {
4222
+ clearInterval(heartbeat);
4223
+ reject(new Error(formatTimeoutMessage(stage, stageTimeoutMs)));
4224
+ }, stageTimeoutMs);
4225
+ promise.then(
4226
+ (value) => {
4227
+ clearInterval(heartbeat);
4228
+ clearTimeout(timer);
4229
+ resolve(value);
4230
+ },
4231
+ (error) => {
4232
+ clearInterval(heartbeat);
4233
+ clearTimeout(timer);
4234
+ reject(error);
4235
+ }
4236
+ );
4237
+ });
4238
+ };
4239
+ const runStage = (stage, fn) => {
4240
+ if (getStageTimeoutMs() == null) {
4241
+ return fn();
4242
+ }
4243
+ return withTimeout(stage, fn());
1693
4244
  };
4245
+ const runStrategyCandleStage = (stage, fn) => withTimeout(stage, fn(), strategyCandleTimeoutMs);
4246
+ const { projectRoot, state } = getTestingKlineCacheState();
4247
+ const connector = await withTimeout(
4248
+ "connector init",
4249
+ getCachedConnector({
4250
+ state,
4251
+ projectRoot,
4252
+ userName,
4253
+ connectorName,
4254
+ universe,
4255
+ accountId,
4256
+ deploymentId
4257
+ })
4258
+ );
4259
+ if (!connector) {
4260
+ throw new Error(`Unknown connector: ${connectorName}`);
4261
+ }
4262
+ const strategyCreator = await withTimeout(
4263
+ "strategy lookup",
4264
+ getStrategyCreator(strategyName, projectRoot)
4265
+ );
4266
+ if (!strategyCreator) {
4267
+ throw new Error(`Unknown strategy: ${strategyName}`);
4268
+ }
4269
+ const preparedData = await withTimeout(
4270
+ "kline preload",
4271
+ prepareTestingData({
4272
+ state,
4273
+ projectRoot,
4274
+ userName,
4275
+ connectorName,
4276
+ symbol,
4277
+ preloadStart,
4278
+ start,
4279
+ end,
4280
+ interval,
4281
+ universe,
4282
+ accountId,
4283
+ deploymentId
4284
+ })
4285
+ );
4286
+ if (!preparedData) {
4287
+ throw new Error("Prepared backtest data not available");
4288
+ }
4289
+ const {
4290
+ prevData,
4291
+ btcPrevData,
4292
+ ethPrevData,
4293
+ ethTestData,
4294
+ testData,
4295
+ btcTestData,
4296
+ btcBinanceData,
4297
+ btcCoinbaseData,
4298
+ backtestExecutionInterval,
4299
+ backtestExecutionData,
4300
+ backtestExecutionBtcData,
4301
+ backtestExecutionDataByTimestamp,
4302
+ backtestExecutionBtcDataByTimestamp
4303
+ } = preparedData;
4304
+ totalCandles = testData.length;
4305
+ const sharedIndicatorsReplayKey = [
4306
+ "shared",
4307
+ userName,
4308
+ connectorName,
4309
+ strategyName,
4310
+ symbol,
4311
+ interval,
4312
+ start,
4313
+ end,
4314
+ chunkId
4315
+ ].join(":");
4316
+ const runners = [];
4317
+ try {
4318
+ for (const test of tests) {
4319
+ const instrument = test.instrument;
4320
+ const { model: executionCostModel, fundingRates } = await resolveExecutionCosts({
4321
+ connector,
4322
+ symbol: test.symbol,
4323
+ config: test.strategyConfig,
4324
+ startTime: start,
4325
+ endTime: end,
4326
+ instrument
4327
+ });
4328
+ const testConnector = createTestConnector(connector, {
4329
+ userName: test.userName,
4330
+ mlEnabled: test.ml,
4331
+ aiEnabled: test.ai,
4332
+ fastMode: test.fast,
4333
+ executionCostModel,
4334
+ fundingRates
4335
+ });
4336
+ const strategy = await withTimeout(
4337
+ "strategy init",
4338
+ strategyCreator({
4339
+ userName: test.userName,
4340
+ connectorName: test.connectorName,
4341
+ universe: test.universe ?? universe,
4342
+ assetClass: test.assetClass ?? instrument?.assetClass,
4343
+ instrument,
4344
+ accountId: test.accountId ?? accountId,
4345
+ deploymentId: test.deploymentId ?? deploymentId,
4346
+ policyProfileId: test.policyProfileId,
4347
+ config: {
4348
+ ...test.strategyConfig,
4349
+ INTERVAL: test.interval ?? interval
4350
+ },
4351
+ symbol: test.symbol,
4352
+ data: prevData.slice(),
4353
+ btcData: btcPrevData.slice(),
4354
+ ethData: [...ethPrevData, ...ethTestData],
4355
+ btcBinanceData,
4356
+ btcCoinbaseData,
4357
+ backtestExecutionMarketData: {
4358
+ interval: backtestExecutionInterval,
4359
+ data: backtestExecutionData,
4360
+ btcData: backtestExecutionBtcData,
4361
+ dataByTimestamp: backtestExecutionDataByTimestamp,
4362
+ btcDataByTimestamp: backtestExecutionBtcDataByTimestamp
4363
+ },
4364
+ connector: testConnector,
4365
+ sharedIndicatorsReplayKey
4366
+ })
4367
+ );
4368
+ runners.push({
4369
+ test,
4370
+ strategy,
4371
+ testConnector,
4372
+ pendingMlPayloadBySignalId: /* @__PURE__ */ new Map(),
4373
+ pendingAiRowBySignalId: /* @__PURE__ */ new Map(),
4374
+ replaySignalEvaluations: collectReplaySignalEvaluations ? [] : null
4375
+ });
4376
+ }
4377
+ const flushClosedResultsBatch = async (runner) => {
4378
+ if (!runner.test.ml && !runner.test.ai) return;
4379
+ const batch = await runner.testConnector.drainMlResultsBatch();
4380
+ if (!batch.length) return;
4381
+ for (const resultRecord of batch) {
4382
+ const payload = runner.pendingMlPayloadBySignalId.get(
4383
+ resultRecord.signalId
4384
+ );
4385
+ if (payload) {
4386
+ runner.pendingMlPayloadBySignalId.delete(resultRecord.signalId);
4387
+ const fullRow = (0, import_ml2.buildMlTrainingRow)(payload, {
4388
+ profit: resultRecord.profit
4389
+ });
4390
+ const resolvedChunkId = runner.test.chunkId ?? "single";
4391
+ const row = {
4392
+ ...(0, import_ml2.trimMlTrainingRowWindows)(fullRow, 5),
4393
+ ...buildBacktestDatasetMetadata({
4394
+ backtestRunId: runner.test.backtestRunId,
4395
+ backtestTestKey: runner.test.backtestTestKey,
4396
+ chunkId: resolvedChunkId
4397
+ })
4398
+ };
4399
+ await (0, import_ml2.appendMlDatasetRow)({
4400
+ strategyName: runner.test.strategyName,
4401
+ chunkId: resolvedChunkId,
4402
+ row
4403
+ });
4404
+ }
4405
+ const aiRowBase = runner.pendingAiRowBySignalId.get(
4406
+ resultRecord.signalId
4407
+ );
4408
+ if (aiRowBase) {
4409
+ runner.pendingAiRowBySignalId.delete(resultRecord.signalId);
4410
+ const { signal: aiSignal, ...rowBase } = aiRowBase;
4411
+ const resolvedChunkId = runner.test.chunkId ?? "single";
4412
+ await (0, import_ai2.appendAiDatasetRow)({
4413
+ strategyName: runner.test.strategyName,
4414
+ chunkId: resolvedChunkId,
4415
+ row: {
4416
+ ...rowBase,
4417
+ payload: buildAiPayload(aiSignal),
4418
+ profit: resultRecord.profit,
4419
+ tradeResult: resultRecord.tradeResult
4420
+ }
4421
+ });
4422
+ }
4423
+ }
4424
+ };
4425
+ const processRunnerSignal = async (runner, signal, candle) => {
4426
+ if (isBacktestEntryDelayControlCode(signal)) {
4427
+ return;
4428
+ }
4429
+ const { test } = runner;
4430
+ if (runner.replaySignalEvaluations) {
4431
+ runner.replaySignalEvaluations.push(
4432
+ buildReplaySignalEvaluationRecord({
4433
+ signal,
4434
+ testId: test.testId,
4435
+ userName: test.userName,
4436
+ strategyName: test.strategyName,
4437
+ symbol: test.symbol,
4438
+ interval: test.interval ?? interval,
4439
+ candle
4440
+ })
4441
+ );
4442
+ }
4443
+ const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (test.ml || test.ai);
4444
+ if (shouldCapturePayload) {
4445
+ await withTimeout(
4446
+ "binance market context",
4447
+ enrichSignalWithBinanceMarketContext({
4448
+ signal,
4449
+ env: "BACKTEST"
4450
+ })
4451
+ );
4452
+ await withTimeout(
4453
+ "coinmarketcap context",
4454
+ enrichSignalWithCoinMarketCapContext({
4455
+ signal,
4456
+ env: "BACKTEST",
4457
+ enabled: Boolean(test.ml || test.ai)
4458
+ })
4459
+ );
4460
+ await withTimeout(
4461
+ "derivatives context",
4462
+ enrichSignalWithDerivativesContext({
4463
+ signal,
4464
+ env: "BACKTEST"
4465
+ })
4466
+ );
4467
+ }
4468
+ if (test.ml && signal && typeof signal !== "string" && signal.signalId) {
4469
+ const payload = buildMlPayload({
4470
+ signal,
4471
+ context: {
4472
+ userName: test.userName,
4473
+ testId: test.testId,
4474
+ testSuiteId: test.testSuiteId,
4475
+ testName: test.name,
4476
+ configId: test.configId,
4477
+ symbol: test.symbol,
4478
+ strategyName: test.strategyName,
4479
+ strategyConfig: test.strategyConfig,
4480
+ connectorName: test.connectorName
4481
+ }
4482
+ });
4483
+ runner.pendingMlPayloadBySignalId.set(signal.signalId, payload);
4484
+ }
4485
+ if (test.ai && signal && typeof signal !== "string" && signal.signalId) {
4486
+ runner.pendingAiRowBySignalId.set(signal.signalId, {
4487
+ signalId: signal.signalId,
4488
+ strategyName: signal.strategy || test.strategyName,
4489
+ symbol: signal.symbol || test.symbol,
4490
+ direction: signal.direction,
4491
+ timestamp: signal.timestamp,
4492
+ signal: cloneAiPayloadSignal(signal),
4493
+ testId: test.testId,
4494
+ testSuiteId: test.testSuiteId,
4495
+ testName: test.name,
4496
+ configId: test.configId,
4497
+ connectorName: test.connectorName,
4498
+ ...buildBacktestDatasetMetadata({
4499
+ backtestRunId: test.backtestRunId,
4500
+ backtestTestKey: test.backtestTestKey,
4501
+ chunkId: test.chunkId ?? "single"
4502
+ })
4503
+ });
4504
+ }
4505
+ };
4506
+ for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
4507
+ if (candleIndex % 25 === 0) {
4508
+ throwIfTimedOut("candle loop");
4509
+ }
4510
+ currentCandleIndex = candleIndex + 1;
4511
+ emitProgress("candle loop", {
4512
+ force: candleIndex === 0 || currentCandleIndex === totalCandles
4513
+ });
4514
+ const candle = testData[candleIndex];
4515
+ const btcCandle = btcTestData[candleIndex];
4516
+ const detectorNoSignalByKey = /* @__PURE__ */ new Map();
4517
+ for (const runner of runners) {
4518
+ const { test, testConnector, strategy } = runner;
4519
+ const delayedSignal = await runStrategyCandleStage(
4520
+ "delayed entry",
4521
+ async () => strategy.__tradejsFlushBacktestDelayedEntry?.(candle, btcCandle)
4522
+ );
4523
+ if (delayedSignal && typeof delayedSignal !== "string") {
4524
+ await processRunnerSignal(runner, delayedSignal, candle);
4525
+ }
4526
+ await runStage("exit checks", () => testConnector.checkExits(candle));
4527
+ const detectorFanoutKey = strategy.detectorFanoutKey;
4528
+ const detectorSkipCode = detectorFanoutKey ? detectorNoSignalByKey.get(detectorFanoutKey) : void 0;
4529
+ const signal = await runStrategyCandleStage(
4530
+ detectorSkipCode ? "strategy detector skip" : "strategy signal",
4531
+ () => detectorSkipCode && strategy.canFastAdvanceDetectorNoSignal && strategy.advanceDetectorNoSignal ? strategy.advanceDetectorNoSignal(
4532
+ candle,
4533
+ btcCandle,
4534
+ detectorSkipCode
4535
+ ) : detectorSkipCode && strategy.skipDetectorNoSignal ? strategy.skipDetectorNoSignal(
4536
+ candle,
4537
+ btcCandle,
4538
+ detectorSkipCode
4539
+ ) : strategy(candle, btcCandle)
4540
+ );
4541
+ if (detectorFanoutKey && strategy.detectorNoSignalSkipReason && typeof signal === "string" && signal === strategy.detectorNoSignalSkipReason) {
4542
+ detectorNoSignalByKey.set(detectorFanoutKey, signal);
4543
+ }
4544
+ await processRunnerSignal(runner, signal, candle);
4545
+ }
4546
+ if ((candleIndex + 1) % CLOSED_RESULT_FLUSH_INTERVAL === 0) {
4547
+ await withTimeout(
4548
+ "flush closed results",
4549
+ Promise.all(runners.map((runner) => flushClosedResultsBatch(runner)))
4550
+ );
4551
+ }
4552
+ }
4553
+ const results = [];
4554
+ for (const runner of runners) {
4555
+ await withTimeout(
4556
+ "flush closed results",
4557
+ flushClosedResultsBatch(runner)
4558
+ );
4559
+ const result = await withTimeout(
4560
+ "collect result",
4561
+ runner.testConnector.getResult()
4562
+ );
4563
+ results.push({
4564
+ test: runner.test,
4565
+ result: runner.replaySignalEvaluations ? {
4566
+ ...result,
4567
+ inlineReplaySignalEvaluations: runner.replaySignalEvaluations
4568
+ } : result
4569
+ });
4570
+ }
4571
+ return results;
4572
+ } finally {
4573
+ (0, import_strategies4.releaseStrategyIndicatorsReplayCache)(sharedIndicatorsReplayKey);
4574
+ (0, import_strategies4.releaseStrategyReplayCache)(sharedIndicatorsReplayKey);
4575
+ }
1694
4576
  };
1695
4577
  // Annotate the CommonJS export names for ESM import in node:
1696
4578
  0 && (module.exports = {
4579
+ canRunTestsInSharedCandleLoop,
1697
4580
  createTestConnector,
4581
+ releaseTestingSymbolCache,
1698
4582
  resetTestingKlineCache,
4583
+ resolveExecutionCosts,
1699
4584
  testing,
4585
+ testingGroupInSharedCandleLoop,
1700
4586
  ...require("@tradejs/core/backtest")
1701
4587
  });