@tradejs/node 1.0.5 → 1.0.6

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,22 +1,20 @@
1
1
  import {
2
2
  BUILTIN_CONNECTOR_NAMES,
3
3
  getConnectorCreatorByName
4
- } from "./chunk-CIY64D57.mjs";
4
+ } from "./chunk-XW5L327F.mjs";
5
5
  import {
6
6
  buildMlPayload
7
- } from "./chunk-RBE4PZER.mjs";
7
+ } from "./chunk-PQH5PAFC.mjs";
8
8
  import {
9
- require_lodash
10
- } from "./chunk-GKDBAF3A.mjs";
9
+ buildAiPayload
10
+ } from "./chunk-72FKXJ2I.mjs";
11
11
  import {
12
12
  getStrategyCreator
13
- } from "./chunk-ZY6ULOWK.mjs";
13
+ } from "./chunk-EOZSJKUM.mjs";
14
14
  import {
15
15
  getTradejsProjectCwd
16
- } from "./chunk-P2ZUWONT.mjs";
17
- import {
18
- __toESM
19
- } from "./chunk-6DZX6EAA.mjs";
16
+ } from "./chunk-CGJ2UU6H.mjs";
17
+ import "./chunk-6DZX6EAA.mjs";
20
18
 
21
19
  // src/backtest.ts
22
20
  export * from "@tradejs/core/backtest";
@@ -25,6 +23,7 @@ export * from "@tradejs/core/backtest";
25
23
  import { alignSortedCandlesByTimestamp } from "@tradejs/core/indicators";
26
24
  import { PRELOAD_DAYS } from "@tradejs/core/constants";
27
25
  import { getTimestamp } 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
  });
