@tradejs/node 2.0.18 → 2.0.19

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,226 @@ 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";
38
145
  import {
39
146
  appendMlDatasetRow,
40
147
  buildMlTrainingRow,
41
148
  trimMlTrainingRowWindows
42
149
  } from "@tradejs/infra/ml";
43
- import { logger } from "@tradejs/infra/logger";
150
+
151
+ // src/executionCosts.ts
152
+ import {
153
+ BACKTEST_BASE_SLIPPAGE_BPS,
154
+ BACKTEST_DELAY_RISK_MULTIPLIER,
155
+ BACKTEST_MARKET_IMPACT_BPS,
156
+ BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER,
157
+ FEE_PERCENT
158
+ } from "@tradejs/core/constants";
159
+ var finiteOr = (value, fallback) => {
160
+ const parsed = Number(value);
161
+ return Number.isFinite(parsed) ? parsed : fallback;
162
+ };
163
+ var feeCache = /* @__PURE__ */ new WeakMap();
164
+ var fundingCache = /* @__PURE__ */ new WeakMap();
165
+ var loadTradingFee = async (connector, symbol) => {
166
+ if (!connector.getTradingFeeRate) return null;
167
+ let cache = feeCache.get(connector);
168
+ if (!cache) {
169
+ cache = /* @__PURE__ */ new Map();
170
+ feeCache.set(connector, cache);
171
+ }
172
+ const key = symbol.toUpperCase();
173
+ const cached = cache.get(key);
174
+ if (cached) return cached;
175
+ const rate = await connector.getTradingFeeRate(symbol);
176
+ cache.set(key, rate);
177
+ return rate;
178
+ };
179
+ var loadFundingRates = (connector, symbol, startTime, endTime) => {
180
+ if (!connector.getFundingRateHistory) return Promise.resolve([]);
181
+ let cache = fundingCache.get(connector);
182
+ if (!cache) {
183
+ cache = /* @__PURE__ */ new Map();
184
+ fundingCache.set(connector, cache);
185
+ }
186
+ const key = `${symbol.toUpperCase()}:${startTime}:${endTime}`;
187
+ const cached = cache.get(key);
188
+ if (cached) return cached;
189
+ const pending = connector.getFundingRateHistory({ symbol, startTime, endTime }).catch(() => []);
190
+ cache.set(key, pending);
191
+ return pending;
192
+ };
193
+ var resolveExecutionCosts = async (params) => {
194
+ const { connector, symbol, config, startTime, endTime, instrument } = params;
195
+ const cacheOnly = config.EXECUTION_COSTS_CACHE_ONLY === true;
196
+ const hasConfiguredFees = Number.isFinite(Number(config.MAKER_FEE_RATE)) && Number.isFinite(Number(config.TAKER_FEE_RATE));
197
+ const exchangeFees = !cacheOnly && !hasConfiguredFees && connector.getTradingFeeRate ? await loadTradingFee(connector, symbol).catch(() => null) : null;
198
+ const makerRate = hasConfiguredFees ? Number(config.MAKER_FEE_RATE) : exchangeFees?.makerRate ?? FEE_PERCENT;
199
+ const takerRate = hasConfiguredFees ? Number(config.TAKER_FEE_RATE) : exchangeFees?.takerRate ?? FEE_PERCENT;
200
+ const fundingEnabled = config.FUNDING_ENABLED !== false && !cacheOnly && typeof connector.getFundingRateHistory === "function";
201
+ const fundingRates = fundingEnabled ? await loadFundingRates(connector, symbol, startTime, endTime) : [];
202
+ const requestedLeverage = Math.max(1, finiteOr(config.LEVERAGE, 10));
203
+ const venueMaxLeverage = Number(instrument?.venueMetadata?.maxLeverage);
204
+ const maxAllowed = Number.isFinite(venueMaxLeverage) ? venueMaxLeverage : null;
205
+ const effectiveLeverage = maxAllowed == null ? requestedLeverage : Math.min(requestedLeverage, maxAllowed);
206
+ const feeSource = hasConfiguredFees ? "config" : exchangeFees?.source ?? "fallback";
207
+ const fundingSource = !fundingEnabled ? cacheOnly ? "fallback" : "disabled" : fundingRates.length ? "historical" : "unavailable";
208
+ const usesFallback = feeSource === "fallback" || fundingEnabled && fundingSource === "unavailable" || config.SLIPPAGE_BASE_BPS == null && config.SLIPPAGE_SPREAD_MULTIPLIER == null && config.SLIPPAGE_MARKET_IMPACT_BPS == null;
209
+ return {
210
+ model: {
211
+ fees: { makerRate, takerRate, source: feeSource },
212
+ funding: {
213
+ enabled: fundingEnabled,
214
+ source: fundingSource,
215
+ points: fundingRates.length,
216
+ fromTimestamp: fundingRates[0]?.timestamp ?? null,
217
+ toTimestamp: fundingRates.at(-1)?.timestamp ?? null
218
+ },
219
+ slippage: {
220
+ baseBps: finiteOr(config.SLIPPAGE_BASE_BPS, BACKTEST_BASE_SLIPPAGE_BPS),
221
+ spreadMultiplier: finiteOr(
222
+ config.SLIPPAGE_SPREAD_MULTIPLIER,
223
+ BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER
224
+ ),
225
+ marketImpactBps: finiteOr(
226
+ config.SLIPPAGE_MARKET_IMPACT_BPS,
227
+ BACKTEST_MARKET_IMPACT_BPS
228
+ ),
229
+ delayRiskMultiplier: finiteOr(
230
+ config.SLIPPAGE_DELAY_RISK_MULTIPLIER,
231
+ BACKTEST_DELAY_RISK_MULTIPLIER
232
+ ),
233
+ source: config.SLIPPAGE_BASE_BPS != null || config.SLIPPAGE_SPREAD_MULTIPLIER != null || config.SLIPPAGE_MARKET_IMPACT_BPS != null ? "config" : "fallback"
234
+ },
235
+ leverage: {
236
+ requested: requestedLeverage,
237
+ effective: effectiveLeverage,
238
+ maxAllowed
239
+ },
240
+ quality: usesFallback ? "fallback" : fundingEnabled && fundingRates.length ? "full" : "partial",
241
+ capturedAt: Date.now()
242
+ },
243
+ fundingRates
244
+ };
245
+ };
44
246
 
