@tradejs/node 1.0.5 → 1.0.8

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.mjs CHANGED
@@ -1,30 +1,29 @@
1
1
  import {
2
2
  BUILTIN_CONNECTOR_NAMES,
3
3
  getConnectorCreatorByName
4
- } from "./chunk-CIY64D57.mjs";
4
+ } from "./chunk-JMDYEKIO.mjs";
5
5
  import {
6
- buildMlPayload
7
- } from "./chunk-RBE4PZER.mjs";
6
+ buildMlPayload,
7
+ enrichSignalWithDerivativesContext
8
+ } from "./chunk-JRRG3YQG.mjs";
8
9
  import {
9
- require_lodash
10
- } from "./chunk-GKDBAF3A.mjs";
10
+ buildAiPayload
11
+ } from "./chunk-2JKX3DM7.mjs";
11
12
  import {
12
13
  getStrategyCreator
13
- } from "./chunk-ZY6ULOWK.mjs";
14
+ } from "./chunk-WGOYR6AB.mjs";
14
15
  import {
15
16
  getTradejsProjectCwd
16
- } from "./chunk-P2ZUWONT.mjs";
17
- import {
18
- __toESM
19
- } from "./chunk-6DZX6EAA.mjs";
17
+ } from "./chunk-JU77QVJ3.mjs";
18
+ import "./chunk-6DZX6EAA.mjs";
20
19
 
21
20
  // src/backtest.ts
22
21
  export * from "@tradejs/core/backtest";
23
22
 
24
23
  // src/testing.ts
25
24
  import { alignSortedCandlesByTimestamp } from "@tradejs/core/indicators";