@@ -106,33 +102,20 @@ var createTestConnector = (connector, context) => {
106
102
  kline: async (options) => connector.kline(options),
107
103
  getResult: async () => {
108
104
  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
105
  return {
125
106
  stat: {
126
107
  amount,
127
108
  profit: amount - INITIAL_AMOUNT,
128
109
  orders: positionLog.length
129
110
  },
130
- orderLogId
111
+ orderLogId,
112
+ inlineOrderLog: [...orderLog],
113
+ inlinePositionLog: [...positionLog]
131
114
  };
132
115
  },
133
116
  getPosition: async () => currentPosition || null,
134
117
  checkTp: async (candle) => {
135
- if (import_lodash.default.isEmpty(candle) || !currentPosition || !currentPosition.qty) {
118
+ if (!candle || !currentPosition || !currentPosition.qty) {
136
119
  return;
137
120
  }
138
121
  const isLong = currentPosition.direction === "LONG";
@@ -167,7 +150,7 @@ var createTestConnector = (connector, context) => {
167
150
  }
168
151
  },
169
152
  checkSl: async (candle) => {
170
- if (!stopLossPrice || !currentPosition || import_lodash.default.isEmpty(candle)) {
153
+ if (!stopLossPrice || !currentPosition || !candle) {
171
154
  return;
172
155
  }
173
156
  const isLong = currentPosition.direction === "LONG";
@@ -187,13 +170,11 @@ var createTestConnector = (connector, context) => {
187
170
  clearPosition(candle.timestamp);
188
171
  }
189
172
  },
190
- placeOrder: async (order, tp = [], slPrice) => {
173
+ placeOrder: async (order) => {
191
174
  if (currentPosition) {
192
175
  return false;
193
176
  }
194
177
  const isLong = order.direction === "LONG";
195
- takeProfits = import_lodash.default.cloneDeep(tp);
196
- stopLossPrice = slPrice || null;
197
178
  currentPosition = { ...order, amount };
198
179
  originalQty = order.qty;
199
180
  const fee = order.price * order.qty * FEE;
@@ -208,6 +189,24 @@ var createTestConnector = (connector, context) => {
208
189
  });
209
190
  return true;
210
191
  },
192
+ setTakeProfits: async ({ takeProfits: nextTakeProfits }) => {
193
+ if (!currentPosition) {
194
+ return false;
195
+ }
196
+ takeProfits = Array.isArray(nextTakeProfits) ? nextTakeProfits.map((tp) => ({ ...tp })) : [];
197
+ return true;
198
+ },
199
+ setStopLoss: async ({ stopLossPrice: nextStopLossPrice }) => {
200
+ if (!currentPosition) {
201
+ return false;
202
+ }
203
+ stopLossPrice = nextStopLossPrice || null;
204
+ currentPosition = {
205
+ ...currentPosition,
206
+ ...stopLossPrice != null ? { slPrice: stopLossPrice } : {}
207
+ };
208
+ return true;
209
+ },
211
210
  closePosition: async (order) => {
212
211
  if (!currentPosition) {
213
212
  return false;
@@ -227,7 +226,7 @@ var createTestConnector = (connector, context) => {
227
226
  },
228
227
  getTickers: connector.getTickers,
229
228
  getPositions: connector.getPositions,
230
- drainMlResultsBatch: async () => closedMlResults.splice(0)
229
+ drainMlResultsBatch: async () => closedSignalResults.splice(0)
231
230
  };
232
231
  };
233
232
 
@@ -237,7 +236,9 @@ var createTestingKlineCacheState = () => ({
237
236
  coinKlineCache: /* @__PURE__ */ new Map(),
238
237
  btcKlineCache: /* @__PURE__ */ new Map(),
239
238
  btcBinanceKlineCache: /* @__PURE__ */ new Map(),
240
- btcCoinbaseKlineCache: /* @__PURE__ */ new Map()
239
+ btcCoinbaseKlineCache: /* @__PURE__ */ new Map(),
240
+ preparedDataCache: /* @__PURE__ */ new Map(),
241
+ connectorCache: /* @__PURE__ */ new Map()
241
242
  });
242
243
  var testingKlineCacheStateByProjectRoot = /* @__PURE__ */ new Map();
243
244
  var getTestingKlineCacheState = (cwd = getTradejsProjectCwd()) => {
@@ -264,6 +265,62 @@ var getKlineCacheKey = (params) => {
264
265
  cacheOnly ? 1 : 0
265
266
  ].join(":");
266
267
  };
268
+ var getPreparedDataCacheKey = (params) => {
269
+ const {
270
+ userName,
271
+ connectorName,
272
+ symbol,
273
+ start,
274
+ end,
275
+ interval,
276
+ btcBinanceConnectorName,
277
+ btcCoinbaseConnectorName
278
+ } = params;
279
+ return [
280
+ userName,
281
+ connectorName,
282
+ symbol,
283
+ start,
284
+ end,
285
+ interval,
286
+ btcBinanceConnectorName,
287
+ btcCoinbaseConnectorName
288
+ ].join(":");
289
+ };
290
+ var getConnectorCacheKey = (params) => [params.userName, params.connectorName].join(":");
291
+ var splitCandlesForTesting = (candles, start) => {
292
+ const prevData = [];
293
+ const testData = [];
294
+ for (const candle of candles) {
295
+ if (candle.timestamp < preloadStart) continue;
296
+ if (candle.timestamp < start) {
297
+ prevData.push(candle);
298
+ } else {
299
+ testData.push(candle);
300
+ }
301
+ }
302
+ return { prevData, testData };
303
+ };
304
+ var getCachedConnector = async (params) => {
305
+ const { state, projectRoot, userName, connectorName } = params;
306
+ const cacheKey = getConnectorCacheKey({ userName, connectorName });
307
+ const cachedConnector = state.connectorCache.get(cacheKey);
308
+ if (cachedConnector) {
309
+ return cachedConnector;
310
+ }
311
+ const connectorCreator = await getConnectorCreatorByName(
312
+ connectorName,
313
+ projectRoot
314
+ );
315
+ if (!connectorCreator) {
316
+ return void 0;
317
+ }
318
+ const connector = await connectorCreator({
319
+ userName
320
+ });
321
+ state.connectorCache.set(cacheKey, connector);
322
+ return connector;
323
+ };
267
324
  var resetTestingKlineCache = (cwd) => {
268
325
  const normalizedCwd = String(cwd ?? "").trim();
269
326
  if (!normalizedCwd) {
@@ -285,35 +342,88 @@ var testing = async ({
285
342
  strategyConfig,
286
343
  connectorName,
287
344
  ml = false,
288
- chunkId = "single"
345
+ ai = false,
346
+ chunkId = "single",
347
+ timeoutMs
289
348
  }) => {
290
349
  if (!start) {
291
350
  throw new Error("no start");
292
351
  }
352
+ const startedAt = Date.now();
353
+ const formatTimeoutMessage = (stage) => `Test ${name} (${symbol}) timed out after ${timeoutMs}ms during ${stage}`;
354
+ const getRemainingTimeoutMs = (stage) => {
355
+ if (!timeoutMs || timeoutMs <= 0) {
356
+ return null;
357
+ }
358
+ const remainingMs = timeoutMs - (Date.now() - startedAt);
359
+ if (remainingMs <= 0) {
360
+ throw new Error(formatTimeoutMessage(stage));
361
+ }
362
+ return remainingMs;
363
+ };
364
+ const throwIfTimedOut = (stage) => {
365
+ getRemainingTimeoutMs(stage);
366
+ };
367
+ const withTimeout = async (stage, promise) => {
368
+ const remainingMs = getRemainingTimeoutMs(stage);
369
+ if (remainingMs == null) {
370
+ return promise;
371
+ }
372
+ return await new Promise((resolve, reject) => {
373
+ const timer = setTimeout(() => {
374
+ reject(new Error(formatTimeoutMessage(stage)));
375
+ }, remainingMs);
376
+ promise.then(
377
+ (value) => {
378
+ clearTimeout(timer);
379
+ resolve(value);
380
+ },
381
+ (error) => {
382
+ clearTimeout(timer);
383
+ reject(error);
384
+ }
385
+ );
386
+ });
387
+ };
293
388
  const { projectRoot, state } = getTestingKlineCacheState();
294
- const connectorCreator = await getConnectorCreatorByName(
295
- connectorName,
296
- projectRoot
389
+ const connector = await withTimeout(
390
+ "connector init",
391
+ getCachedConnector({
392
+ state,
393
+ projectRoot,
394
+ userName,
395
+ connectorName
396
+ })
297
397
  );
298
- if (!connectorCreator) {
398
+ if (!connector) {
299
399
  throw new Error(`Unknown connector: ${connectorName}`);
300
400
  }
301
- const connector = await connectorCreator({
302
- userName
303
- });
304
- const strategyCreator = await getStrategyCreator(strategyName, projectRoot);
401
+ const strategyCreator = await withTimeout(
402
+ "strategy lookup",
403
+ getStrategyCreator(strategyName, projectRoot)
404
+ );
305
405
  if (!strategyCreator) {
306
406
  throw new Error(`Unknown strategy: ${strategyName}`);
307
407
  }
308
- const binanceCreator = await getConnectorCreatorByName(
309
- BUILTIN_CONNECTOR_NAMES.Binance,
310
- projectRoot
408
+ const binanceConnector = await withTimeout(
409
+ "binance connector init",
410
+ getCachedConnector({
411
+ state,
412
+ projectRoot,
413
+ userName,
414
+ connectorName: BUILTIN_CONNECTOR_NAMES.Binance
415
+ })
311
416
  );
312
- const coinbaseCreator = await getConnectorCreatorByName(
313
- BUILTIN_CONNECTOR_NAMES.Coinbase,
314
- projectRoot
417
+ const coinbaseConnector = await withTimeout(
418
+ "coinbase connector init",
419
+ getCachedConnector({
420
+ state,
421
+ projectRoot,
422
+ userName,
423
+ connectorName: BUILTIN_CONNECTOR_NAMES.Coinbase
424
+ })
315
425
  );
316
- if (!binanceCreator || !coinbaseCreator) {
426
+ if (!binanceConnector || !coinbaseConnector) {
317
427
  logger.warn(
318
428
  "Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
319
429
  connectorName
@@ -337,11 +447,13 @@ var testing = async ({
337
447
  interval,
338
448
  cacheOnly
339
449
  });
450
+ const btcBinanceConnectorName = binanceConnector ? BUILTIN_CONNECTOR_NAMES.Binance : connectorName;
451
+ const btcCoinbaseConnectorName = coinbaseConnector ? BUILTIN_CONNECTOR_NAMES.Coinbase : connectorName;
340
452
  const cachedCoinData = state.coinKlineCache.get(coinCacheKey);
341
453
  const cachedBtcData = state.btcKlineCache.get(btcCacheKey);
342
454
  const btcBinanceCacheKey = getKlineCacheKey({
343
455
  userName,
344
- connectorName: binanceCreator ? BUILTIN_CONNECTOR_NAMES.Binance : connectorName,
456
+ connectorName: btcBinanceConnectorName,
345
457
  symbol: "BTCUSDT",
346
458
  end,
347
459
  interval,
@@ -349,7 +461,7 @@ var testing = async ({
349
461
  });
350
462
  const btcCoinbaseCacheKey = getKlineCacheKey({
351
463
  userName,
352
- connectorName: coinbaseCreator ? BUILTIN_CONNECTOR_NAMES.Coinbase : connectorName,
464
+ connectorName: btcCoinbaseConnectorName,
353
465
  symbol: "BTCUSDT",
354
466
  end,
355
467
  interval,
@@ -357,136 +469,166 @@ var testing = async ({
357
469
  });
358
470
  const cachedBtcBinanceData = state.btcBinanceKlineCache.get(btcBinanceCacheKey);
359
471
  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);
472
+ const preparedDataCacheKey = getPreparedDataCacheKey({
473
+ userName,
474
+ connectorName,
475
+ symbol,
476
+ start,
477
+ end,
478
+ interval,
479
+ btcBinanceConnectorName,
480
+ btcCoinbaseConnectorName
481
+ });
482
+ let preparedData = state.preparedDataCache.get(preparedDataCacheKey);
483
+ if (!preparedData) {
484
+ const [data, btcData, btcBinanceData, btcCoinbaseData] = await withTimeout(
485
+ "kline preload",
486
+ Promise.all([
487
+ cachedCoinData ? Promise.resolve(cachedCoinData) : connector.kline({
488
+ symbol,
489
+ start: preloadStart,
490
+ end,
491
+ interval,
492
+ silent: true,
493
+ cacheOnly
494
+ }),
495
+ cachedBtcData ? Promise.resolve(cachedBtcData) : connector.kline({
496
+ symbol: "BTCUSDT",
497
+ start: preloadStart,
498
+ end,
499
+ interval,
500
+ silent: true,
501
+ cacheOnly
502
+ }),
503
+ cachedBtcBinanceData ? Promise.resolve(cachedBtcBinanceData) : (binanceConnector ?? connector).kline({
504
+ symbol: "BTCUSDT",
505
+ start: preloadStart,
506
+ end,
507
+ interval,
508
+ silent: true,
509
+ cacheOnly
510
+ }),
511
+ cachedBtcCoinbaseData ? Promise.resolve(cachedBtcCoinbaseData) : (coinbaseConnector ?? connector).kline({
512
+ symbol: "BTCUSDT",
513
+ start: preloadStart,
514
+ end,
515
+ interval,
516
+ silent: true,
517
+ cacheOnly
518
+ })
519
+ ])
520
+ );
521
+ if (!cachedCoinData) {
522
+ state.coinKlineCache.set(coinCacheKey, data);
523
+ }
524
+ if (!cachedBtcData) {
525
+ state.btcKlineCache.set(btcCacheKey, btcData);
526
+ }
527
+ if (!cachedBtcBinanceData) {
528
+ state.btcBinanceKlineCache.set(btcBinanceCacheKey, btcBinanceData);
529
+ }
530
+ if (!cachedBtcCoinbaseData) {
531
+ state.btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseData);
532
+ }
533
+ const { prevData: prevDataRaw, testData: testDataRaw } = splitCandlesForTesting(data, start);
534
+ const { prevData: btcPrevDataRaw, testData: btcTestDataRaw } = splitCandlesForTesting(btcData, start);
535
+ const { prevData: btcBinancePrevDataRaw } = splitCandlesForTesting(
536
+ btcBinanceData,
537
+ start
538
+ );
539
+ const { prevData: btcCoinbasePrevDataRaw } = splitCandlesForTesting(
540
+ btcCoinbaseData,
541
+ start
542
+ );
543
+ const { alignedCoinCandles: prevData2, alignedBtcCandles: btcPrevData2 } = alignSortedCandlesByTimestamp(prevDataRaw, btcPrevDataRaw);
544
+ const { alignedCoinCandles: testData2, alignedBtcCandles: btcTestData2 } = alignSortedCandlesByTimestamp(testDataRaw, btcTestDataRaw);
545
+ const { alignedBtcCandles: btcBinancePrevData2 } = alignSortedCandlesByTimestamp(prevDataRaw, btcBinancePrevDataRaw);
546
+ const { alignedBtcCandles: btcCoinbasePrevData2 } = alignSortedCandlesByTimestamp(prevDataRaw, btcCoinbasePrevDataRaw);
547
+ preparedData = {
548
+ prevData: prevData2,
549
+ btcPrevData: btcPrevData2,
550
+ testData: testData2,
551
+ btcTestData: btcTestData2,
552
+ btcBinancePrevData: btcBinancePrevData2,
553
+ btcCoinbasePrevData: btcCoinbasePrevData2
554
+ };
555
+ state.preparedDataCache.set(preparedDataCacheKey, preparedData);
420
556
  }
421
- if (!cachedBtcCoinbaseData) {
422
- state.btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseData);
557
+ if (!preparedData) {
558
+ throw new Error("Prepared backtest data not available");
423
559
  }
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
- );
560
+ const {
561
+ prevData,
562
+ btcPrevData,
563
+ testData,
564
+ btcTestData,
565
+ btcBinancePrevData,
566
+ btcCoinbasePrevData
567
+ } = preparedData;
450
568
  const testConnector = createTestConnector(connector, {
451
569
  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
570
+ mlEnabled: ml,
571
+ aiEnabled: ai
463
572
  });
573
+ const strategy = await withTimeout(
574
+ "strategy init",
575
+ strategyCreator({
576
+ userName,
577
+ config: strategyConfig,
578
+ symbol,
579
+ data: prevData,
580
+ btcData: btcPrevData,
581
+ btcBinanceData: btcBinancePrevData,
582
+ btcCoinbaseData: btcCoinbasePrevData,
583
+ connector: testConnector
584
+ })
585
+ );
464
586
  const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
465
- const flushMlResultsBatch = async () => {
466
- if (!ml) return;
587
+ const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
588
+ const flushClosedResultsBatch = async () => {
589
+ if (!ml && !ai) return;
467
590
  const batch = await testConnector.drainMlResultsBatch();
468
591
  if (!batch.length) return;
469
592
  for (const resultRecord of batch) {
470
593
  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
- });
594
+ if (payload) {
595
+ pendingMlPayloadBySignalId.delete(resultRecord.signalId);
596
+ const fullRow = buildMlTrainingRow(payload, {
597
+ profit: resultRecord.profit
598
+ });
599
+ const row = trimMlTrainingRowWindows(fullRow, 5);
600
+ await appendMlDatasetRow({
601
+ strategyName,
602
+ chunkId,
603
+ row
604
+ });
605
+ }
606
+ const aiRowBase = pendingAiRowBySignalId.get(resultRecord.signalId);
607
+ if (aiRowBase) {
608
+ pendingAiRowBySignalId.delete(resultRecord.signalId);
609
+ await appendAiDatasetRow({
610
+ strategyName,
611
+ chunkId,
612
+ row: {
613
+ ...aiRowBase,
614
+ profit: resultRecord.profit
615
+ }
616
+ });
617
+ }
482
618
  }
483
619
  };
484
620
  for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
621
+ if (candleIndex % 25 === 0) {
622
+ throwIfTimedOut("candle loop");
623
+ }
485
624
  const candle = testData[candleIndex];
486
625
  const btcCandle = btcTestData[candleIndex];
487
- await testConnector.checkSl(candle);
488
- await testConnector.checkTp(candle);
489
- const signal = await strategy(candle, btcCandle);
626
+ await withTimeout("stop-loss check", testConnector.checkSl(candle));
627
+ await withTimeout("take-profit check", testConnector.checkTp(candle));
628
+ const signal = await withTimeout(
629
+ "strategy signal",
630
+ strategy(candle, btcCandle)
631
+ );
490
632
  if (ml && signal && typeof signal !== "string" && signal.signalId) {
491
633
  const payload = buildMlPayload({
492
634
  signal,
@@ -503,9 +645,23 @@ var testing = async ({
503
645
  });
504
646
  pendingMlPayloadBySignalId.set(signal.signalId, payload);
505
647
  }
648
+ if (ai && signal && typeof signal !== "string" && signal.signalId) {
649
+ pendingAiRowBySignalId.set(signal.signalId, {
650
+ signalId: signal.signalId,
651
+ strategyName: signal.strategy || strategyName,
652
+ symbol: signal.symbol || symbol,
653
+ direction: signal.direction,
654
+ timestamp: signal.timestamp,
655
+ payload: buildAiPayload(signal),
656
+ testId,
657
+ testSuiteId,
658
+ testName: name,
659
+ connectorName
660
+ });
661
+ }
506
662
  }
507
- await flushMlResultsBatch();
508
- return await testConnector.getResult();
663
+ await withTimeout("flush closed results", flushClosedResultsBatch());
664
+ return await withTimeout("collect result", testConnector.getResult());
509
665
  };
510
666
  export {
511
667
  createTestConnector,