45
247
  // src/testConnector.ts
46
248
  import { randomUUID } from "crypto";
47
- import { FEE_PERCENT, INITIAL_BACKTEST_AMOUNT } from "@tradejs/core/constants";
249
+ import { FEE_PERCENT as FEE_PERCENT2, INITIAL_BACKTEST_AMOUNT } from "@tradejs/core/constants";
48
250
  import { calculateStatsFull } from "@tradejs/core/backtest";
49
251
  import {
50
252
  applyExecutionSlippage as applyModeledExecutionSlippage,
@@ -77,8 +279,8 @@ var createTestConnector = (connector, context) => {
77
279
  const positionLog = [];
78
280
  const fastMode = Boolean(context?.fastMode);
79
281
  const executionCostModel = context?.executionCostModel;
80
- const makerFeeRate = executionCostModel?.fees.makerRate ?? FEE_PERCENT;
81
- const takerFeeRate = executionCostModel?.fees.takerRate ?? FEE_PERCENT;
282
+ const makerFeeRate = executionCostModel?.fees.makerRate ?? FEE_PERCENT2;
283
+ const takerFeeRate = executionCostModel?.fees.takerRate ?? FEE_PERCENT2;
82
284
  const fundingRates = [...context?.fundingRates ?? []].sort(
83
285
  (left, right) => left.timestamp - right.timestamp
84
286
  );
@@ -1054,193 +1256,59 @@ var createTestConnector = (connector, context) => {
1054
1256
  };
1055
1257
  };
1056
1258
 
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);
1259
+ // src/backtest/session.ts
1260
+ var createWarningCounts = () => ({
1261
+ [BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY]: 0
1262
+ });
1263
+ var recordSignalWarning = (warningCounts, signal) => {
1264
+ if (signal && typeof signal !== "string" && signal.orderStatus === "failed" && signal.orderFailureReason === BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY) {
1265
+ const code = BACKTEST_WARNING_CODES.TAKE_PROFIT_CROSSED_BEFORE_ENTRY;
1266
+ warningCounts[code] = (warningCounts[code] ?? 0) + 1;
1077
1267
  }
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;
1084
1268
  };
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);
1091
- }
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;
1269
+ var buildDatasetMetadata = (test) => test.backtestRunId && test.backtestTestKey && test.chunkId ? {
1270
+ backtestRunId: test.backtestRunId,
1271
+ backtestTestKey: test.backtestTestKey,
1272
+ backtestChunkId: test.chunkId
1273
+ } : {};
1274
+ var cloneSignal = (signal) => {
1275
+ const cloneValue = (value) => {
1276
+ if (value == null) return value;
1277
+ if (typeof structuredClone === "function") return structuredClone(value);
1278
+ return JSON.parse(JSON.stringify(value));
1279
+ };
1280
+ return {
1281
+ ...signal,
1282
+ figures: cloneValue(signal.figures),
1283
+ indicators: cloneValue(signal.indicators),
1284
+ additionalIndicators: cloneValue(signal.additionalIndicators)
1285
+ };
1098
1286
  };
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;
1287
+ var buildReplayEvaluation = ({
1288
+ signal,
1289
+ test,
1290
+ interval,
1291
+ candle
1292
+ }) => {
1293
+ if (!signal || typeof signal === "string") {
1294
+ return {
1295
+ evaluationId: `${test.testId}:${test.strategyName}:${test.symbol}:${candle.timestamp}`,
1296
+ userName: test.userName,
1297
+ strategy: test.strategyName,
1298
+ symbol: test.symbol,
1299
+ interval,
1300
+ timestamp: candle.timestamp,
1301
+ evaluatedAt: candle.timestamp,
1302
+ status: "skip",
1303
+ reason: typeof signal === "string" && signal.trim() ? signal : "NO_SIGNAL"
1304
+ };
1305
+ }
1306
+ const signalTimestamp = typeof signal.timestamp === "number" && Number.isFinite(signal.timestamp) ? signal.timestamp : candle.timestamp;
1115
1307
  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
- };
1209
- return {
1210
- ...signal,
1211
- figures: cloneValue(signal.figures),
1212
- indicators: cloneValue(signal.indicators),
1213
- additionalIndicators: cloneValue(signal.additionalIndicators)
1214
- };
1215
- };
1216
- var buildReplaySignalEvaluationRecord = ({
1217
- signal,
1218
- testId,
1219
- userName,
1220
- strategyName,
1221
- symbol,
1222
- interval,
1223
- candle
1224
- }) => {
1225
- if (!signal || typeof signal === "string") {
1226
- return {
1227
- evaluationId: `${testId}:${strategyName}:${symbol}:${candle.timestamp}`,
1228
- userName,
1229
- strategy: strategyName,
1230
- symbol,
1231
- interval,
1232
- timestamp: candle.timestamp,
1233
- evaluatedAt: candle.timestamp,
1234
- status: "skip",
1235
- reason: typeof signal === "string" && signal.trim() ? signal : "NO_SIGNAL"
1236
- };
1237
- }
1238
- const signalTimestamp = typeof signal.timestamp === "number" && Number.isFinite(signal.timestamp) ? signal.timestamp : candle.timestamp;
1239
- return {
1240
- evaluationId: `${signal.signalId || testId}:${strategyName}:${symbol}:${signalTimestamp}`,
1241
- userName,
1242
- strategy: signal.strategy || strategyName,
1243
- symbol: signal.symbol || symbol,
1308
+ evaluationId: `${signal.signalId || test.testId}:${test.strategyName}:${test.symbol}:${signalTimestamp}`,
1309
+ userName: test.userName,
1310
+ strategy: signal.strategy || test.strategyName,
1311
+ symbol: signal.symbol || test.symbol,
1244
1312
  interval: signal.interval || interval,
1245
1313
  timestamp: signalTimestamp,
1246
1314
  evaluatedAt: candle.timestamp,
@@ -1254,6 +1322,207 @@ var buildReplaySignalEvaluationRecord = ({
1254
1322
  ml: signal.ml
1255
1323
  };
1256
1324
  };
1325
+ var createBacktestSession = async ({
1326
+ test,
1327
+ connector,
1328
+ strategyCreator,
1329
+ preparedData,
1330
+ interval,
1331
+ sharedIndicatorsReplayKey,
1332
+ monitor
1333
+ }) => {
1334
+ const instrument = test.instrument;
1335
+ const start = test.options.start;
1336
+ if (!start) throw new Error("no start");
1337
+ const { model: executionCostModel, fundingRates } = await resolveExecutionCosts({
1338
+ connector,
1339
+ symbol: test.symbol,
1340
+ config: test.strategyConfig,
1341
+ startTime: start,
1342
+ endTime: test.options.end,
1343
+ instrument
1344
+ });
1345
+ const testConnector = createTestConnector(connector, {
1346
+ userName: test.userName,
1347
+ mlEnabled: test.ml,
1348
+ aiEnabled: test.ai,
1349
+ fastMode: test.fast,
1350
+ instrument,
1351
+ executionCostModel,
1352
+ fundingRates
1353
+ });
1354
+ const strategy = await monitor.run(
1355
+ "strategy init",
1356
+ () => strategyCreator({
1357
+ userName: test.userName,
1358
+ connectorName: test.connectorName,
1359
+ universe: test.universe ?? "crypto",
1360
+ assetClass: test.assetClass ?? instrument?.assetClass,
1361
+ instrument,
1362
+ accountId: test.accountId,
1363
+ deploymentId: test.deploymentId,
1364
+ policyProfileId: test.policyProfileId,
1365
+ config: { ...test.strategyConfig, INTERVAL: test.interval ?? interval },
1366
+ symbol: test.symbol,
1367
+ data: preparedData.prevData.slice(),
1368
+ btcData: preparedData.btcPrevData.slice(),
1369
+ ethData: [...preparedData.ethPrevData, ...preparedData.ethTestData],
1370
+ btcBinanceData: preparedData.btcBinanceData,
1371
+ btcCoinbaseData: preparedData.btcCoinbaseData,
1372
+ backtestExecutionMarketData: {
1373
+ interval: preparedData.backtestExecutionInterval,
1374
+ data: preparedData.backtestExecutionData,
1375
+ btcData: preparedData.backtestExecutionBtcData,
1376
+ dataByTimestamp: preparedData.backtestExecutionDataByTimestamp,
1377
+ btcDataByTimestamp: preparedData.backtestExecutionBtcDataByTimestamp
1378
+ },
1379
+ connector: testConnector,
1380
+ sharedIndicatorsReplayKey
1381
+ })
1382
+ );
1383
+ const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
1384
+ const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
1385
+ const warningCounts = createWarningCounts();
1386
+ const replayEvaluations = test.collectReplaySignalEvaluations ? [] : null;
1387
+ const chunkId = test.chunkId ?? "single";
1388
+ const processSignal = async (signal, candle) => {
1389
+ recordSignalWarning(warningCounts, signal);
1390
+ if (typeof signal === "string" && signal.startsWith("BACKTEST_ENTRY_DELAY_")) {
1391
+ return;
1392
+ }
1393
+ if (replayEvaluations) {
1394
+ replayEvaluations.push(
1395
+ buildReplayEvaluation({ signal, test, interval, candle })
1396
+ );
1397
+ }
1398
+ if (signal && typeof signal !== "string" && signal.signalId && (test.ml || test.ai)) {
1399
+ await enrichSignalWithMarketContextStages({
1400
+ signal,
1401
+ env: "BACKTEST",
1402
+ coinMarketCapEnabled: true,
1403
+ onStageStart: monitor.contextStage
1404
+ });
1405
+ }
1406
+ if (test.ml && signal && typeof signal !== "string" && signal.signalId) {
1407
+ pendingMlPayloadBySignalId.set(
1408
+ signal.signalId,
1409
+ buildMlPayload({
1410
+ signal,
1411
+ context: {
1412
+ userName: test.userName,
1413
+ testId: test.testId,
1414
+ testSuiteId: test.testSuiteId,
1415
+ testName: test.name,
1416
+ configId: test.configId,
1417
+ symbol: test.symbol,
1418
+ strategyName: test.strategyName,
1419
+ strategyConfig: test.strategyConfig,
1420
+ connectorName: test.connectorName
1421
+ }
1422
+ })
1423
+ );
1424
+ }
1425
+ if (test.ai && signal && typeof signal !== "string" && signal.signalId) {
1426
+ pendingAiRowBySignalId.set(signal.signalId, {
1427
+ signalId: signal.signalId,
1428
+ strategyName: signal.strategy || test.strategyName,
1429
+ symbol: signal.symbol || test.symbol,
1430
+ direction: signal.direction,
1431
+ timestamp: signal.timestamp,
1432
+ signal: cloneSignal(signal),
1433
+ testId: test.testId,
1434
+ testSuiteId: test.testSuiteId,
1435
+ testName: test.name,
1436
+ configId: test.configId,
1437
+ connectorName: test.connectorName,
1438
+ ...buildDatasetMetadata({ ...test, chunkId })
1439
+ });
1440
+ }
1441
+ };
1442
+ const flush = async () => {
1443
+ if (!test.ml && !test.ai) return;
1444
+ const batch = await testConnector.drainMlResultsBatch();
1445
+ for (const resultRecord of batch) {
1446
+ const payload = pendingMlPayloadBySignalId.get(resultRecord.signalId);
1447
+ if (payload) {
1448
+ pendingMlPayloadBySignalId.delete(resultRecord.signalId);
1449
+ const fullRow = buildMlTrainingRow(payload, {
1450
+ profit: resultRecord.profit
1451
+ });
1452
+ await appendMlDatasetRow({
1453
+ strategyName: test.strategyName,
1454
+ chunkId,
1455
+ row: {
1456
+ ...trimMlTrainingRowWindows(fullRow, 5),
1457
+ ...buildDatasetMetadata({ ...test, chunkId })
1458
+ }
1459
+ });
1460
+ }
1461
+ const aiRowBase = pendingAiRowBySignalId.get(resultRecord.signalId);
1462
+ if (aiRowBase) {
1463
+ pendingAiRowBySignalId.delete(resultRecord.signalId);
1464
+ const { signal: aiSignal, ...rowBase } = aiRowBase;
1465
+ await appendAiDatasetRow({
1466
+ strategyName: test.strategyName,
1467
+ chunkId,
1468
+ row: {
1469
+ ...rowBase,
1470
+ payload: buildAiPayload(aiSignal),
1471
+ profit: resultRecord.profit,
1472
+ tradeResult: resultRecord.tradeResult
1473
+ }
1474
+ });
1475
+ }
1476
+ }
1477
+ };
1478
+ return {
1479
+ detectorFanoutKey: strategy.detectorFanoutKey,
1480
+ detectorNoSignalSkipReason: strategy.detectorNoSignalSkipReason,
1481
+ next: async (candle, btcCandle, detectorSkipCode) => {
1482
+ const delayedSignal = await monitor.runStrategy(
1483
+ "delayed entry",
1484
+ () => strategy.__tradejsFlushBacktestDelayedEntry?.(candle, btcCandle) ?? Promise.resolve(void 0)
1485
+ );
1486
+ if (delayedSignal && typeof delayedSignal !== "string") {
1487
+ await processSignal(delayedSignal, candle);
1488
+ }
1489
+ await monitor.run("exit checks", () => testConnector.checkExits(candle));
1490
+ const signal = await monitor.runStrategy(
1491
+ detectorSkipCode ? "strategy detector skip" : "strategy signal",
1492
+ () => detectorSkipCode && strategy.canFastAdvanceDetectorNoSignal && strategy.advanceDetectorNoSignal ? strategy.advanceDetectorNoSignal(
1493
+ candle,
1494
+ btcCandle,
1495
+ detectorSkipCode
1496
+ ) : detectorSkipCode && strategy.skipDetectorNoSignal ? strategy.skipDetectorNoSignal(
1497
+ candle,
1498
+ btcCandle,
1499
+ detectorSkipCode
1500
+ ) : strategy(candle, btcCandle)
1501
+ );
1502
+ await processSignal(signal, candle);
1503
+ return signal;
1504
+ },
1505
+ flush: () => monitor.run("flush closed results", flush),
1506
+ result: async () => {
1507
+ await monitor.run("flush closed results", flush);
1508
+ const result = await monitor.run(
1509
+ "collect result",
1510
+ () => testConnector.getResult()
1511
+ );
1512
+ return replayEvaluations ? {
1513
+ ...result,
1514
+ warningCounts,
1515
+ inlineReplaySignalEvaluations: replayEvaluations
1516
+ } : { ...result, warningCounts };
1517
+ }
1518
+ };
1519
+ };
1520
+
1521
+ // src/testing.ts
1522
+ var CLOSED_RESULT_FLUSH_INTERVAL = 500;
1523
+ var buildCandleByTimestamp = (candles) => new Map(
1524
+ (candles ?? []).filter((candle) => typeof candle?.timestamp === "number").map((candle) => [candle.timestamp, candle])
1525
+ );
1257
1526
  var createTestingKlineCacheState = () => ({
1258
1527
  coinKlineCache: /* @__PURE__ */ new Map(),
1259
1528
  btcKlineCache: /* @__PURE__ */ new Map(),
@@ -1763,132 +2032,35 @@ var canRunTestsInSharedCandleLoop = (tests) => {
1763
2032
  (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
2033
  );
1765
2034
  };
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
- }) => {
2035
+ var testing = async (test) => {
2036
+ const {
2037
+ userName,
2038
+ symbol,
2039
+ options: { start, end },
2040
+ name,
2041
+ strategyName,
2042
+ connectorName,
2043
+ universe = "crypto",
2044
+ accountId,
2045
+ deploymentId,
2046
+ interval = BACKTEST_INTERVAL,
2047
+ timeoutMs
2048
+ } = test;
1793
2049
  if (!start) {
1794
2050
  throw new Error("no start");
1795
2051
  }
1796
2052
  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);
2053
+ const progress = createBacktestProgress({
2054
+ testName: name,
2055
+ symbol,
2056
+ strategyName,
2057
+ timeoutMs,
2058
+ timeoutSubject: `Test ${name} (${symbol})`
2059
+ });
1888
2060
  const { projectRoot, state } = getTestingKlineCacheState();
1889
- const connector = await withTimeout(
2061
+ const connector = await progress.run(
1890
2062
  "connector init",
1891
- getCachedConnector({
2063
+ () => getCachedConnector({
1892
2064
  state,
1893
2065
  projectRoot,
1894
2066
  userName,
@@ -1901,16 +2073,16 @@ var testing = async ({
1901
2073
  if (!connector) {
1902
2074
  throw new Error(`Unknown connector: ${connectorName}`);
1903
2075
  }
1904
- const strategyCreator = await withTimeout(
2076
+ const strategyCreator = await progress.run(
1905
2077
  "strategy lookup",
1906
- getStrategyCreator(strategyName, projectRoot)
2078
+ () => getStrategyCreator(strategyName, projectRoot)
1907
2079
  );
1908
2080
  if (!strategyCreator) {
1909
2081
  throw new Error(`Unknown strategy: ${strategyName}`);
1910
2082
  }
1911
- const preparedData = await withTimeout(
2083
+ const preparedData = await progress.run(
1912
2084
  "kline preload",
1913
- prepareTestingData({
2085
+ () => prepareTestingData({
1914
2086
  state,
1915
2087
  projectRoot,
1916
2088
  userName,
@@ -1928,225 +2100,26 @@ var testing = async ({
1928
2100
  if (!preparedData) {
1929
2101
  throw new Error("Prepared backtest data not available");
1930
2102
  }
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({
2103
+ const { testData, btcTestData } = preparedData;
2104
+ const session = await createBacktestSession({
2105
+ test,
1952
2106
  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
2107
+ strategyCreator,
2108
+ preparedData,
2109
+ interval,
2110
+ monitor: progress
1967
2111
  });
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
2112
  for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
2114
2113
  if (candleIndex % 25 === 0) {
2115
- throwIfTimedOut("candle loop");
2116
- }
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);
2114
+ progress.checkpoint("candle loop");
2129
2115
  }
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);
2116
+ progress.setCandle(candleIndex + 1, testData.length);
2117
+ await session.next(testData[candleIndex], btcTestData[candleIndex]);
2136
2118
  if ((candleIndex + 1) % CLOSED_RESULT_FLUSH_INTERVAL === 0) {
2137
- await withTimeout("flush closed results", flushClosedResultsBatch());
2119
+ await session.flush();
2138
2120
  }
2139
2121
  }
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;
2122
+ return session.result();
2150
2123
  };
2151
2124
  var testingGroupInSharedCandleLoop = async (tests) => {
2152
2125
  if (!canRunTestsInSharedCandleLoop(tests)) {
@@ -2170,10 +2143,6 @@ var testingGroupInSharedCandleLoop = async (tests) => {
2170
2143
  accountId,
2171
2144
  deploymentId,
2172
2145
  interval = BACKTEST_INTERVAL,
2173
- ml = false,
2174
- ai = false,
2175
- fast = false,
2176
- collectReplaySignalEvaluations = false,
2177
2146
  chunkId = "single",
2178
2147
  timeoutMs
2179
2148
  } = first;
@@ -2181,101 +2150,17 @@ var testingGroupInSharedCandleLoop = async (tests) => {
2181
2150
  throw new Error("no start");
2182
2151
  }
2183
2152
  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);
2153
+ const progress = createBacktestProgress({
2154
+ testName: first.name,
2155
+ symbol,
2156
+ strategyName,
2157
+ timeoutMs,
2158
+ timeoutSubject: `Test group ${strategyName}/${symbol}`
2159
+ });
2275
2160
  const { projectRoot, state } = getTestingKlineCacheState();
2276
- const connector = await withTimeout(
2161
+ const connector = await progress.run(
2277
2162
  "connector init",
2278
- getCachedConnector({
2163
+ () => getCachedConnector({
2279
2164
  state,
2280
2165
  projectRoot,
2281
2166
  userName,
@@ -2288,16 +2173,16 @@ var testingGroupInSharedCandleLoop = async (tests) => {
2288
2173
  if (!connector) {
2289
2174
  throw new Error(`Unknown connector: ${connectorName}`);
2290
2175
  }
2291
- const strategyCreator = await withTimeout(
2176
+ const strategyCreator = await progress.run(
2292
2177
  "strategy lookup",
2293
- getStrategyCreator(strategyName, projectRoot)
2178
+ () => getStrategyCreator(strategyName, projectRoot)
2294
2179
  );
2295
2180
  if (!strategyCreator) {
2296
2181
  throw new Error(`Unknown strategy: ${strategyName}`);
2297
2182
  }
2298
- const preparedData = await withTimeout(
2183
+ const preparedData = await progress.run(
2299
2184
  "kline preload",
2300
- prepareTestingData({
2185
+ () => prepareTestingData({
2301
2186
  state,
2302
2187
  projectRoot,
2303
2188
  userName,
@@ -2315,22 +2200,7 @@ var testingGroupInSharedCandleLoop = async (tests) => {
2315
2200
  if (!preparedData) {
2316
2201
  throw new Error("Prepared backtest data not available");
2317
2202
  }
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;
2203
+ const { testData, btcTestData } = preparedData;
2334
2204
  const sharedIndicatorsReplayKey = [
2335
2205
  "shared",
2336
2206
  userName,
@@ -2345,250 +2215,45 @@ var testingGroupInSharedCandleLoop = async (tests) => {
2345
2215
  const runners = [];
2346
2216
  try {
2347
2217
  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
2218
  runners.push({
2399
2219
  test,
2400
- strategy,
2401
- testConnector,
2402
- pendingMlPayloadBySignalId: /* @__PURE__ */ new Map(),
2403
- pendingAiRowBySignalId: /* @__PURE__ */ new Map(),
2404
- warningCounts: createBacktestWarningCounts(),
2405
- replaySignalEvaluations: collectReplaySignalEvaluations ? [] : null
2220
+ session: await createBacktestSession({
2221
+ test,
2222
+ connector,
2223
+ strategyCreator,
2224
+ preparedData,
2225
+ interval,
2226
+ sharedIndicatorsReplayKey,
2227
+ monitor: progress
2228
+ })
2406
2229
  });
2407
2230
  }
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
2231
  for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
2526
2232
  if (candleIndex % 25 === 0) {
2527
- throwIfTimedOut("candle loop");
2233
+ progress.checkpoint("candle loop");
2528
2234
  }
2529
- currentCandleIndex = candleIndex + 1;
2530
- emitProgress("candle loop", {
2531
- force: candleIndex === 0 || currentCandleIndex === totalCandles
2532
- });
2235
+ progress.setCandle(candleIndex + 1, testData.length);
2533
2236
  const candle = testData[candleIndex];
2534
2237
  const btcCandle = btcTestData[candleIndex];
2535
2238
  const detectorNoSignalByKey = /* @__PURE__ */ new Map();
2536
2239
  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;
2240
+ const { session } = runner;
2241
+ const detectorFanoutKey = session.detectorFanoutKey;
2547
2242
  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) {
2243
+ const signal = await session.next(candle, btcCandle, detectorSkipCode);
2244
+ if (detectorFanoutKey && session.detectorNoSignalSkipReason && typeof signal === "string" && signal === session.detectorNoSignalSkipReason) {
2561
2245
  detectorNoSignalByKey.set(detectorFanoutKey, signal);
2562
2246
  }
2563
- await processRunnerSignal(runner, signal, candle);
2564
2247
  }
2565
2248
  if ((candleIndex + 1) % CLOSED_RESULT_FLUSH_INTERVAL === 0) {
2566
- await withTimeout(
2567
- "flush closed results",
2568
- Promise.all(runners.map((runner) => flushClosedResultsBatch(runner)))
2569
- );
2249
+ await Promise.all(runners.map(({ session }) => session.flush()));
2570
2250
  }
2571
2251
  }
2572
2252
  const results = [];
2573
2253
  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
2254
  results.push({
2583
2255
  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
- }
2256
+ result: await runner.session.result()
2592
2257
  });
2593
2258
  }
2594
2259
  return results;