@tradejs/node 2.0.18 → 2.0.20

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
@@ -3,27 +3,20 @@ import {
3
3
  getConnectorCreatorByName
4
4
  } from "./chunk-V3YMKE4I.mjs";
5
5
  import {
6
+ buildAiPayload,
6
7
  buildMlPayload,
7
- enrichSignalWithMarketContextStages
8
- } from "./chunk-3BP3ETAD.mjs";
9
- import {
10
- buildAiPayload
11
- } from "./chunk-WK5EUCX5.mjs";
12
- import {
8
+ enrichSignalWithMarketContextStages,
13
9
  getStrategyCreator
14
- } from "./chunk-QVSMINLG.mjs";
10
+ } from "./chunk-4AVFJMQL.mjs";
15
11
  import {
16
12
  getTradejsProjectCwd
17
13
  } from "./chunk-WS5DYEVZ.mjs";
18
- import "./chunk-UV2HVMKZ.mjs";
14
+ import "./chunk-Y6FXYEAI.mjs";
19
15
 
20
16
  // src/backtest.ts
21
17
  export * from "@tradejs/core/backtest";
22
18
 
23
19
  // src/testing.ts
24
- import {
25
- BACKTEST_WARNING_CODES
26
- } from "@tradejs/types";
27
20
  import { alignSortedCandlesByTimestamp } from "@tradejs/core/indicators";
