@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/ai.js +4763 -168
- package/dist/ai.mjs +2 -3
- package/dist/backtest.js +4117 -1326
- package/dist/backtest.mjs +660 -850
- package/dist/chunk-4AVFJMQL.mjs +6054 -0
- package/dist/chunk-Y6FXYEAI.mjs +10 -0
- package/dist/cli.js +5537 -6403
- package/dist/cli.mjs +5 -11
- package/dist/connectors.mjs +1 -1
- package/dist/constants.mjs +1 -1
- package/dist/pine.mjs +122 -13
- package/dist/registry.js +5732 -48
- package/dist/registry.mjs +2 -2
- package/dist/strategies.d.mts +2 -2
- package/dist/strategies.d.ts +2 -2
- package/dist/strategies.js +2296 -7843
- package/dist/strategies.mjs +46 -2547
- package/package.json +6 -4
- package/dist/chunk-3BP3ETAD.mjs +0 -2134
- package/dist/chunk-GPR56UYQ.mjs +0 -5518
- package/dist/chunk-IQKMII6L.mjs +0 -137
- package/dist/chunk-QVSMINLG.mjs +0 -246
- package/dist/chunk-UV2HVMKZ.mjs +0 -41
- package/dist/chunk-WK5EUCX5.mjs +0 -1191
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-
|
|
10
|
+
} from "./chunk-4AVFJMQL.mjs";
|
|
15
11
|
import {
|
|
16
12
|
getTradejsProjectCwd
|
|
17
13
|
} from "./chunk-WS5DYEVZ.mjs";
|
|
18
|
-
import "./chunk-
|
|
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
|
-
|
|
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 ??
|
|
81
|
-
const takerFeeRate = executionCostModel?.fees.takerRate ??
|
|
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/
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
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
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
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
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
const
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
|
1319
|
+
var buildReplayEvaluation = ({
|
|
1217
1320
|
signal,
|
|
1218
|
-
|
|
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
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
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
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1933
|
-
|
|
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
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
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
|
-
|
|
2259
|
+
progress.checkpoint("candle loop");
|
|
2116
2260
|
}
|
|
2117
|
-
|
|
2118
|
-
|
|
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
|
|
2264
|
+
await session.flush();
|
|
2138
2265
|
}
|
|
2139
2266
|
}
|
|
2140
|
-
|
|
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
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
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
|
-
|
|
2378
|
+
progress.checkpoint("candle loop");
|
|
2528
2379
|
}
|
|
2529
|
-
|
|
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 {
|
|
2538
|
-
const
|
|
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
|
|
2549
|
-
|
|
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
|
|
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.
|
|
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;
|