26
- import { PRELOAD_DAYS } from "@tradejs/core/constants";
27
- import { getTimestamp } from "@tradejs/core/time";
25
+ import { getBacktestPreloadStart } from "@tradejs/core/time";
26
+ import { appendAiDatasetRow } from "@tradejs/infra/ai";
28
27
  import {
29
28
  appendMlDatasetRow,
30
29
  buildMlTrainingRow,
@@ -33,15 +32,11 @@ import {
33
32
  import { logger } from "@tradejs/infra/logger";
34
33
 
35
34
  // src/testConnector.ts
36
- var import_lodash = __toESM(require_lodash());
37
35
  import { randomUUID } from "crypto";
38
- import { TTL_1D } from "@tradejs/core/constants";
39
36
  import { round } from "@tradejs/core/math";
40
- import { redisKeys, setData } from "@tradejs/infra/redis";
41
37
  var FEE = 5e-3;
42
38
  var INITIAL_AMOUNT = 100;
43
39
  var createTestConnector = (connector, context) => {
44
- const userName = context?.userName;
45
40
  let state = {};
46
41
  const orderLog = [];
47
42
  const positionLog = [];
@@ -51,7 +46,7 @@ var createTestConnector = (connector, context) => {
51
46
  let currentPositionProfit = 0;
52
47
  let takeProfits = [];
53
48
  let stopLossPrice = null;
54
- const closedMlResults = [];
49
+ const closedSignalResults = [];
55
50
  const logOrder = (data) => {
56
51
  const nextEntry = {
57
52
  ...currentPosition || {},
@@ -61,7 +56,8 @@ var createTestConnector = (connector, context) => {
61
56
  index: orderLog.length
62
57
  };
63
58
  if (nextEntry.signal) {
64
- nextEntry.signal = import_lodash.default.omit(nextEntry.signal, "indicators");
59
+ const { indicators: _indicators, ...signalWithoutIndicators } = nextEntry.signal;
60
+ nextEntry.signal = signalWithoutIndicators;
65
61
  }
66
62
  orderLog.push(nextEntry);
67
63
  };
@@ -72,10 +68,10 @@ var createTestConnector = (connector, context) => {
72
68
  if (!currentPosition) {
73
69
  return;
74
70
  }
75
- if (context?.mlEnabled) {
71
+ if (context?.mlEnabled || context?.aiEnabled) {
76
72
  const signalId = currentPosition.signal?.signalId;
77
73
  if (signalId) {
78
- closedMlResults.push({
74
+ closedSignalResults.push({
79
75
  signalId,
80
76
  profit: currentPositionProfit
81
77
  });
@@ -96,6 +92,7 @@ var createTestConnector = (connector, context) => {
96
92
  currentPositionProfit = 0;
97
93
  };
98
94
  return {
95
+ __tradejsTestConnector: true,
99
96
  getState: async () => state,
100
97
  setState: async (newState) => {
101
98
  state = {
@@ -106,33 +103,38 @@ var createTestConnector = (connector, context) => {
106
103
  kline: async (options) => connector.kline(options),
107
104
  getResult: async () => {
108
105
  const orderLogId = randomUUID().slice(-12);
109
- const cacheUserName = userName || "root";
110
- await setData(
111
- redisKeys.cacheOrders(cacheUserName, orderLogId),
112
- orderLog,
113
- {
114
- expire: TTL_1D
115
- }
116
- );
117
- await setData(
118
- redisKeys.cachePositions(cacheUserName, orderLogId),
119
- positionLog,
120
- {
121
- expire: TTL_1D
122
- }
123
- );
124
106
  return {
125
107
  stat: {
126
108
  amount,
127
109
  profit: amount - INITIAL_AMOUNT,
128
110
  orders: positionLog.length
129
111
  },
130
- orderLogId
112
+ orderLogId,
113
+ inlineOrderLog: [...orderLog],
114
+ inlinePositionLog: [...positionLog]
131
115
  };
132
116
  },
133
117
  getPosition: async () => currentPosition || null,
118
+ getOpenPositionPnl: async () => {
119
+ if (typeof connector.getOpenPositionPnl === "function") {
120
+ return connector.getOpenPositionPnl();
121
+ }
122
+ if (!currentPosition) {
123
+ return [];
124
+ }
125
+ return [
126
+ {
127
+ symbol: currentPosition.symbol,
128
+ qty: currentPosition.qty,
129
+ price: currentPosition.price,
130
+ currentPrice: currentPosition.price,
131
+ unrealizedPnl: 0,
132
+ direction: currentPosition.direction
133
+ }
134
+ ];
135
+ },
134
136
  checkTp: async (candle) => {
135
- if (import_lodash.default.isEmpty(candle) || !currentPosition || !currentPosition.qty) {
137
+ if (!candle || !currentPosition || !currentPosition.qty) {
136
138
  return;
137
139
  }
138
140
  const isLong = currentPosition.direction === "LONG";
@@ -167,7 +169,7 @@ var createTestConnector = (connector, context) => {
167
169
  }
168
170
  },
169
171
  checkSl: async (candle) => {
170
- if (!stopLossPrice || !currentPosition || import_lodash.default.isEmpty(candle)) {
172
+ if (!stopLossPrice || !currentPosition || !candle) {
171
173
  return;
172
174
  }
173
175
  const isLong = currentPosition.direction === "LONG";
@@ -187,13 +189,11 @@ var createTestConnector = (connector, context) => {
187
189
  clearPosition(candle.timestamp);
188
190
  }
189
191
  },
190
- placeOrder: async (order, tp = [], slPrice) => {
192
+ placeOrder: async (order) => {
191
193
  if (currentPosition) {
192
194
  return false;
193
195
  }
194
196
  const isLong = order.direction === "LONG";
195
- takeProfits = import_lodash.default.cloneDeep(tp);
196
- stopLossPrice = slPrice || null;
197
197
  currentPosition = { ...order, amount };
198
198
  originalQty = order.qty;
199
199
  const fee = order.price * order.qty * FEE;
@@ -208,6 +208,24 @@ var createTestConnector = (connector, context) => {
208
208
  });
209
209
  return true;
210
210
  },
211
+ setTakeProfits: async ({ takeProfits: nextTakeProfits }) => {
212
+ if (!currentPosition) {
213
+ return false;
214
+ }
215
+ takeProfits = Array.isArray(nextTakeProfits) ? nextTakeProfits.map((tp) => ({ ...tp })) : [];
216
+ return true;
217
+ },
218
+ setStopLoss: async ({ stopLossPrice: nextStopLossPrice }) => {
219
+ if (!currentPosition) {
220
+ return false;
221
+ }
222
+ stopLossPrice = nextStopLossPrice || null;
223
+ currentPosition = {
224
+ ...currentPosition,
225
+ ...stopLossPrice != null ? { slPrice: stopLossPrice } : {}
226
+ };
227
+ return true;
228
+ },
211
229
  closePosition: async (order) => {
212
230
  if (!currentPosition) {
213
231
  return false;
@@ -227,17 +245,18 @@ var createTestConnector = (connector, context) => {
227
245
  },
228
246
  getTickers: connector.getTickers,
229
247
  getPositions: connector.getPositions,
230
- drainMlResultsBatch: async () => closedMlResults.splice(0)
248
+ drainMlResultsBatch: async () => closedSignalResults.splice(0)
231
249
  };
232
250
  };
233
251
 
234
252
  // src/testing.ts
235
- var preloadStart = getTimestamp(PRELOAD_DAYS);
236
253
  var createTestingKlineCacheState = () => ({
237
254
  coinKlineCache: /* @__PURE__ */ new Map(),
238
255
  btcKlineCache: /* @__PURE__ */ new Map(),
239
256
  btcBinanceKlineCache: /* @__PURE__ */ new Map(),
240
- btcCoinbaseKlineCache: /* @__PURE__ */ new Map()
257
+ btcCoinbaseKlineCache: /* @__PURE__ */ new Map(),
258
+ preparedDataCache: /* @__PURE__ */ new Map(),
259
+ connectorCache: /* @__PURE__ */ new Map()
241
260
  });
242
261
  var testingKlineCacheStateByProjectRoot = /* @__PURE__ */ new Map();
243
262
  var getTestingKlineCacheState = (cwd = getTradejsProjectCwd()) => {
@@ -253,7 +272,15 @@ var getTestingKlineCacheState = (cwd = getTradejsProjectCwd()) => {
253
272
  };
254
273
  };
255
274
  var getKlineCacheKey = (params) => {
256
- const { userName, connectorName, symbol, end, interval, cacheOnly } = params;
275
+ const {
276
+ userName,
277
+ connectorName,
278
+ symbol,
279
+ preloadStart,
280
+ end,
281
+ interval,
282
+ cacheOnly
283
+ } = params;
257
284
  return [
258
285
  userName,
259
286
  connectorName,
@@ -264,6 +291,64 @@ var getKlineCacheKey = (params) => {
264
291
  cacheOnly ? 1 : 0
265
292
  ].join(":");
266
293
  };
294
+ var getPreparedDataCacheKey = (params) => {
295
+ const {
296
+ userName,
297
+ connectorName,
298
+ symbol,
299
+ preloadStart,
300
+ start,
301
+ end,
302
+ interval,
303
+ btcBinanceConnectorName,
304
+ btcCoinbaseConnectorName
305
+ } = params;
306
+ return [
307
+ userName,
308
+ connectorName,
309
+ symbol,
310
+ preloadStart,
311
+ start,
312
+ end,
313
+ interval,
314
+ btcBinanceConnectorName,
315
+ btcCoinbaseConnectorName
316
+ ].join(":");
317
+ };
318
+ var getConnectorCacheKey = (params) => [params.userName, params.connectorName].join(":");
319
+ var splitCandlesForTesting = (candles, start, preloadStart) => {
320
+ const prevData = [];
321
+ const testData = [];
322
+ for (const candle of candles) {
323
+ if (candle.timestamp < preloadStart) continue;
324
+ if (candle.timestamp < start) {
325
+ prevData.push(candle);
326
+ } else {
327
+ testData.push(candle);
328
+ }
329
+ }
330
+ return { prevData, testData };
331
+ };
332
+ var getCachedConnector = async (params) => {
333
+ const { state, projectRoot, userName, connectorName } = params;
334
+ const cacheKey = getConnectorCacheKey({ userName, connectorName });
335
+ const cachedConnector = state.connectorCache.get(cacheKey);
336
+ if (cachedConnector) {
337
+ return cachedConnector;
338
+ }
339
+ const connectorCreator = await getConnectorCreatorByName(
340
+ connectorName,
341
+ projectRoot
342
+ );
343
+ if (!connectorCreator) {
344
+ return void 0;
345
+ }
346
+ const connector = await connectorCreator({
347
+ userName
348
+ });
349
+ state.connectorCache.set(cacheKey, connector);
350
+ return connector;
351
+ };
267
352
  var resetTestingKlineCache = (cwd) => {
268
353
  const normalizedCwd = String(cwd ?? "").trim();
269
354
  if (!normalizedCwd) {
@@ -285,35 +370,89 @@ var testing = async ({
285
370
  strategyConfig,
286
371
  connectorName,
287
372
  ml = false,
288
- chunkId = "single"
373
+ ai = false,
374
+ chunkId = "single",
375
+ timeoutMs
289
376
  }) => {
290
377
  if (!start) {
291
378
  throw new Error("no start");
292
379
  }
380
+ const preloadStart = getBacktestPreloadStart(start);
381
+ const startedAt = Date.now();
382
+ const formatTimeoutMessage = (stage) => `Test ${name} (${symbol}) timed out after ${timeoutMs}ms during ${stage}`;
383
+ const getRemainingTimeoutMs = (stage) => {
384
+ if (!timeoutMs || timeoutMs <= 0) {
385
+ return null;
386
+ }
387
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
388
+ if (remainingMs <= 0) {
389
+ throw new Error(formatTimeoutMessage(stage));
390
+ }
391
+ return remainingMs;
392
+ };
393
+ const throwIfTimedOut = (stage) => {
394
+ getRemainingTimeoutMs(stage);
395
+ };
396
+ const withTimeout = async (stage, promise) => {
397
+ const remainingMs = getRemainingTimeoutMs(stage);
398
+ if (remainingMs == null) {
399
+ return promise;
400
+ }
401
+ return await new Promise((resolve, reject) => {
402
+ const timer = setTimeout(() => {
403
+ reject(new Error(formatTimeoutMessage(stage)));
404
+ }, remainingMs);
405
+ promise.then(
406
+ (value) => {
407
+ clearTimeout(timer);
408
+ resolve(value);
409
+ },
410
+ (error) => {
411
+ clearTimeout(timer);
412
+ reject(error);
413
+ }
414
+ );
415
+ });
416
+ };
293
417
  const { projectRoot, state } = getTestingKlineCacheState();
294
- const connectorCreator = await getConnectorCreatorByName(
295
- connectorName,
296
- projectRoot
418
+ const connector = await withTimeout(
419
+ "connector init",
420
+ getCachedConnector({
421
+ state,
422
+ projectRoot,
423
+ userName,
424
+ connectorName
425
+ })
297
426
  );
298
- if (!connectorCreator) {
427
+ if (!connector) {
299
428
  throw new Error(`Unknown connector: ${connectorName}`);
300
429
  }
301
- const connector = await connectorCreator({
302
- userName
303
- });
304
- const strategyCreator = await getStrategyCreator(strategyName, projectRoot);
430
+ const strategyCreator = await withTimeout(
431
+ "strategy lookup",
432
+ getStrategyCreator(strategyName, projectRoot)
433
+ );
305
434
  if (!strategyCreator) {
306
435
  throw new Error(`Unknown strategy: ${strategyName}`);
307
436
  }
308
- const binanceCreator = await getConnectorCreatorByName(
309
- BUILTIN_CONNECTOR_NAMES.Binance,
310
- projectRoot
437
+ const binanceConnector = await withTimeout(
438
+ "binance connector init",
439
+ getCachedConnector({
440
+ state,
441
+ projectRoot,
442
+ userName,
443
+ connectorName: BUILTIN_CONNECTOR_NAMES.Binance
444
+ })
311
445
  );
312
- const coinbaseCreator = await getConnectorCreatorByName(
313
- BUILTIN_CONNECTOR_NAMES.Coinbase,
314
- projectRoot
446
+ const coinbaseConnector = await withTimeout(
447
+ "coinbase connector init",
448
+ getCachedConnector({
449
+ state,
450
+ projectRoot,
451
+ userName,
452
+ connectorName: BUILTIN_CONNECTOR_NAMES.Coinbase
453
+ })
315
454
  );
316
- if (!binanceCreator || !coinbaseCreator) {
455
+ if (!binanceConnector || !coinbaseConnector) {
317
456
  logger.warn(
318
457
  "Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
319
458
  connectorName
@@ -325,6 +464,7 @@ var testing = async ({
325
464
  userName,
326
465
  connectorName,
327
466
  symbol,
467
+ preloadStart,
328
468
  end,
329
469
  interval,
330
470
  cacheOnly
@@ -333,160 +473,240 @@ var testing = async ({
333
473
  userName,
334
474
  connectorName,
335
475
  symbol: "BTCUSDT",
476
+ preloadStart,
336
477
  end,
337
478
  interval,
338
479
  cacheOnly
339
480
  });
481
+ const btcBinanceConnectorName = binanceConnector ? BUILTIN_CONNECTOR_NAMES.Binance : connectorName;
482
+ const btcCoinbaseConnectorName = coinbaseConnector ? BUILTIN_CONNECTOR_NAMES.Coinbase : connectorName;
340
483
  const cachedCoinData = state.coinKlineCache.get(coinCacheKey);
341
484
  const cachedBtcData = state.btcKlineCache.get(btcCacheKey);
342
485
  const btcBinanceCacheKey = getKlineCacheKey({
343
486
  userName,
344
- connectorName: binanceCreator ? BUILTIN_CONNECTOR_NAMES.Binance : connectorName,
487
+ connectorName: btcBinanceConnectorName,
345
488
  symbol: "BTCUSDT",
489
+ preloadStart,
346
490
  end,
347
491
  interval,
348
492
  cacheOnly
349
493
  });
350
494
  const btcCoinbaseCacheKey = getKlineCacheKey({
351
495
  userName,
352
- connectorName: coinbaseCreator ? BUILTIN_CONNECTOR_NAMES.Coinbase : connectorName,
496
+ connectorName: btcCoinbaseConnectorName,
353
497
  symbol: "BTCUSDT",
498
+ preloadStart,
354
499
  end,
355
500
  interval,
356
501
  cacheOnly
357
502
  });
358
503
  const cachedBtcBinanceData = state.btcBinanceKlineCache.get(btcBinanceCacheKey);
359
504
  const cachedBtcCoinbaseData = state.btcCoinbaseKlineCache.get(btcCoinbaseCacheKey);
360
- const [data, btcData, btcBinanceData, btcCoinbaseData] = await Promise.all([
361
- cachedCoinData ? Promise.resolve(cachedCoinData) : connector.kline({
362
- symbol,
363
- start: preloadStart,
364
- end,
365
- interval,
366
- silent: true,
367
- cacheOnly
368
- }),
369
- cachedBtcData ? Promise.resolve(cachedBtcData) : connector.kline({
370
- symbol: "BTCUSDT",
371
- start: preloadStart,
372
- end,
373
- interval,
374
- silent: true,
375
- cacheOnly
376
- }),
377
- cachedBtcBinanceData ? Promise.resolve(cachedBtcBinanceData) : binanceCreator ? binanceCreator({ userName }).then(
378
- (binanceConnector) => binanceConnector.kline({
379
- symbol: "BTCUSDT",
380
- start: preloadStart,
381
- end,
382
- interval,
383
- silent: true,
384
- cacheOnly
385
- })
386
- ) : connector.kline({
387
- symbol: "BTCUSDT",
388
- start: preloadStart,
389
- end,
390
- interval,
391
- silent: true,
392
- cacheOnly
393
- }),
394
- cachedBtcCoinbaseData ? Promise.resolve(cachedBtcCoinbaseData) : coinbaseCreator ? coinbaseCreator({ userName }).then(
395
- (coinbaseConnector) => coinbaseConnector.kline({
396
- symbol: "BTCUSDT",
397
- start: preloadStart,
398
- end,
399
- interval,
400
- silent: true,
401
- cacheOnly
402
- })
403
- ) : connector.kline({
404
- symbol: "BTCUSDT",
405
- start: preloadStart,
406
- end,
407
- interval,
408
- silent: true,
409
- cacheOnly
410
- })
411
- ]);
412
- if (!cachedCoinData) {
413
- state.coinKlineCache.set(coinCacheKey, data);
414
- }
415
- if (!cachedBtcData) {
416
- state.btcKlineCache.set(btcCacheKey, btcData);
417
- }
418
- if (!cachedBtcBinanceData) {
419
- state.btcBinanceKlineCache.set(btcBinanceCacheKey, btcBinanceData);
505
+ const preparedDataCacheKey = getPreparedDataCacheKey({
506
+ userName,
507
+ connectorName,
508
+ symbol,
509
+ preloadStart,
510
+ start,
511
+ end,
512
+ interval,
513
+ btcBinanceConnectorName,
514
+ btcCoinbaseConnectorName
515
+ });
516
+ let preparedData = state.preparedDataCache.get(preparedDataCacheKey);
517
+ if (!preparedData) {
518
+ const [data, btcData, btcBinanceData, btcCoinbaseData] = await withTimeout(
519
+ "kline preload",
520
+ Promise.all([
521
+ cachedCoinData ? Promise.resolve(cachedCoinData) : connector.kline({
522
+ symbol,
523
+ start: preloadStart,
524
+ end,
525
+ interval,
526
+ silent: true,
527
+ cacheOnly
528
+ }),
529
+ cachedBtcData ? Promise.resolve(cachedBtcData) : connector.kline({
530
+ symbol: "BTCUSDT",
531
+ start: preloadStart,
532
+ end,
533
+ interval,
534
+ silent: true,
535
+ cacheOnly
536
+ }),
537
+ cachedBtcBinanceData ? Promise.resolve(cachedBtcBinanceData) : (binanceConnector ?? connector).kline({
538
+ symbol: "BTCUSDT",
539
+ start: preloadStart,
540
+ end,
541
+ interval,
542
+ silent: true,
543
+ cacheOnly
544
+ }),
545
+ cachedBtcCoinbaseData ? Promise.resolve(cachedBtcCoinbaseData) : (coinbaseConnector ?? connector).kline({
546
+ symbol: "BTCUSDT",
547
+ start: preloadStart,
548
+ end,
549
+ interval,
550
+ silent: true,
551
+ cacheOnly
552
+ })
553
+ ])
554
+ );
555
+ if (!cachedCoinData) {
556
+ state.coinKlineCache.set(coinCacheKey, data);
557
+ }
558
+ if (!cachedBtcData) {
559
+ state.btcKlineCache.set(btcCacheKey, btcData);
560
+ }
561
+ if (!cachedBtcBinanceData) {
562
+ state.btcBinanceKlineCache.set(btcBinanceCacheKey, btcBinanceData);
563
+ }
564
+ if (!cachedBtcCoinbaseData) {
565
+ state.btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseData);
566
+ }
567
+ const { prevData: prevDataRaw, testData: testDataRaw } = splitCandlesForTesting(data, start, preloadStart);
568
+ const { prevData: btcPrevDataRaw, testData: btcTestDataRaw } = splitCandlesForTesting(btcData, start, preloadStart);
569
+ const { prevData: btcBinancePrevDataRaw } = splitCandlesForTesting(
570
+ btcBinanceData,
571
+ start,
572
+ preloadStart
573
+ );
574
+ const { prevData: btcCoinbasePrevDataRaw } = splitCandlesForTesting(
575
+ btcCoinbaseData,
576
+ start,
577
+ preloadStart
578
+ );
579
+ const { alignedCoinCandles: prevData2, alignedBtcCandles: btcPrevData2 } = alignSortedCandlesByTimestamp(prevDataRaw, btcPrevDataRaw);
580
+ const { alignedCoinCandles: testData2, alignedBtcCandles: btcTestData2 } = alignSortedCandlesByTimestamp(testDataRaw, btcTestDataRaw);
581
+ const { alignedBtcCandles: btcBinancePrevData2 } = alignSortedCandlesByTimestamp(prevDataRaw, btcBinancePrevDataRaw);
582
+ const { alignedBtcCandles: btcCoinbasePrevData2 } = alignSortedCandlesByTimestamp(prevDataRaw, btcCoinbasePrevDataRaw);
583
+ preparedData = {
584
+ prevData: prevData2,
585
+ btcPrevData: btcPrevData2,
586
+ testData: testData2,
587
+ btcTestData: btcTestData2,
588
+ btcBinancePrevData: btcBinancePrevData2,
589
+ btcCoinbasePrevData: btcCoinbasePrevData2
590
+ };
591
+ state.preparedDataCache.set(preparedDataCacheKey, preparedData);
420
592
  }
421
- if (!cachedBtcCoinbaseData) {
422
- state.btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseData);
593
+ if (!preparedData) {
594
+ throw new Error("Prepared backtest data not available");
423
595
  }
424
- const prevDataRaw = data.filter(
425
- (candle) => candle.timestamp >= preloadStart && candle.timestamp < start
426
- );
427
- const btcPrevDataRaw = btcData.filter(
428
- (candle) => candle.timestamp >= preloadStart && candle.timestamp < start
429
- );
430
- const testDataRaw = data.filter(
431
- (candle) => candle.timestamp >= start
432
- );
433
- const btcTestDataRaw = btcData.filter(
434
- (candle) => candle.timestamp >= start
435
- );
436
- const { alignedCoinCandles: prevData, alignedBtcCandles: btcPrevData } = alignSortedCandlesByTimestamp(prevDataRaw, btcPrevDataRaw);
437
- const { alignedCoinCandles: testData, alignedBtcCandles: btcTestData } = alignSortedCandlesByTimestamp(testDataRaw, btcTestDataRaw);
438
- const { alignedBtcCandles: btcBinancePrevData } = alignSortedCandlesByTimestamp(
439
- prevDataRaw,
440
- btcBinanceData.filter(
441
- (candle) => candle.timestamp >= preloadStart && candle.timestamp < start
442
- )
443
- );
444
- const { alignedBtcCandles: btcCoinbasePrevData } = alignSortedCandlesByTimestamp(
445
- prevDataRaw,
446
- btcCoinbaseData.filter(
447
- (candle) => candle.timestamp >= preloadStart && candle.timestamp < start
448
- )
449
- );
596
+ const {
597
+ prevData,
598
+ btcPrevData,
599
+ testData,
600
+ btcTestData,
601
+ btcBinancePrevData,
602
+ btcCoinbasePrevData
603
+ } = preparedData;
450
604
  const testConnector = createTestConnector(connector, {
451
605
  userName,
452
- mlEnabled: ml
453
- });
454
- const strategy = await strategyCreator({
455
- userName,
456
- config: strategyConfig,
457
- symbol,
458
- data: prevData,
459
- btcData: btcPrevData,
460
- btcBinanceData: btcBinancePrevData,
461
- btcCoinbaseData: btcCoinbasePrevData,
462
- connector: testConnector
606
+ mlEnabled: ml,
607
+ aiEnabled: ai
463
608
  });
609
+ const strategy = await withTimeout(
610
+ "strategy init",
611
+ strategyCreator({
612
+ userName,
613
+ config: strategyConfig,
614
+ symbol,
615
+ data: prevData,
616
+ btcData: btcPrevData,
617
+ btcBinanceData: btcBinancePrevData,
618
+ btcCoinbaseData: btcCoinbasePrevData,
619
+ connector: testConnector
620
+ })
621
+ );
464
622
  const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
465
- const flushMlResultsBatch = async () => {
466
- if (!ml) return;
623
+ const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
624
+ const replaySignalEvaluations = [];
625
+ const flushClosedResultsBatch = async () => {
626
+ if (!ml && !ai) return;
467
627
  const batch = await testConnector.drainMlResultsBatch();
468
628
  if (!batch.length) return;
469
629
  for (const resultRecord of batch) {
470
630
  const payload = pendingMlPayloadBySignalId.get(resultRecord.signalId);
471
- if (!payload) continue;
472
- pendingMlPayloadBySignalId.delete(resultRecord.signalId);
473
- const fullRow = buildMlTrainingRow(payload, {
474
- profit: resultRecord.profit
475
- });
476
- const row = trimMlTrainingRowWindows(fullRow, 5);
477
- await appendMlDatasetRow({
478
- strategyName,
479
- chunkId,
480
- row
481
- });
631
+ if (payload) {
632
+ pendingMlPayloadBySignalId.delete(resultRecord.signalId);
633
+ const fullRow = buildMlTrainingRow(payload, {
634
+ profit: resultRecord.profit
635
+ });
636
+ const row = trimMlTrainingRowWindows(fullRow, 5);
637
+ await appendMlDatasetRow({
638
+ strategyName,
639
+ chunkId,
640
+ row
641
+ });
642
+ }
643
+ const aiRowBase = pendingAiRowBySignalId.get(resultRecord.signalId);
644
+ if (aiRowBase) {
645
+ pendingAiRowBySignalId.delete(resultRecord.signalId);
646
+ await appendAiDatasetRow({
647
+ strategyName,
648
+ chunkId,
649
+ row: {
650
+ ...aiRowBase,
651
+ profit: resultRecord.profit
652
+ }
653
+ });
654
+ }
482
655
  }
483
656
  };
484
657
  for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
658
+ if (candleIndex % 25 === 0) {
659
+ throwIfTimedOut("candle loop");
660
+ }
485
661
  const candle = testData[candleIndex];
486
662
  const btcCandle = btcTestData[candleIndex];
487
- await testConnector.checkSl(candle);
488
- await testConnector.checkTp(candle);
489
- const signal = await strategy(candle, btcCandle);
663
+ await withTimeout("stop-loss check", testConnector.checkSl(candle));
664
+ await withTimeout("take-profit check", testConnector.checkTp(candle));
665
+ const signal = await withTimeout(
666
+ "strategy signal",
667
+ strategy(candle, btcCandle)
668
+ );
669
+ if (!signal || typeof signal === "string") {
670
+ replaySignalEvaluations.push({
671
+ evaluationId: `${testId}:${strategyName}:${symbol}:${candle.timestamp}`,
672
+ userName,
673
+ strategy: strategyName,
674
+ symbol,
675
+ interval,
676
+ timestamp: candle.timestamp,
677
+ evaluatedAt: candle.timestamp,
678
+ status: "skip",
679
+ reason: typeof signal === "string" && signal.trim() ? signal : "NO_SIGNAL"
680
+ });
681
+ } else {
682
+ replaySignalEvaluations.push({
683
+ evaluationId: `${signal.signalId || testId}:${strategyName}:${symbol}:${signal.timestamp || candle.timestamp}`,
684
+ userName,
685
+ strategy: signal.strategy || strategyName,
686
+ symbol: signal.symbol || symbol,
687
+ interval: signal.interval || interval,
688
+ timestamp: typeof signal.timestamp === "number" && Number.isFinite(signal.timestamp) ? signal.timestamp : candle.timestamp,
689
+ evaluatedAt: candle.timestamp,
690
+ status: "signal",
691
+ reason: signal.orderSkipReason || signal.orderStatus,
692
+ signalId: signal.signalId,
693
+ direction: signal.direction,
694
+ orderStatus: signal.orderStatus,
695
+ orderSkipReason: signal.orderSkipReason,
696
+ aiAnalysis: signal.aiAnalysis ?? null,
697
+ ml: signal.ml
698
+ });
699
+ }
700
+ const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (ml || ai);
701
+ if (shouldCapturePayload) {
702
+ await withTimeout(
703
+ "derivatives context",
704
+ enrichSignalWithDerivativesContext({
705
+ signal,
706
+ env: "BACKTEST"
707
+ })
708
+ );
709
+ }
490
710
  if (ml && signal && typeof signal !== "string" && signal.signalId) {
491
711
  const payload = buildMlPayload({
492
712
  signal,
@@ -503,9 +723,27 @@ var testing = async ({
503
723
  });
504
724
  pendingMlPayloadBySignalId.set(signal.signalId, payload);
505
725
  }
726
+ if (ai && signal && typeof signal !== "string" && signal.signalId) {
727
+ pendingAiRowBySignalId.set(signal.signalId, {
728
+ signalId: signal.signalId,
729
+ strategyName: signal.strategy || strategyName,
730
+ symbol: signal.symbol || symbol,
731
+ direction: signal.direction,
732
+ timestamp: signal.timestamp,
733
+ payload: buildAiPayload(signal),
734
+ testId,
735
+ testSuiteId,
736
+ testName: name,
737
+ connectorName
738
+ });
739
+ }
506
740
  }
507
- await flushMlResultsBatch();
508
- return await testConnector.getResult();
741
+ await withTimeout("flush closed results", flushClosedResultsBatch());
742
+ const result = await withTimeout("collect result", testConnector.getResult());
743
+ return {
744
+ ...result,
745
+ inlineReplaySignalEvaluations: replaySignalEvaluations
746
+ };
509
747
  };
510
748
  export {
511
749
  createTestConnector,