@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/ai.js +4763 -168
- package/dist/ai.mjs +2 -3
- package/dist/backtest.js +4018 -1372
- package/dist/backtest.mjs +537 -872
- 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,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
|
-
|
|
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 ??
|
|
81
|
-
const takerFeeRate = executionCostModel?.fees.takerRate ??
|
|
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/
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
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
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
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
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
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
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
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
|
-
|
|
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
|
-
}) => {
|
|
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
|
|
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);
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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({
|
|
2103
|
+
const { testData, btcTestData } = preparedData;
|
|
2104
|
+
const session = await createBacktestSession({
|
|
2105
|
+
test,
|
|
1952
2106
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2131
|
-
|
|
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
|
|
2119
|
+
await session.flush();
|
|
2138
2120
|
}
|
|
2139
2121
|
}
|
|
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;
|
|
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
|
|
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);
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
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
|
-
|
|
2233
|
+
progress.checkpoint("candle loop");
|
|
2528
2234
|
}
|
|
2529
|
-
|
|
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 {
|
|
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;
|
|
2240
|
+
const { session } = runner;
|
|
2241
|
+
const detectorFanoutKey = session.detectorFanoutKey;
|
|
2547
2242
|
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) {
|
|
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
|
|
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.
|
|
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;
|