28
21
  import {
29
22
  BACKTEST_EXECUTION_INTERVAL,
@@ -34,17 +27,227 @@ import {
34
27
  releaseStrategyReplayCache
35
28
  } from "@tradejs/core/strategies";
36
29
  import { getBacktestPreloadStart } from "@tradejs/core/time";
30
+ import { logger } from "@tradejs/infra/logger";
31
+
32
+ // src/backtest/progress.ts
33
+ var DEFAULT_STRATEGY_CANDLE_TIMEOUT_MS = 6e4;
34
+ var resolvePositiveInt = (value, fallback) => {
35
+ const parsed = parseInt(String(value ?? ""), 10);
36
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
37
+ };
38
+ var getEffectiveTimeoutMs = (baseTimeoutMs, stageTimeoutMs) => {
39
+ const base = baseTimeoutMs && baseTimeoutMs > 0 ? Math.trunc(baseTimeoutMs) : null;
40
+ const stage = stageTimeoutMs && stageTimeoutMs > 0 ? Math.trunc(stageTimeoutMs) : null;
41
+ if (base == null) return stage;
42
+ if (stage == null) return base;
43
+ return Math.min(base, stage);
44
+ };
45
+ var createBacktestProgress = ({
46
+ testName,
47
+ symbol,
48
+ strategyName,
49
+ timeoutMs,
50
+ timeoutSubject
51
+ }) => {
52
+ const startedAt = Date.now();
53
+ let activeStageStartedAt = startedAt;
54
+ let lastProgressSentAt = 0;
55
+ let lastProgressSignature = "";
56
+ let currentCandleIndex = 0;
57
+ let totalCandles = 0;
58
+ const strategyCandleTimeoutMs = resolvePositiveInt(
59
+ process.env.BACKTEST_STRATEGY_CANDLE_TIMEOUT_MS,
60
+ DEFAULT_STRATEGY_CANDLE_TIMEOUT_MS
61
+ );
62
+ const emit = (stage, force = false) => {
63
+ const now = Date.now();
64
+ const signature = [
65
+ stage,
66
+ currentCandleIndex,
67
+ totalCandles,
68
+ Math.floor((now - activeStageStartedAt) / 5e3)
69
+ ].join(":");
70
+ if (!force) {
71
+ if (signature === lastProgressSignature || now - lastProgressSentAt < 4e3) {
72
+ return;
73
+ }
74
+ }
75
+ lastProgressSentAt = now;
76
+ lastProgressSignature = signature;
77
+ process.send?.({
78
+ progress: true,
79
+ testName,
80
+ symbol,
81
+ strategyName,
82
+ stage,
83
+ candleIndex: currentCandleIndex,
84
+ candleTotal: totalCandles,
85
+ elapsedMs: now - startedAt,
86
+ stageElapsedMs: now - activeStageStartedAt
87
+ });
88
+ };
89
+ const runWithTimeout = async (stage, action, stageTimeoutOverrideMs) => {
90
+ const stageTimeoutMs = getEffectiveTimeoutMs(
91
+ timeoutMs,
92
+ stageTimeoutOverrideMs
93
+ );
94
+ if (stageTimeoutMs == null) return action();
95
+ activeStageStartedAt = Date.now();
96
+ emit(stage, true);
97
+ const promise = action();
98
+ return await new Promise((resolve, reject) => {
99
+ const heartbeat = setInterval(() => emit(stage), 5e3);
100
+ const timer = setTimeout(() => {
101
+ clearInterval(heartbeat);
102
+ reject(
103
+ new Error(
104
+ `${timeoutSubject} timed out after ${stageTimeoutMs}ms during ${stage}`
105
+ )
106
+ );
107
+ }, stageTimeoutMs);
108
+ promise.then(
109
+ (value) => {
110
+ clearInterval(heartbeat);
111
+ clearTimeout(timer);
112
+ resolve(value);
113
+ },
114
+ (error) => {
115
+ clearInterval(heartbeat);
116
+ clearTimeout(timer);
117
+ reject(error);
118
+ }
119
+ );
120
+ });
121
+ };
122
+ return {
123
+ run: (stage, action) => runWithTimeout(stage, action, null),
124
+ runStrategy: (stage, action) => runWithTimeout(stage, action, strategyCandleTimeoutMs),
125
+ contextStage: (stage) => {
126
+ activeStageStartedAt = Date.now();
127
+ emit(`${stage} context`, true);
128
+ },
129
+ checkpoint: (stage) => {
130
+ if (timeoutMs && timeoutMs > 0) emit(stage);
131
+ },
132
+ setCandle: (index, total) => {
133
+ currentCandleIndex = index;
134
+ totalCandles = total;
135
+ emit("candle loop", index === 1 || index === total);
136
+ }
137
+ };
138
+ };
139
+
140
+ // src/backtest/session.ts
141
+ import {
142
+ BACKTEST_WARNING_CODES
143
+ } from "@tradejs/types";
37
144
  import { appendAiDatasetRow } from "@tradejs/infra/ai";
145
+ import { appendCoreResearchTraceEvent } from "@tradejs/infra/coreResearch";
38
146
  import {
39
147
  appendMlDatasetRow,
40
148
  buildMlTrainingRow,
41
149
  trimMlTrainingRowWindows
42
150
  } from "@tradejs/infra/ml";
43
- import { logger } from "@tradejs/infra/logger";
151
+
152
+ // src/executionCosts.ts
153
+ import {
154
+ BACKTEST_BASE_SLIPPAGE_BPS,
155
+ BACKTEST_DELAY_RISK_MULTIPLIER,
156
+ BACKTEST_MARKET_IMPACT_BPS,
157
+ BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER,
158
+ FEE_PERCENT
159
+ } from "@tradejs/core/constants";
160
+ var finiteOr = (value, fallback) => {
161
+ const parsed = Number(value);
162
+ return Number.isFinite(parsed) ? parsed : fallback;
163
+ };
164
+ var feeCache = /* @__PURE__ */ new WeakMap();
165
+ var fundingCache = /* @__PURE__ */ new WeakMap();
166
+ var loadTradingFee = async (connector, symbol) => {
167
+ if (!connector.getTradingFeeRate) return null;
168
+ let cache = feeCache.get(connector);
169
+ if (!cache) {
170
+ cache = /* @__PURE__ */ new Map();
171
+ feeCache.set(connector, cache);
172
+ }
173
+ const key = symbol.toUpperCase();
174
+ const cached = cache.get(key);
175
+ if (cached) return cached;
176
+ const rate = await connector.getTradingFeeRate(symbol);
177
+ cache.set(key, rate);
178
+ return rate;
179
+ };
180
+ var loadFundingRates = (connector, symbol, startTime, endTime) => {
181
+ if (!connector.getFundingRateHistory) return Promise.resolve([]);
182
+ let cache = fundingCache.get(connector);
183
+ if (!cache) {
184
+ cache = /* @__PURE__ */ new Map();
185
+ fundingCache.set(connector, cache);
186
+ }
187
+ const key = `${symbol.toUpperCase()}:${startTime}:${endTime}`;
188
+ const cached = cache.get(key);
189
+ if (cached) return cached;
190
+ const pending = connector.getFundingRateHistory({ symbol, startTime, endTime }).catch(() => []);
191
+ cache.set(key, pending);
192
+ return pending;
193
+ };
194
+ var resolveExecutionCosts = async (params) => {
195
+ const { connector, symbol, config, startTime, endTime, instrument } = params;
196
+ const cacheOnly = config.EXECUTION_COSTS_CACHE_ONLY === true;
197
+ const hasConfiguredFees = Number.isFinite(Number(config.MAKER_FEE_RATE)) && Number.isFinite(Number(config.TAKER_FEE_RATE));
198
+ const exchangeFees = !cacheOnly && !hasConfiguredFees && connector.getTradingFeeRate ? await loadTradingFee(connector, symbol).catch(() => null) : null;
199
+ const makerRate = hasConfiguredFees ? Number(config.MAKER_FEE_RATE) : exchangeFees?.makerRate ?? FEE_PERCENT;
200
+ const takerRate = hasConfiguredFees ? Number(config.TAKER_FEE_RATE) : exchangeFees?.takerRate ?? FEE_PERCENT;
201
+ const fundingEnabled = config.FUNDING_ENABLED !== false && !cacheOnly && typeof connector.getFundingRateHistory === "function";
202
+ const fundingRates = fundingEnabled ? await loadFundingRates(connector, symbol, startTime, endTime) : [];
203
+ const requestedLeverage = Math.max(1, finiteOr(config.LEVERAGE, 10));
204
+ const venueMaxLeverage = Number(instrument?.venueMetadata?.maxLeverage);
205
+ const maxAllowed = Number.isFinite(venueMaxLeverage) ? venueMaxLeverage : null;
206
+ const effectiveLeverage = maxAllowed == null ? requestedLeverage : Math.min(requestedLeverage, maxAllowed);
207
+ const feeSource = hasConfiguredFees ? "config" : exchangeFees?.source ?? "fallback";
208
+ const fundingSource = !fundingEnabled ? cacheOnly ? "fallback" : "disabled" : fundingRates.length ? "historical" : "unavailable";
209
+ const usesFallback = feeSource === "fallback" || fundingEnabled && fundingSource === "unavailable" || config.SLIPPAGE_BASE_BPS == null && config.SLIPPAGE_SPREAD_MULTIPLIER == null && config.SLIPPAGE_MARKET_IMPACT_BPS == null;
210
+ return {
211
+ model: {
212
+ fees: { makerRate, takerRate, source: feeSource },
213
+ funding: {
214
+ enabled: fundingEnabled,
215
+ source: fundingSource,
216
+ points: fundingRates.length,
217
+ fromTimestamp: fundingRates[0]?.timestamp ?? null,
218
+ toTimestamp: fundingRates.at(-1)?.timestamp ?? null
219
+ },
220
+ slippage: {
221
+ baseBps: finiteOr(config.SLIPPAGE_BASE_BPS, BACKTEST_BASE_SLIPPAGE_BPS),
222
+ spreadMultiplier: finiteOr(
223
+ config.SLIPPAGE_SPREAD_MULTIPLIER,
224
+ BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER
225
+ ),
226
+ marketImpactBps: finiteOr(
227
+ config.SLIPPAGE_MARKET_IMPACT_BPS,
228
+ BACKTEST_MARKET_IMPACT_BPS
229
+ ),
230
+ delayRiskMultiplier: finiteOr(
231
+ config.SLIPPAGE_DELAY_RISK_MULTIPLIER,
232
+ BACKTEST_DELAY_RISK_MULTIPLIER
233
+ ),
234
+ source: config.SLIPPAGE_BASE_BPS != null || config.SLIPPAGE_SPREAD_MULTIPLIER != null || config.SLIPPAGE_MARKET_IMPACT_BPS != null ? "config" : "fallback"
235
+ },
236
+ leverage: {
237
+ requested: requestedLeverage,
238
+ effective: effectiveLeverage,
239
+ maxAllowed
240
+ },
241
+ quality: usesFallback ? "fallback" : fundingEnabled && fundingRates.length ? "full" : "partial",
242
+ capturedAt: Date.now()
243
+ },
244
+ fundingRates
245
+ };
246
+ };
44
247
 
45
248
  // src/testConnector.ts
46
249
  import { randomUUID } from "crypto";
47
- import { FEE_PERCENT, INITIAL_BACKTEST_AMOUNT } from "@tradejs/core/constants";
250
+ import { FEE_PERCENT as FEE_PERCENT2, INITIAL_BACKTEST_AMOUNT } from "@tradejs/core/constants";
48
251
  import { calculateStatsFull } from "@tradejs/core/backtest";
49
252
  import {
50
253
  applyExecutionSlippage as applyModeledExecutionSlippage,
@@ -77,8 +280,8 @@ var createTestConnector = (connector, context) => {
77
280
  const positionLog = [];
78
281
  const fastMode = Boolean(context?.fastMode);
79
282
  const executionCostModel = context?.executionCostModel;
80
- const makerFeeRate = executionCostModel?.fees.makerRate ?? FEE_PERCENT;
81
- const takerFeeRate = executionCostModel?.fees.takerRate ?? FEE_PERCENT;
283
+ const makerFeeRate = executionCostModel?.fees.makerRate ?? FEE_PERCENT2;
284
+ const takerFeeRate = executionCostModel?.fees.takerRate ?? FEE_PERCENT2;
82
285
  const fundingRates = [...context?.fundingRates ?? []].sort(
83
286
  (left, right) => left.timestamp - right.timestamp
84
287
  );
@@ -1054,158 +1257,58 @@ var createTestConnector = (connector, context) => {
1054
1257
  };
1055
1258
  };
1056
1259
 
1057
- // src/executionCosts.ts
1058
- import {
1059
- BACKTEST_BASE_SLIPPAGE_BPS,
1060
- BACKTEST_DELAY_RISK_MULTIPLIER,
1061
- BACKTEST_MARKET_IMPACT_BPS,
1062
- BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER,
1063
- FEE_PERCENT as FEE_PERCENT2
1064
- } from "@tradejs/core/constants";
1065
- var finiteOr = (value, fallback) => {
1066
- const parsed = Number(value);
1067
- return Number.isFinite(parsed) ? parsed : fallback;
1068
- };
1069
- var feeCache = /* @__PURE__ */ new WeakMap();
1070
- var fundingCache = /* @__PURE__ */ new WeakMap();
1071
- var loadTradingFee = async (connector, symbol) => {
1072
- if (!connector.getTradingFeeRate) return null;
1073
- let cache = feeCache.get(connector);
1074
- if (!cache) {
1075
- cache = /* @__PURE__ */ new Map();
1076
- feeCache.set(connector, cache);
1260
+ // src/backtest/researchTrace.ts
1261
+ var asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : null;
1262
+ var findSetupKey = (value, depth = 0) => {
1263
+ if (depth > 5) return null;
1264
+ const record = asRecord(value);
1265
+ if (!record) return null;
1266
+ for (const key of ["setupIdentity", "setupId", "patternId", "candidateId"]) {
1267
+ const candidate = record[key];
1268
+ if (typeof candidate === "string" && candidate.trim() || typeof candidate === "number" && Number.isFinite(candidate)) {
1269
+ return `${key}:${String(candidate)}`;
1270
+ }
1077
1271
  }
1078
- const key = symbol.toUpperCase();
1079
- const cached = cache.get(key);
1080
- if (cached) return cached;
1081
- const rate = await connector.getTradingFeeRate(symbol);
1082
- cache.set(key, rate);
1083
- return rate;
1272
+ for (const [key, nested] of Object.entries(record)) {
1273
+ if (/context|signal|setup|pattern|candidate|strategy/i.test(key) && nested != null) {
1274
+ const found = findSetupKey(nested, depth + 1);
1275
+ if (found) return found;
1276
+ }
1277
+ }
1278
+ return null;
1084
1279
  };
1085
- var loadFundingRates = (connector, symbol, startTime, endTime) => {
1086
- if (!connector.getFundingRateHistory) return Promise.resolve([]);
1087
- let cache = fundingCache.get(connector);
1088
- if (!cache) {
1089
- cache = /* @__PURE__ */ new Map();
1090
- fundingCache.set(connector, cache);
1280
+ var resolveCoreResearchSetupIdentity = (signal) => {
1281
+ const strategyKey = findSetupKey(signal.additionalIndicators);
1282
+ return strategyKey ? {
1283
+ setupIdentity: `${signal.strategy}|${signal.symbol}|${signal.direction}|${strategyKey}`,
1284
+ setupIdentitySource: "strategy-context"
1285
+ } : {
1286
+ setupIdentity: `${signal.strategy}|${signal.symbol}|${signal.direction}|${signal.timestamp}`,
1287
+ setupIdentitySource: "signal-time-fallback"
1288
+ };
1289
+ };
1290
+
1291
+ // src/backtest/session.ts
1292
+ var createWarningCounts = () => ({
1293
+ [BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY]: 0
1294
+ });
1295
+ var recordSignalWarning = (warningCounts, signal) => {
1296
+ if (signal && typeof signal !== "string" && signal.orderStatus === "failed" && signal.orderFailureReason === BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
1297
+ const code = BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY;
1298
+ warningCounts[code] = (warningCounts[code] ?? 0) + 1;
1091
1299
  }
1092
- const key = `${symbol.toUpperCase()}:${startTime}:${endTime}`;
1093
- const cached = cache.get(key);
1094
- if (cached) return cached;
1095
- const pending = connector.getFundingRateHistory({ symbol, startTime, endTime }).catch(() => []);
1096
- cache.set(key, pending);
1097
- return pending;
1098
1300
  };
1099
- var resolveExecutionCosts = async (params) => {
1100
- const { connector, symbol, config, startTime, endTime, instrument } = params;
1101
- const cacheOnly = config.EXECUTION_COSTS_CACHE_ONLY === true;
1102
- const hasConfiguredFees = Number.isFinite(Number(config.MAKER_FEE_RATE)) && Number.isFinite(Number(config.TAKER_FEE_RATE));
1103
- const exchangeFees = !cacheOnly && !hasConfiguredFees && connector.getTradingFeeRate ? await loadTradingFee(connector, symbol).catch(() => null) : null;
1104
- const makerRate = hasConfiguredFees ? Number(config.MAKER_FEE_RATE) : exchangeFees?.makerRate ?? FEE_PERCENT2;
1105
- const takerRate = hasConfiguredFees ? Number(config.TAKER_FEE_RATE) : exchangeFees?.takerRate ?? FEE_PERCENT2;
1106
- const fundingEnabled = config.FUNDING_ENABLED !== false && !cacheOnly && typeof connector.getFundingRateHistory === "function";
1107
- const fundingRates = fundingEnabled ? await loadFundingRates(connector, symbol, startTime, endTime) : [];
1108
- const requestedLeverage = Math.max(1, finiteOr(config.LEVERAGE, 10));
1109
- const venueMaxLeverage = Number(instrument?.venueMetadata?.maxLeverage);
1110
- const maxAllowed = Number.isFinite(venueMaxLeverage) ? venueMaxLeverage : null;
1111
- const effectiveLeverage = maxAllowed == null ? requestedLeverage : Math.min(requestedLeverage, maxAllowed);
1112
- const feeSource = hasConfiguredFees ? "config" : exchangeFees?.source ?? "fallback";
1113
- const fundingSource = !fundingEnabled ? cacheOnly ? "fallback" : "disabled" : fundingRates.length ? "historical" : "unavailable";
1114
- const usesFallback = feeSource === "fallback" || fundingEnabled && fundingSource === "unavailable" || config.SLIPPAGE_BASE_BPS == null && config.SLIPPAGE_SPREAD_MULTIPLIER == null && config.SLIPPAGE_MARKET_IMPACT_BPS == null;
1115
- return {
1116
- model: {
1117
- fees: { makerRate, takerRate, source: feeSource },
1118
- funding: {
1119
- enabled: fundingEnabled,
1120
- source: fundingSource,
1121
- points: fundingRates.length,
1122
- fromTimestamp: fundingRates[0]?.timestamp ?? null,
1123
- toTimestamp: fundingRates.at(-1)?.timestamp ?? null
1124
- },
1125
- slippage: {
1126
- baseBps: finiteOr(config.SLIPPAGE_BASE_BPS, BACKTEST_BASE_SLIPPAGE_BPS),
1127
- spreadMultiplier: finiteOr(
1128
- config.SLIPPAGE_SPREAD_MULTIPLIER,
1129
- BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER
1130
- ),
1131
- marketImpactBps: finiteOr(
1132
- config.SLIPPAGE_MARKET_IMPACT_BPS,
1133
- BACKTEST_MARKET_IMPACT_BPS
1134
- ),
1135
- delayRiskMultiplier: finiteOr(
1136
- config.SLIPPAGE_DELAY_RISK_MULTIPLIER,
1137
- BACKTEST_DELAY_RISK_MULTIPLIER
1138
- ),
1139
- source: config.SLIPPAGE_BASE_BPS != null || config.SLIPPAGE_SPREAD_MULTIPLIER != null || config.SLIPPAGE_MARKET_IMPACT_BPS != null ? "config" : "fallback"
1140
- },
1141
- leverage: {
1142
- requested: requestedLeverage,
1143
- effective: effectiveLeverage,
1144
- maxAllowed
1145
- },
1146
- quality: usesFallback ? "fallback" : fundingEnabled && fundingRates.length ? "full" : "partial",
1147
- capturedAt: Date.now()
1148
- },
1149
- fundingRates
1150
- };
1151
- };
1152
-
1153
- // src/testing.ts
1154
- var isBacktestEntryDelayControlCode = (value) => typeof value === "string" && value.startsWith("BACKTEST_ENTRY_DELAY_");
1155
- var CLOSED_RESULT_FLUSH_INTERVAL = 500;
1156
- var DEFAULT_STRATEGY_CANDLE_TIMEOUT_MS = 6e4;
1157
- var createBacktestWarningCounts = () => ({
1158
- [BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY]: 0
1159
- });
1160
- var recordBacktestSignalWarning = (warningCounts, signal) => {
1161
- if (!signal || typeof signal === "string" || signal.orderStatus !== "failed" || signal.orderFailureReason !== BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
1162
- return;
1163
- }
1164
- const code = BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY;
1165
- warningCounts[code] = (warningCounts[code] ?? 0) + 1;
1166
- };
1167
- var resolvePositiveInt = (value, fallback) => {
1168
- const parsed = parseInt(String(value ?? ""), 10);
1169
- return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
1170
- };
1171
- var getStrategyCandleTimeoutMs = () => resolvePositiveInt(
1172
- process.env.BACKTEST_STRATEGY_CANDLE_TIMEOUT_MS,
1173
- DEFAULT_STRATEGY_CANDLE_TIMEOUT_MS
1174
- );
1175
- var getEffectiveTimeoutMs = (baseTimeoutMs, stageTimeoutMs) => {
1176
- const base = baseTimeoutMs && baseTimeoutMs > 0 ? Math.trunc(baseTimeoutMs) : null;
1177
- const stage = stageTimeoutMs && stageTimeoutMs > 0 ? Math.trunc(stageTimeoutMs) : null;
1178
- if (base == null) return stage;
1179
- if (stage == null) return base;
1180
- return Math.min(base, stage);
1181
- };
1182
- var buildCandleByTimestamp = (candles) => new Map(
1183
- (candles ?? []).filter((candle) => typeof candle?.timestamp === "number").map((candle) => [candle.timestamp, candle])
1184
- );
1185
- var buildBacktestDatasetMetadata = ({
1186
- backtestRunId,
1187
- backtestTestKey,
1188
- chunkId
1189
- }) => {
1190
- if (!backtestRunId || !backtestTestKey || !chunkId) {
1191
- return {};
1192
- }
1193
- return {
1194
- backtestRunId,
1195
- backtestTestKey,
1196
- backtestChunkId: chunkId
1197
- };
1198
- };
1199
- var cloneAiPayloadSignal = (signal) => {
1200
- const cloneValue = (value) => {
1201
- if (value == null) {
1202
- return value;
1203
- }
1204
- if (typeof structuredClone === "function") {
1205
- return structuredClone(value);
1206
- }
1207
- return JSON.parse(JSON.stringify(value));
1208
- };
1301
+ var buildDatasetMetadata = (test) => test.backtestRunId && test.backtestTestKey && test.chunkId ? {
1302
+ backtestRunId: test.backtestRunId,
1303
+ backtestTestKey: test.backtestTestKey,
1304
+ backtestChunkId: test.chunkId
1305
+ } : {};
1306
+ var cloneSignal = (signal) => {
1307
+ const cloneValue = (value) => {
1308
+ if (value == null) return value;
1309
+ if (typeof structuredClone === "function") return structuredClone(value);
1310
+ return JSON.parse(JSON.stringify(value));
1311
+ };
1209
1312
  return {
1210
1313
  ...signal,
1211
1314
  figures: cloneValue(signal.figures),
@@ -1213,21 +1316,18 @@ var cloneAiPayloadSignal = (signal) => {
1213
1316
  additionalIndicators: cloneValue(signal.additionalIndicators)
1214
1317
  };
1215
1318
  };
1216
- var buildReplaySignalEvaluationRecord = ({
1319
+ var buildReplayEvaluation = ({
1217
1320
  signal,
1218
- testId,
1219
- userName,
1220
- strategyName,
1221
- symbol,
1321
+ test,
1222
1322
  interval,
1223
1323
  candle
1224
1324
  }) => {
1225
1325
  if (!signal || typeof signal === "string") {
1226
1326
  return {
1227
- evaluationId: `${testId}:${strategyName}:${symbol}:${candle.timestamp}`,
1228
- userName,
1229
- strategy: strategyName,
1230
- symbol,
1327
+ evaluationId: `${test.testId}:${test.strategyName}:${test.symbol}:${candle.timestamp}`,
1328
+ userName: test.userName,
1329
+ strategy: test.strategyName,
1330
+ symbol: test.symbol,
1231
1331
  interval,
1232
1332
  timestamp: candle.timestamp,
1233
1333
  evaluatedAt: candle.timestamp,
@@ -1237,10 +1337,10 @@ var buildReplaySignalEvaluationRecord = ({
1237
1337
  }
1238
1338
  const signalTimestamp = typeof signal.timestamp === "number" && Number.isFinite(signal.timestamp) ? signal.timestamp : candle.timestamp;
1239
1339
  return {
1240
- evaluationId: `${signal.signalId || testId}:${strategyName}:${symbol}:${signalTimestamp}`,
1241
- userName,
1242
- strategy: signal.strategy || strategyName,
1243
- symbol: signal.symbol || symbol,
1340
+ evaluationId: `${signal.signalId || test.testId}:${test.strategyName}:${test.symbol}:${signalTimestamp}`,
1341
+ userName: test.userName,
1342
+ strategy: signal.strategy || test.strategyName,
1343
+ symbol: signal.symbol || test.symbol,
1244
1344
  interval: signal.interval || interval,
1245
1345
  timestamp: signalTimestamp,
1246
1346
  evaluatedAt: candle.timestamp,
@@ -1254,6 +1354,320 @@ var buildReplaySignalEvaluationRecord = ({
1254
1354
  ml: signal.ml
1255
1355
  };
1256
1356
  };
1357
+ var createBacktestSession = async ({
1358
+ test,
1359
+ connector,
1360
+ strategyCreator,
1361
+ preparedData,
1362
+ interval,
1363
+ sharedIndicatorsReplayKey,
1364
+ monitor
1365
+ }) => {
1366
+ const instrument = test.instrument;
1367
+ const start = test.options.start;
1368
+ if (!start) throw new Error("no start");
1369
+ const { model: executionCostModel, fundingRates } = await resolveExecutionCosts({
1370
+ connector,
1371
+ symbol: test.symbol,
1372
+ config: test.strategyConfig,
1373
+ startTime: start,
1374
+ endTime: test.options.end,
1375
+ instrument
1376
+ });
1377
+ const testConnector = createTestConnector(connector, {
1378
+ userName: test.userName,
1379
+ mlEnabled: test.ml,
1380
+ aiEnabled: Boolean(test.ai || test.researchTrace),
1381
+ fastMode: test.fast,
1382
+ instrument,
1383
+ executionCostModel,
1384
+ fundingRates
1385
+ });
1386
+ const strategy = await monitor.run(
1387
+ "strategy init",
1388
+ () => strategyCreator({
1389
+ userName: test.userName,
1390
+ connectorName: test.connectorName,
1391
+ universe: test.universe ?? "crypto",
1392
+ assetClass: test.assetClass ?? instrument?.assetClass,
1393
+ instrument,
1394
+ accountId: test.accountId,
1395
+ deploymentId: test.deploymentId,
1396
+ policyProfileId: test.policyProfileId,
1397
+ config: { ...test.strategyConfig, INTERVAL: test.interval ?? interval },
1398
+ symbol: test.symbol,
1399
+ data: preparedData.prevData.slice(),
1400
+ btcData: preparedData.btcPrevData.slice(),
1401
+ ethData: [...preparedData.ethPrevData, ...preparedData.ethTestData],
1402
+ btcBinanceData: preparedData.btcBinanceData,
1403
+ btcCoinbaseData: preparedData.btcCoinbaseData,
1404
+ backtestExecutionMarketData: {
1405
+ interval: preparedData.backtestExecutionInterval,
1406
+ data: preparedData.backtestExecutionData,
1407
+ btcData: preparedData.backtestExecutionBtcData,
1408
+ dataByTimestamp: preparedData.backtestExecutionDataByTimestamp,
1409
+ btcDataByTimestamp: preparedData.backtestExecutionBtcDataByTimestamp
1410
+ },
1411
+ connector: testConnector,
1412
+ sharedIndicatorsReplayKey
1413
+ })
1414
+ );
1415
+ const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
1416
+ const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
1417
+ const pendingResearchSetupBySignalId = /* @__PURE__ */ new Map();
1418
+ const researchSkipCounts = /* @__PURE__ */ new Map();
1419
+ const researchEventCounts = /* @__PURE__ */ new Map();
1420
+ const warningCounts = createWarningCounts();
1421
+ const replayEvaluations = test.collectReplaySignalEvaluations ? [] : null;
1422
+ const chunkId = test.chunkId ?? "single";
1423
+ const processSignal = async (signal, candle) => {
1424
+ recordSignalWarning(warningCounts, signal);
1425
+ if (test.researchTrace && typeof signal === "string") {
1426
+ const reason = signal.trim() || "NO_SIGNAL";
1427
+ researchSkipCounts.set(reason, (researchSkipCounts.get(reason) ?? 0) + 1);
1428
+ }
1429
+ if (typeof signal === "string" && signal.startsWith("BACKTEST_ENTRY_DELAY_")) {
1430
+ return;
1431
+ }
1432
+ if (replayEvaluations) {
1433
+ replayEvaluations.push(
1434
+ buildReplayEvaluation({ signal, test, interval, candle })
1435
+ );
1436
+ }
1437
+ if (signal && typeof signal !== "string" && signal.signalId && (test.ml || test.ai)) {
1438
+ await enrichSignalWithMarketContextStages({
1439
+ signal,
1440
+ env: "BACKTEST",
1441
+ coinMarketCapEnabled: true,
1442
+ onStageStart: monitor.contextStage
1443
+ });
1444
+ }
1445
+ if (test.researchTrace && signal && typeof signal !== "string" && signal.signalId) {
1446
+ const identity = resolveCoreResearchSetupIdentity(signal);
1447
+ pendingResearchSetupBySignalId.set(signal.signalId, identity);
1448
+ await appendCoreResearchTraceEvent({
1449
+ strategyName: test.strategyName,
1450
+ chunkId,
1451
+ event: {
1452
+ schema: "tradejs-core-research-trace/v1",
1453
+ event: signal.orderStatus === "failed" || signal.orderStatus === "skipped" ? "entry_rejected" : "signal_emitted",
1454
+ timestamp: signal.timestamp,
1455
+ strategy: signal.strategy || test.strategyName,
1456
+ symbol: signal.symbol || test.symbol,
1457
+ direction: signal.direction,
1458
+ signalId: signal.signalId,
1459
+ configId: test.configId,
1460
+ backtestRunId: test.backtestRunId,
1461
+ backtestTestKey: test.backtestTestKey,
1462
+ ...identity
1463
+ }
1464
+ });
1465
+ const traceEvent = signal.orderStatus === "failed" || signal.orderStatus === "skipped" ? "entry_rejected" : "signal_emitted";
1466
+ researchEventCounts.set(
1467
+ traceEvent,
1468
+ (researchEventCounts.get(traceEvent) ?? 0) + 1
1469
+ );
1470
+ }
1471
+ if (test.ml && signal && typeof signal !== "string" && signal.signalId) {
1472
+ pendingMlPayloadBySignalId.set(
1473
+ signal.signalId,
1474
+ buildMlPayload({
1475
+ signal,
1476
+ context: {
1477
+ userName: test.userName,
1478
+ testId: test.testId,
1479
+ testSuiteId: test.testSuiteId,
1480
+ testName: test.name,
1481
+ configId: test.configId,
1482
+ symbol: test.symbol,
1483
+ strategyName: test.strategyName,
1484
+ strategyConfig: test.strategyConfig,
1485
+ connectorName: test.connectorName
1486
+ }
1487
+ })
1488
+ );
1489
+ }
1490
+ if (test.ai && signal && typeof signal !== "string" && signal.signalId) {
1491
+ pendingAiRowBySignalId.set(signal.signalId, {
1492
+ signalId: signal.signalId,
1493
+ strategyName: signal.strategy || test.strategyName,
1494
+ symbol: signal.symbol || test.symbol,
1495
+ direction: signal.direction,
1496
+ timestamp: signal.timestamp,
1497
+ signal: cloneSignal(signal),
1498
+ testId: test.testId,
1499
+ testSuiteId: test.testSuiteId,
1500
+ testName: test.name,
1501
+ configId: test.configId,
1502
+ connectorName: test.connectorName,
1503
+ ...buildDatasetMetadata({ ...test, chunkId })
1504
+ });
1505
+ }
1506
+ };
1507
+ const flush = async () => {
1508
+ if (!test.ml && !test.ai && !test.researchTrace) return;
1509
+ const batch = await testConnector.drainMlResultsBatch();
1510
+ for (const resultRecord of batch) {
1511
+ const payload = pendingMlPayloadBySignalId.get(resultRecord.signalId);
1512
+ if (payload) {
1513
+ pendingMlPayloadBySignalId.delete(resultRecord.signalId);
1514
+ const fullRow = buildMlTrainingRow(payload, {
1515
+ profit: resultRecord.profit
1516
+ });
1517
+ await appendMlDatasetRow({
1518
+ strategyName: test.strategyName,
1519
+ chunkId,
1520
+ row: {
1521
+ ...trimMlTrainingRowWindows(fullRow, 5),
1522
+ ...buildDatasetMetadata({ ...test, chunkId })
1523
+ }
1524
+ });
1525
+ }
1526
+ const aiRowBase = pendingAiRowBySignalId.get(resultRecord.signalId);
1527
+ if (aiRowBase) {
1528
+ pendingAiRowBySignalId.delete(resultRecord.signalId);
1529
+ const { signal: aiSignal, ...rowBase } = aiRowBase;
1530
+ await appendAiDatasetRow({
1531
+ strategyName: test.strategyName,
1532
+ chunkId,
1533
+ row: {
1534
+ ...rowBase,
1535
+ payload: buildAiPayload(aiSignal),
1536
+ profit: resultRecord.profit,
1537
+ tradeResult: resultRecord.tradeResult,
1538
+ research: {
1539
+ schema: "tradejs-core-research-row/v1",
1540
+ ...resolveCoreResearchSetupIdentity(aiSignal)
1541
+ }
1542
+ }
1543
+ });
1544
+ }
1545
+ const researchSetup = pendingResearchSetupBySignalId.get(
1546
+ resultRecord.signalId
1547
+ );
1548
+ if (test.researchTrace && researchSetup && resultRecord.tradeResult) {
1549
+ pendingResearchSetupBySignalId.delete(resultRecord.signalId);
1550
+ await appendCoreResearchTraceEvent({
1551
+ strategyName: test.strategyName,
1552
+ chunkId,
1553
+ event: {
1554
+ schema: "tradejs-core-research-trace/v1",
1555
+ event: "entry_executed",
1556
+ timestamp: resultRecord.tradeResult.entryTimestamp,
1557
+ strategy: test.strategyName,
1558
+ symbol: test.symbol,
1559
+ direction: resultRecord.tradeResult.direction,
1560
+ signalId: resultRecord.signalId,
1561
+ configId: test.configId,
1562
+ backtestRunId: test.backtestRunId,
1563
+ backtestTestKey: test.backtestTestKey,
1564
+ ...researchSetup
1565
+ }
1566
+ });
1567
+ researchEventCounts.set(
1568
+ "entry_executed",
1569
+ (researchEventCounts.get("entry_executed") ?? 0) + 1
1570
+ );
1571
+ await appendCoreResearchTraceEvent({
1572
+ strategyName: test.strategyName,
1573
+ chunkId,
1574
+ event: {
1575
+ schema: "tradejs-core-research-trace/v1",
1576
+ event: "position_exited",
1577
+ timestamp: resultRecord.tradeResult.exitTimestamp,
1578
+ strategy: test.strategyName,
1579
+ symbol: test.symbol,
1580
+ direction: resultRecord.tradeResult.direction,
1581
+ signalId: resultRecord.signalId,
1582
+ configId: test.configId,
1583
+ backtestRunId: test.backtestRunId,
1584
+ backtestTestKey: test.backtestTestKey,
1585
+ netProfit: resultRecord.tradeResult.netProfit,
1586
+ exitReason: resultRecord.tradeResult.exitReason,
1587
+ ...researchSetup
1588
+ }
1589
+ });
1590
+ researchEventCounts.set(
1591
+ "position_exited",
1592
+ (researchEventCounts.get("position_exited") ?? 0) + 1
1593
+ );
1594
+ }
1595
+ }
1596
+ };
1597
+ return {
1598
+ detectorFanoutKey: strategy.detectorFanoutKey,
1599
+ detectorNoSignalSkipReason: strategy.detectorNoSignalSkipReason,
1600
+ next: async (candle, btcCandle, detectorSkipCode) => {
1601
+ const delayedSignal = await monitor.runStrategy(
1602
+ "delayed entry",
1603
+ () => strategy.__tradejsFlushBacktestDelayedEntry?.(candle, btcCandle) ?? Promise.resolve(void 0)
1604
+ );
1605
+ if (delayedSignal && typeof delayedSignal !== "string") {
1606
+ await processSignal(delayedSignal, candle);
1607
+ }
1608
+ await monitor.run("exit checks", () => testConnector.checkExits(candle));
1609
+ const signal = await monitor.runStrategy(
1610
+ detectorSkipCode ? "strategy detector skip" : "strategy signal",
1611
+ () => detectorSkipCode && strategy.canFastAdvanceDetectorNoSignal && strategy.advanceDetectorNoSignal ? strategy.advanceDetectorNoSignal(
1612
+ candle,
1613
+ btcCandle,
1614
+ detectorSkipCode
1615
+ ) : detectorSkipCode && strategy.skipDetectorNoSignal ? strategy.skipDetectorNoSignal(
1616
+ candle,
1617
+ btcCandle,
1618
+ detectorSkipCode
1619
+ ) : strategy(candle, btcCandle)
1620
+ );
1621
+ await processSignal(signal, candle);
1622
+ return signal;
1623
+ },
1624
+ flush: () => monitor.run("flush closed results", flush),
1625
+ result: async () => {
1626
+ await monitor.run("flush closed results", flush);
1627
+ if (test.researchTrace) {
1628
+ await appendCoreResearchTraceEvent({
1629
+ strategyName: test.strategyName,
1630
+ chunkId,
1631
+ event: {
1632
+ schema: "tradejs-core-research-trace/v1",
1633
+ event: "skip_summary",
1634
+ timestamp: test.options.end ?? test.options.start ?? 0,
1635
+ strategy: test.strategyName,
1636
+ symbol: test.symbol,
1637
+ configId: test.configId,
1638
+ backtestRunId: test.backtestRunId,
1639
+ backtestTestKey: test.backtestTestKey,
1640
+ skipCounts: Object.fromEntries(
1641
+ [...researchSkipCounts.entries()].sort(
1642
+ ([left], [right]) => left < right ? -1 : left > right ? 1 : 0
1643
+ )
1644
+ )
1645
+ }
1646
+ });
1647
+ }
1648
+ const result = await monitor.run(
1649
+ "collect result",
1650
+ () => testConnector.getResult()
1651
+ );
1652
+ const researchTraceSummary = test.researchTrace ? {
1653
+ events: Object.fromEntries(researchEventCounts),
1654
+ skipCounts: Object.fromEntries(researchSkipCounts)
1655
+ } : void 0;
1656
+ return replayEvaluations ? {
1657
+ ...result,
1658
+ warningCounts,
1659
+ inlineReplaySignalEvaluations: replayEvaluations,
1660
+ researchTraceSummary
1661
+ } : { ...result, warningCounts, researchTraceSummary };
1662
+ }
1663
+ };
1664
+ };
1665
+
1666
+ // src/testing.ts
1667
+ var CLOSED_RESULT_FLUSH_INTERVAL = 500;
1668
+ var buildCandleByTimestamp = (candles) => new Map(
1669
+ (candles ?? []).filter((candle) => typeof candle?.timestamp === "number").map((candle) => [candle.timestamp, candle])
1670
+ );
1257
1671
  var createTestingKlineCacheState = () => ({
1258
1672
  coinKlineCache: /* @__PURE__ */ new Map(),
1259
1673
  btcKlineCache: /* @__PURE__ */ new Map(),
@@ -1763,132 +2177,35 @@ var canRunTestsInSharedCandleLoop = (tests) => {
1763
2177
  (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)
1764
2178
  );
1765
2179
  };
1766
- var testing = async ({
1767
- userName,
1768
- symbol,
1769
- options: { start, end },
1770
- name,
1771
- testId,
1772
- testSuiteId,
1773
- configId,
1774
- strategyName,
1775
- strategyConfig,
1776
- connectorName,
1777
- universe = "crypto",
1778
- assetClass,
1779
- instrument: requestedInstrument,
1780
- accountId,
1781
- deploymentId,
1782
- policyProfileId,
1783
- interval = BACKTEST_INTERVAL,
1784
- ml = false,
1785
- ai = false,
1786
- fast = false,
1787
- collectReplaySignalEvaluations = false,
1788
- chunkId = "single",
1789
- backtestRunId,
1790
- backtestTestKey,
1791
- timeoutMs
1792
- }) => {
2180
+ var testing = async (test) => {
2181
+ const {
2182
+ userName,
2183
+ symbol,
2184
+ options: { start, end },
2185
+ name,
2186
+ strategyName,
2187
+ connectorName,
2188
+ universe = "crypto",
2189
+ accountId,
2190
+ deploymentId,
2191
+ interval = BACKTEST_INTERVAL,
2192
+ timeoutMs
2193
+ } = test;
1793
2194
  if (!start) {
1794
2195
  throw new Error("no start");
1795
2196
  }
1796
2197
  const preloadStart = getBacktestPreloadStart(start);
1797
- const startedAt = Date.now();
1798
- let activeStageStartedAt = startedAt;
1799
- let lastProgressSentAt = 0;
1800
- let lastProgressSignature = "";
1801
- let currentCandleIndex = 0;
1802
- let totalCandles = 0;
1803
- const strategyCandleTimeoutMs = getStrategyCandleTimeoutMs();
1804
- const formatTimeoutMessage = (stage, stageTimeoutMs) => `Test ${name} (${symbol}) timed out after ${stageTimeoutMs}ms during ${stage}`;
1805
- const emitProgress = (stage, options = {}) => {
1806
- const now = Date.now();
1807
- const candleIndex = typeof options.candleIndex === "number" ? options.candleIndex : currentCandleIndex;
1808
- const candleTotal = typeof options.candleTotal === "number" ? options.candleTotal : totalCandles;
1809
- const signature = [
1810
- stage,
1811
- candleIndex,
1812
- candleTotal,
1813
- Math.floor((now - activeStageStartedAt) / 5e3)
1814
- ].join(":");
1815
- if (!options.force) {
1816
- if (signature === lastProgressSignature) {
1817
- return;
1818
- }
1819
- if (now - lastProgressSentAt < 4e3) {
1820
- return;
1821
- }
1822
- }
1823
- lastProgressSentAt = now;
1824
- lastProgressSignature = signature;
1825
- process.send?.({
1826
- progress: true,
1827
- testName: name,
1828
- symbol,
1829
- strategyName,
1830
- stage,
1831
- candleIndex,
1832
- candleTotal,
1833
- elapsedMs: now - startedAt,
1834
- stageElapsedMs: now - activeStageStartedAt
1835
- });
1836
- };
1837
- const getStageTimeoutMs = () => {
1838
- if (!timeoutMs || timeoutMs <= 0) {
1839
- return null;
1840
- }
1841
- return timeoutMs;
1842
- };
1843
- const throwIfTimedOut = (stage) => {
1844
- if (getStageTimeoutMs() == null) {
1845
- return;
1846
- }
1847
- emitProgress(stage);
1848
- };
1849
- const withTimeout = async (stage, promise, stageTimeoutOverrideMs = null) => {
1850
- const stageTimeoutMs = getEffectiveTimeoutMs(
1851
- getStageTimeoutMs() ?? void 0,
1852
- stageTimeoutOverrideMs
1853
- );
1854
- if (stageTimeoutMs == null) {
1855
- return promise;
1856
- }
1857
- activeStageStartedAt = Date.now();
1858
- emitProgress(stage, { force: true });
1859
- return await new Promise((resolve, reject) => {
1860
- const heartbeat = setInterval(() => {
1861
- emitProgress(stage);
1862
- }, 5e3);
1863
- const timer = setTimeout(() => {
1864
- clearInterval(heartbeat);
1865
- reject(new Error(formatTimeoutMessage(stage, stageTimeoutMs)));
1866
- }, stageTimeoutMs);
1867
- promise.then(
1868
- (value) => {
1869
- clearInterval(heartbeat);
1870
- clearTimeout(timer);
1871
- resolve(value);
1872
- },
1873
- (error) => {
1874
- clearInterval(heartbeat);
1875
- clearTimeout(timer);
1876
- reject(error);
1877
- }
1878
- );
1879
- });
1880
- };
1881
- const runStage = (stage, fn) => {
1882
- if (getStageTimeoutMs() == null) {
1883
- return fn();
1884
- }
1885
- return withTimeout(stage, fn());
1886
- };
1887
- const runStrategyCandleStage = (stage, fn) => withTimeout(stage, fn(), strategyCandleTimeoutMs);
2198
+ const progress = createBacktestProgress({
2199
+ testName: name,
2200
+ symbol,
2201
+ strategyName,
2202
+ timeoutMs,
2203
+ timeoutSubject: `Test ${name} (${symbol})`
2204
+ });
1888
2205
  const { projectRoot, state } = getTestingKlineCacheState();
1889
- const connector = await withTimeout(
2206
+ const connector = await progress.run(
1890
2207
  "connector init",
1891
- getCachedConnector({
2208
+ () => getCachedConnector({
1892
2209
  state,
1893
2210
  projectRoot,
1894
2211
  userName,
@@ -1901,16 +2218,16 @@ var testing = async ({
1901
2218
  if (!connector) {
1902
2219
  throw new Error(`Unknown connector: ${connectorName}`);
1903
2220
  }
1904
- const strategyCreator = await withTimeout(
2221
+ const strategyCreator = await progress.run(
1905
2222
  "strategy lookup",
1906
- getStrategyCreator(strategyName, projectRoot)
2223
+ () => getStrategyCreator(strategyName, projectRoot)
1907
2224
  );
1908
2225
  if (!strategyCreator) {
1909
2226
  throw new Error(`Unknown strategy: ${strategyName}`);
1910
2227
  }
1911
- const preparedData = await withTimeout(
2228
+ const preparedData = await progress.run(
1912
2229
  "kline preload",
1913
- prepareTestingData({
2230
+ () => prepareTestingData({
1914
2231
  state,
1915
2232
  projectRoot,
1916
2233
  userName,
@@ -1928,225 +2245,26 @@ var testing = async ({
1928
2245
  if (!preparedData) {
1929
2246
  throw new Error("Prepared backtest data not available");
1930
2247
  }
1931
- const {
1932
- prevData,
1933
- btcPrevData,
1934
- ethPrevData,
1935
- ethTestData,
1936
- testData,
1937
- btcTestData,
1938
- btcBinanceData,
1939
- btcCoinbaseData,
1940
- backtestExecutionInterval,
1941
- backtestExecutionData,
1942
- backtestExecutionBtcData,
1943
- backtestExecutionDataByTimestamp,
1944
- backtestExecutionBtcDataByTimestamp
1945
- } = preparedData;
1946
- const runtimePrevData = prevData.slice();
1947
- const runtimeBtcPrevData = btcPrevData.slice();
1948
- const runtimeEthData = [...ethPrevData, ...ethTestData];
1949
- totalCandles = testData.length;
1950
- const instrument = requestedInstrument;
1951
- const { model: executionCostModel, fundingRates } = await resolveExecutionCosts({
2248
+ const { testData, btcTestData } = preparedData;
2249
+ const session = await createBacktestSession({
2250
+ test,
1952
2251
  connector,
1953
- symbol,
1954
- config: strategyConfig,
1955
- startTime: start,
1956
- endTime: end,
1957
- instrument
1958
- });
1959
- const testConnector = createTestConnector(connector, {
1960
- userName,
1961
- mlEnabled: ml,
1962
- aiEnabled: ai,
1963
- fastMode: fast,
1964
- instrument,
1965
- executionCostModel,
1966
- fundingRates
2252
+ strategyCreator,
2253
+ preparedData,
2254
+ interval,
2255
+ monitor: progress
1967
2256
  });
1968
- const strategy = await withTimeout(
1969
- "strategy init",
1970
- strategyCreator({
1971
- userName,
1972
- connectorName,
1973
- universe,
1974
- assetClass: assetClass ?? instrument?.assetClass,
1975
- instrument,
1976
- accountId,
1977
- deploymentId,
1978
- policyProfileId,
1979
- config: {
1980
- ...strategyConfig,
1981
- INTERVAL: interval
1982
- },
1983
- symbol,
1984
- data: runtimePrevData,
1985
- btcData: runtimeBtcPrevData,
1986
- ethData: runtimeEthData,
1987
- btcBinanceData,
1988
- btcCoinbaseData,
1989
- backtestExecutionMarketData: {
1990
- interval: backtestExecutionInterval,
1991
- data: backtestExecutionData,
1992
- btcData: backtestExecutionBtcData,
1993
- dataByTimestamp: backtestExecutionDataByTimestamp,
1994
- btcDataByTimestamp: backtestExecutionBtcDataByTimestamp
1995
- },
1996
- connector: testConnector
1997
- })
1998
- );
1999
- const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
2000
- const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
2001
- const warningCounts = createBacktestWarningCounts();
2002
- const replaySignalEvaluations = collectReplaySignalEvaluations ? [] : null;
2003
- const flushClosedResultsBatch = async () => {
2004
- if (!ml && !ai) return;
2005
- const batch = await testConnector.drainMlResultsBatch();
2006
- if (!batch.length) return;
2007
- for (const resultRecord of batch) {
2008
- const payload = pendingMlPayloadBySignalId.get(resultRecord.signalId);
2009
- if (payload) {
2010
- pendingMlPayloadBySignalId.delete(resultRecord.signalId);
2011
- const fullRow = buildMlTrainingRow(payload, {
2012
- profit: resultRecord.profit
2013
- });
2014
- const row = {
2015
- ...trimMlTrainingRowWindows(fullRow, 5),
2016
- ...buildBacktestDatasetMetadata({
2017
- backtestRunId,
2018
- backtestTestKey,
2019
- chunkId
2020
- })
2021
- };
2022
- await appendMlDatasetRow({
2023
- strategyName,
2024
- chunkId,
2025
- row
2026
- });
2027
- }
2028
- const aiRowBase = pendingAiRowBySignalId.get(resultRecord.signalId);
2029
- if (aiRowBase) {
2030
- pendingAiRowBySignalId.delete(resultRecord.signalId);
2031
- const { signal: aiSignal, ...rowBase } = aiRowBase;
2032
- await appendAiDatasetRow({
2033
- strategyName,
2034
- chunkId,
2035
- row: {
2036
- ...rowBase,
2037
- payload: buildAiPayload(aiSignal),
2038
- profit: resultRecord.profit,
2039
- tradeResult: resultRecord.tradeResult
2040
- }
2041
- });
2042
- }
2043
- }
2044
- };
2045
- const processSignal = async (signal, candle) => {
2046
- recordBacktestSignalWarning(warningCounts, signal);
2047
- if (isBacktestEntryDelayControlCode(signal)) {
2048
- return;
2049
- }
2050
- if (replaySignalEvaluations) {
2051
- replaySignalEvaluations.push(
2052
- buildReplaySignalEvaluationRecord({
2053
- signal,
2054
- testId,
2055
- userName,
2056
- strategyName,
2057
- symbol,
2058
- interval,
2059
- candle
2060
- })
2061
- );
2062
- }
2063
- const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (ml || ai);
2064
- if (shouldCapturePayload) {
2065
- await enrichSignalWithMarketContextStages({
2066
- signal,
2067
- env: "BACKTEST",
2068
- coinMarketCapEnabled: true,
2069
- onStageStart: (stage) => {
2070
- activeStageStartedAt = Date.now();
2071
- emitProgress(`${stage} context`, { force: true });
2072
- }
2073
- });
2074
- }
2075
- if (ml && signal && typeof signal !== "string" && signal.signalId) {
2076
- const payload = buildMlPayload({
2077
- signal,
2078
- context: {
2079
- userName,
2080
- testId,
2081
- testSuiteId,
2082
- testName: name,
2083
- configId,
2084
- symbol,
2085
- strategyName,
2086
- strategyConfig,
2087
- connectorName
2088
- }
2089
- });
2090
- pendingMlPayloadBySignalId.set(signal.signalId, payload);
2091
- }
2092
- if (ai && signal && typeof signal !== "string" && signal.signalId) {
2093
- pendingAiRowBySignalId.set(signal.signalId, {
2094
- signalId: signal.signalId,
2095
- strategyName: signal.strategy || strategyName,
2096
- symbol: signal.symbol || symbol,
2097
- direction: signal.direction,
2098
- timestamp: signal.timestamp,
2099
- signal: cloneAiPayloadSignal(signal),
2100
- testId,
2101
- testSuiteId,
2102
- testName: name,
2103
- configId,
2104
- connectorName,
2105
- ...buildBacktestDatasetMetadata({
2106
- backtestRunId,
2107
- backtestTestKey,
2108
- chunkId
2109
- })
2110
- });
2111
- }
2112
- };
2113
2257
  for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
2114
2258
  if (candleIndex % 25 === 0) {
2115
- throwIfTimedOut("candle loop");
2259
+ progress.checkpoint("candle loop");
2116
2260
  }
2117
- currentCandleIndex = candleIndex + 1;
2118
- emitProgress("candle loop", {
2119
- force: candleIndex === 0 || currentCandleIndex === totalCandles
2120
- });
2121
- const candle = testData[candleIndex];
2122
- const btcCandle = btcTestData[candleIndex];
2123
- const delayedSignal = await runStrategyCandleStage(
2124
- "delayed entry",
2125
- async () => strategy.__tradejsFlushBacktestDelayedEntry?.(candle, btcCandle)
2126
- );
2127
- if (delayedSignal && typeof delayedSignal !== "string") {
2128
- await processSignal(delayedSignal, candle);
2129
- }
2130
- await runStage("exit checks", () => testConnector.checkExits(candle));
2131
- const signal = await runStrategyCandleStage(
2132
- "strategy signal",
2133
- () => strategy(candle, btcCandle)
2134
- );
2135
- await processSignal(signal, candle);
2261
+ progress.setCandle(candleIndex + 1, testData.length);
2262
+ await session.next(testData[candleIndex], btcTestData[candleIndex]);
2136
2263
  if ((candleIndex + 1) % CLOSED_RESULT_FLUSH_INTERVAL === 0) {
2137
- await withTimeout("flush closed results", flushClosedResultsBatch());
2264
+ await session.flush();
2138
2265
  }
2139
2266
  }
2140
- await withTimeout("flush closed results", flushClosedResultsBatch());
2141
- const result = await withTimeout("collect result", testConnector.getResult());
2142
- const resultWithWarnings = {
2143
- ...result,
2144
- warningCounts
2145
- };
2146
- return replaySignalEvaluations ? {
2147
- ...resultWithWarnings,
2148
- inlineReplaySignalEvaluations: replaySignalEvaluations
2149
- } : resultWithWarnings;
2267
+ return session.result();
2150
2268
  };
2151
2269
  var testingGroupInSharedCandleLoop = async (tests) => {
2152
2270
  if (!canRunTestsInSharedCandleLoop(tests)) {
@@ -2170,10 +2288,6 @@ var testingGroupInSharedCandleLoop = async (tests) => {
2170
2288
  accountId,
2171
2289
  deploymentId,
2172
2290
  interval = BACKTEST_INTERVAL,
2173
- ml = false,
2174
- ai = false,
2175
- fast = false,
2176
- collectReplaySignalEvaluations = false,
2177
2291
  chunkId = "single",
2178
2292
  timeoutMs
2179
2293
  } = first;
@@ -2181,101 +2295,17 @@ var testingGroupInSharedCandleLoop = async (tests) => {
2181
2295
  throw new Error("no start");
2182
2296
  }
2183
2297
  const preloadStart = getBacktestPreloadStart(start);
2184
- const startedAt = Date.now();
2185
- let activeStageStartedAt = startedAt;
2186
- let lastProgressSentAt = 0;
2187
- let lastProgressSignature = "";
2188
- let currentCandleIndex = 0;
2189
- let totalCandles = 0;
2190
- const strategyCandleTimeoutMs = getStrategyCandleTimeoutMs();
2191
- const formatTimeoutMessage = (stage, stageTimeoutMs) => `Test group ${strategyName}/${symbol} timed out after ${stageTimeoutMs}ms during ${stage}`;
2192
- const emitProgress = (stage, options = {}) => {
2193
- const now = Date.now();
2194
- const candleIndex = typeof options.candleIndex === "number" ? options.candleIndex : currentCandleIndex;
2195
- const candleTotal = typeof options.candleTotal === "number" ? options.candleTotal : totalCandles;
2196
- const signature = [
2197
- stage,
2198
- candleIndex,
2199
- candleTotal,
2200
- Math.floor((now - activeStageStartedAt) / 5e3)
2201
- ].join(":");
2202
- if (!options.force) {
2203
- if (signature === lastProgressSignature) {
2204
- return;
2205
- }
2206
- if (now - lastProgressSentAt < 4e3) {
2207
- return;
2208
- }
2209
- }
2210
- lastProgressSentAt = now;
2211
- lastProgressSignature = signature;
2212
- process.send?.({
2213
- progress: true,
2214
- testName: first.name,
2215
- symbol,
2216
- strategyName,
2217
- stage,
2218
- candleIndex,
2219
- candleTotal,
2220
- elapsedMs: now - startedAt,
2221
- stageElapsedMs: now - activeStageStartedAt
2222
- });
2223
- };
2224
- const getStageTimeoutMs = () => {
2225
- if (!timeoutMs || timeoutMs <= 0) {
2226
- return null;
2227
- }
2228
- return timeoutMs;
2229
- };
2230
- const throwIfTimedOut = (stage) => {
2231
- if (getStageTimeoutMs() == null) {
2232
- return;
2233
- }
2234
- emitProgress(stage);
2235
- };
2236
- const withTimeout = async (stage, promise, stageTimeoutOverrideMs = null) => {
2237
- const stageTimeoutMs = getEffectiveTimeoutMs(
2238
- getStageTimeoutMs() ?? void 0,
2239
- stageTimeoutOverrideMs
2240
- );
2241
- if (stageTimeoutMs == null) {
2242
- return promise;
2243
- }
2244
- activeStageStartedAt = Date.now();
2245
- emitProgress(stage, { force: true });
2246
- return await new Promise((resolve, reject) => {
2247
- const heartbeat = setInterval(() => {
2248
- emitProgress(stage);
2249
- }, 5e3);
2250
- const timer = setTimeout(() => {
2251
- clearInterval(heartbeat);
2252
- reject(new Error(formatTimeoutMessage(stage, stageTimeoutMs)));
2253
- }, stageTimeoutMs);
2254
- promise.then(
2255
- (value) => {
2256
- clearInterval(heartbeat);
2257
- clearTimeout(timer);
2258
- resolve(value);
2259
- },
2260
- (error) => {
2261
- clearInterval(heartbeat);
2262
- clearTimeout(timer);
2263
- reject(error);
2264
- }
2265
- );
2266
- });
2267
- };
2268
- const runStage = (stage, fn) => {
2269
- if (getStageTimeoutMs() == null) {
2270
- return fn();
2271
- }
2272
- return withTimeout(stage, fn());
2273
- };
2274
- const runStrategyCandleStage = (stage, fn) => withTimeout(stage, fn(), strategyCandleTimeoutMs);
2298
+ const progress = createBacktestProgress({
2299
+ testName: first.name,
2300
+ symbol,
2301
+ strategyName,
2302
+ timeoutMs,
2303
+ timeoutSubject: `Test group ${strategyName}/${symbol}`
2304
+ });
2275
2305
  const { projectRoot, state } = getTestingKlineCacheState();
2276
- const connector = await withTimeout(
2306
+ const connector = await progress.run(
2277
2307
  "connector init",
2278
- getCachedConnector({
2308
+ () => getCachedConnector({
2279
2309
  state,
2280
2310
  projectRoot,
2281
2311
  userName,
@@ -2288,16 +2318,16 @@ var testingGroupInSharedCandleLoop = async (tests) => {
2288
2318
  if (!connector) {
2289
2319
  throw new Error(`Unknown connector: ${connectorName}`);
2290
2320
  }
2291
- const strategyCreator = await withTimeout(
2321
+ const strategyCreator = await progress.run(
2292
2322
  "strategy lookup",
2293
- getStrategyCreator(strategyName, projectRoot)
2323
+ () => getStrategyCreator(strategyName, projectRoot)
2294
2324
  );
2295
2325
  if (!strategyCreator) {
2296
2326
  throw new Error(`Unknown strategy: ${strategyName}`);
2297
2327
  }
2298
- const preparedData = await withTimeout(
2328
+ const preparedData = await progress.run(
2299
2329
  "kline preload",
2300
- prepareTestingData({
2330
+ () => prepareTestingData({
2301
2331
  state,
2302
2332
  projectRoot,
2303
2333
  userName,
@@ -2315,22 +2345,7 @@ var testingGroupInSharedCandleLoop = async (tests) => {
2315
2345
  if (!preparedData) {
2316
2346
  throw new Error("Prepared backtest data not available");
2317
2347
  }
2318
- const {
2319
- prevData,
2320
- btcPrevData,
2321
- ethPrevData,
2322
- ethTestData,
2323
- testData,
2324
- btcTestData,
2325
- btcBinanceData,
2326
- btcCoinbaseData,
2327
- backtestExecutionInterval,
2328
- backtestExecutionData,
2329
- backtestExecutionBtcData,
2330
- backtestExecutionDataByTimestamp,
2331
- backtestExecutionBtcDataByTimestamp
2332
- } = preparedData;
2333
- totalCandles = testData.length;
2348
+ const { testData, btcTestData } = preparedData;
2334
2349
  const sharedIndicatorsReplayKey = [
2335
2350
  "shared",
2336
2351
  userName,
@@ -2345,250 +2360,45 @@ var testingGroupInSharedCandleLoop = async (tests) => {
2345
2360
  const runners = [];
2346
2361
  try {
2347
2362
  for (const test of tests) {
2348
- const instrument = test.instrument;
2349
- const { model: executionCostModel, fundingRates } = await resolveExecutionCosts({
2350
- connector,
2351
- symbol: test.symbol,
2352
- config: test.strategyConfig,
2353
- startTime: start,
2354
- endTime: end,
2355
- instrument
2356
- });
2357
- const testConnector = createTestConnector(connector, {
2358
- userName: test.userName,
2359
- mlEnabled: test.ml,
2360
- aiEnabled: test.ai,
2361
- fastMode: test.fast,
2362
- instrument,
2363
- executionCostModel,
2364
- fundingRates
2365
- });
2366
- const strategy = await withTimeout(
2367
- "strategy init",
2368
- strategyCreator({
2369
- userName: test.userName,
2370
- connectorName: test.connectorName,
2371
- universe: test.universe ?? universe,
2372
- assetClass: test.assetClass ?? instrument?.assetClass,
2373
- instrument,
2374
- accountId: test.accountId ?? accountId,
2375
- deploymentId: test.deploymentId ?? deploymentId,
2376
- policyProfileId: test.policyProfileId,
2377
- config: {
2378
- ...test.strategyConfig,
2379
- INTERVAL: test.interval ?? interval
2380
- },
2381
- symbol: test.symbol,
2382
- data: prevData.slice(),
2383
- btcData: btcPrevData.slice(),
2384
- ethData: [...ethPrevData, ...ethTestData],
2385
- btcBinanceData,
2386
- btcCoinbaseData,
2387
- backtestExecutionMarketData: {
2388
- interval: backtestExecutionInterval,
2389
- data: backtestExecutionData,
2390
- btcData: backtestExecutionBtcData,
2391
- dataByTimestamp: backtestExecutionDataByTimestamp,
2392
- btcDataByTimestamp: backtestExecutionBtcDataByTimestamp
2393
- },
2394
- connector: testConnector,
2395
- sharedIndicatorsReplayKey
2396
- })
2397
- );
2398
2363
  runners.push({
2399
2364
  test,
2400
- strategy,
2401
- testConnector,
2402
- pendingMlPayloadBySignalId: /* @__PURE__ */ new Map(),
2403
- pendingAiRowBySignalId: /* @__PURE__ */ new Map(),
2404
- warningCounts: createBacktestWarningCounts(),
2405
- replaySignalEvaluations: collectReplaySignalEvaluations ? [] : null
2365
+ session: await createBacktestSession({
2366
+ test,
2367
+ connector,
2368
+ strategyCreator,
2369
+ preparedData,
2370
+ interval,
2371
+ sharedIndicatorsReplayKey,
2372
+ monitor: progress
2373
+ })
2406
2374
  });
2407
2375
  }
2408
- const flushClosedResultsBatch = async (runner) => {
2409
- if (!runner.test.ml && !runner.test.ai) return;
2410
- const batch = await runner.testConnector.drainMlResultsBatch();
2411
- if (!batch.length) return;
2412
- for (const resultRecord of batch) {
2413
- const payload = runner.pendingMlPayloadBySignalId.get(
2414
- resultRecord.signalId
2415
- );
2416
- if (payload) {
2417
- runner.pendingMlPayloadBySignalId.delete(resultRecord.signalId);
2418
- const fullRow = buildMlTrainingRow(payload, {
2419
- profit: resultRecord.profit
2420
- });
2421
- const resolvedChunkId = runner.test.chunkId ?? "single";
2422
- const row = {
2423
- ...trimMlTrainingRowWindows(fullRow, 5),
2424
- ...buildBacktestDatasetMetadata({
2425
- backtestRunId: runner.test.backtestRunId,
2426
- backtestTestKey: runner.test.backtestTestKey,
2427
- chunkId: resolvedChunkId
2428
- })
2429
- };
2430
- await appendMlDatasetRow({
2431
- strategyName: runner.test.strategyName,
2432
- chunkId: resolvedChunkId,
2433
- row
2434
- });
2435
- }
2436
- const aiRowBase = runner.pendingAiRowBySignalId.get(
2437
- resultRecord.signalId
2438
- );
2439
- if (aiRowBase) {
2440
- runner.pendingAiRowBySignalId.delete(resultRecord.signalId);
2441
- const { signal: aiSignal, ...rowBase } = aiRowBase;
2442
- const resolvedChunkId = runner.test.chunkId ?? "single";
2443
- await appendAiDatasetRow({
2444
- strategyName: runner.test.strategyName,
2445
- chunkId: resolvedChunkId,
2446
- row: {
2447
- ...rowBase,
2448
- payload: buildAiPayload(aiSignal),
2449
- profit: resultRecord.profit,
2450
- tradeResult: resultRecord.tradeResult
2451
- }
2452
- });
2453
- }
2454
- }
2455
- };
2456
- const processRunnerSignal = async (runner, signal, candle) => {
2457
- recordBacktestSignalWarning(runner.warningCounts, signal);
2458
- if (isBacktestEntryDelayControlCode(signal)) {
2459
- return;
2460
- }
2461
- const { test } = runner;
2462
- if (runner.replaySignalEvaluations) {
2463
- runner.replaySignalEvaluations.push(
2464
- buildReplaySignalEvaluationRecord({
2465
- signal,
2466
- testId: test.testId,
2467
- userName: test.userName,
2468
- strategyName: test.strategyName,
2469
- symbol: test.symbol,
2470
- interval: test.interval ?? interval,
2471
- candle
2472
- })
2473
- );
2474
- }
2475
- const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (test.ml || test.ai);
2476
- if (shouldCapturePayload) {
2477
- await enrichSignalWithMarketContextStages({
2478
- signal,
2479
- env: "BACKTEST",
2480
- coinMarketCapEnabled: true,
2481
- onStageStart: (stage) => {
2482
- activeStageStartedAt = Date.now();
2483
- emitProgress(`${stage} context`, { force: true });
2484
- }
2485
- });
2486
- }
2487
- if (test.ml && signal && typeof signal !== "string" && signal.signalId) {
2488
- const payload = buildMlPayload({
2489
- signal,
2490
- context: {
2491
- userName: test.userName,
2492
- testId: test.testId,
2493
- testSuiteId: test.testSuiteId,
2494
- testName: test.name,
2495
- configId: test.configId,
2496
- symbol: test.symbol,
2497
- strategyName: test.strategyName,
2498
- strategyConfig: test.strategyConfig,
2499
- connectorName: test.connectorName
2500
- }
2501
- });
2502
- runner.pendingMlPayloadBySignalId.set(signal.signalId, payload);
2503
- }
2504
- if (test.ai && signal && typeof signal !== "string" && signal.signalId) {
2505
- runner.pendingAiRowBySignalId.set(signal.signalId, {
2506
- signalId: signal.signalId,
2507
- strategyName: signal.strategy || test.strategyName,
2508
- symbol: signal.symbol || test.symbol,
2509
- direction: signal.direction,
2510
- timestamp: signal.timestamp,
2511
- signal: cloneAiPayloadSignal(signal),
2512
- testId: test.testId,
2513
- testSuiteId: test.testSuiteId,
2514
- testName: test.name,
2515
- configId: test.configId,
2516
- connectorName: test.connectorName,
2517
- ...buildBacktestDatasetMetadata({
2518
- backtestRunId: test.backtestRunId,
2519
- backtestTestKey: test.backtestTestKey,
2520
- chunkId: test.chunkId ?? "single"
2521
- })
2522
- });
2523
- }
2524
- };
2525
2376
  for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
2526
2377
  if (candleIndex % 25 === 0) {
2527
- throwIfTimedOut("candle loop");
2378
+ progress.checkpoint("candle loop");
2528
2379
  }
2529
- currentCandleIndex = candleIndex + 1;
2530
- emitProgress("candle loop", {
2531
- force: candleIndex === 0 || currentCandleIndex === totalCandles
2532
- });
2380
+ progress.setCandle(candleIndex + 1, testData.length);
2533
2381
  const candle = testData[candleIndex];
2534
2382
  const btcCandle = btcTestData[candleIndex];
2535
2383
  const detectorNoSignalByKey = /* @__PURE__ */ new Map();
2536
2384
  for (const runner of runners) {
2537
- const { test, testConnector, strategy } = runner;
2538
- const delayedSignal = await runStrategyCandleStage(
2539
- "delayed entry",
2540
- async () => strategy.__tradejsFlushBacktestDelayedEntry?.(candle, btcCandle)
2541
- );
2542
- if (delayedSignal && typeof delayedSignal !== "string") {
2543
- await processRunnerSignal(runner, delayedSignal, candle);
2544
- }
2545
- await runStage("exit checks", () => testConnector.checkExits(candle));
2546
- const detectorFanoutKey = strategy.detectorFanoutKey;
2385
+ const { session } = runner;
2386
+ const detectorFanoutKey = session.detectorFanoutKey;
2547
2387
  const detectorSkipCode = detectorFanoutKey ? detectorNoSignalByKey.get(detectorFanoutKey) : void 0;
2548
- const signal = await runStrategyCandleStage(
2549
- detectorSkipCode ? "strategy detector skip" : "strategy signal",
2550
- () => detectorSkipCode && strategy.canFastAdvanceDetectorNoSignal && strategy.advanceDetectorNoSignal ? strategy.advanceDetectorNoSignal(
2551
- candle,
2552
- btcCandle,
2553
- detectorSkipCode
2554
- ) : detectorSkipCode && strategy.skipDetectorNoSignal ? strategy.skipDetectorNoSignal(
2555
- candle,
2556
- btcCandle,
2557
- detectorSkipCode
2558
- ) : strategy(candle, btcCandle)
2559
- );
2560
- if (detectorFanoutKey && strategy.detectorNoSignalSkipReason && typeof signal === "string" && signal === strategy.detectorNoSignalSkipReason) {
2388
+ const signal = await session.next(candle, btcCandle, detectorSkipCode);
2389
+ if (detectorFanoutKey && session.detectorNoSignalSkipReason && typeof signal === "string" && signal === session.detectorNoSignalSkipReason) {
2561
2390
  detectorNoSignalByKey.set(detectorFanoutKey, signal);
2562
2391
  }
2563
- await processRunnerSignal(runner, signal, candle);
2564
2392
  }
2565
2393
  if ((candleIndex + 1) % CLOSED_RESULT_FLUSH_INTERVAL === 0) {
2566
- await withTimeout(
2567
- "flush closed results",
2568
- Promise.all(runners.map((runner) => flushClosedResultsBatch(runner)))
2569
- );
2394
+ await Promise.all(runners.map(({ session }) => session.flush()));
2570
2395
  }
2571
2396
  }
2572
2397
  const results = [];
2573
2398
  for (const runner of runners) {
2574
- await withTimeout(
2575
- "flush closed results",
2576
- flushClosedResultsBatch(runner)
2577
- );
2578
- const result = await withTimeout(
2579
- "collect result",
2580
- runner.testConnector.getResult()
2581
- );
2582
2399
  results.push({
2583
2400
  test: runner.test,
2584
- result: runner.replaySignalEvaluations ? {
2585
- ...result,
2586
- warningCounts: runner.warningCounts,
2587
- inlineReplaySignalEvaluations: runner.replaySignalEvaluations
2588
- } : {
2589
- ...result,
2590
- warningCounts: runner.warningCounts
2591
- }
2401
+ result: await runner.session.result()
2592
2402
  });
2593
2403
  }
2594
2404
  return results;