@tradejs/node 1.0.9 → 1.0.11
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/README.md +1 -1
- package/dist/ai.d.mts +4 -1
- package/dist/ai.d.ts +4 -1
- package/dist/ai.js +601 -93
- package/dist/ai.mjs +5 -3
- package/dist/backtest.d.mts +26 -2
- package/dist/backtest.d.ts +26 -2
- package/dist/backtest.js +3367 -481
- package/dist/backtest.mjs +1845 -269
- package/dist/chunk-37VNDZVX.mjs +1040 -0
- package/dist/chunk-IUZML4RK.mjs +1136 -0
- package/dist/{chunk-WGOYR6AB.mjs → chunk-QVSMINLG.mjs} +1 -1
- package/dist/{chunk-JMDYEKIO.mjs → chunk-V3YMKE4I.mjs} +1 -1
- package/dist/{chunk-JU77QVJ3.mjs → chunk-WS5DYEVZ.mjs} +59 -5
- package/dist/cli.d.mts +13 -3
- package/dist/cli.d.ts +13 -3
- package/dist/cli.js +1016 -234
- package/dist/cli.mjs +338 -65
- package/dist/connectors.js +59 -5
- package/dist/connectors.mjs +2 -2
- package/dist/registry.js +59 -5
- package/dist/registry.mjs +2 -2
- package/dist/strategies.d.mts +29 -8
- package/dist/strategies.d.ts +29 -8
- package/dist/strategies.js +3428 -1285
- package/dist/strategies.mjs +904 -112
- package/package.json +7 -7
- package/dist/chunk-2JKX3DM7.mjs +0 -619
- package/dist/chunk-JRRG3YQG.mjs +0 -154
package/dist/backtest.mjs
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BUILTIN_CONNECTOR_NAMES,
|
|
3
3
|
getConnectorCreatorByName
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-V3YMKE4I.mjs";
|
|
5
5
|
import {
|
|
6
6
|
buildMlPayload,
|
|
7
|
+
enrichSignalWithBinanceMarketContext,
|
|
8
|
+
enrichSignalWithCoinMarketCapContext,
|
|
7
9
|
enrichSignalWithDerivativesContext
|
|
8
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-37VNDZVX.mjs";
|
|
9
11
|
import {
|
|
10
12
|
buildAiPayload
|
|
11
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-IUZML4RK.mjs";
|
|
12
14
|
import {
|
|
13
15
|
getStrategyCreator
|
|
14
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-QVSMINLG.mjs";
|
|
15
17
|
import {
|
|
16
18
|
getTradejsProjectCwd
|
|
17
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-WS5DYEVZ.mjs";
|
|
18
20
|
import "./chunk-6DZX6EAA.mjs";
|
|
19
21
|
|
|
20
22
|
// src/backtest.ts
|
|
@@ -22,6 +24,14 @@ export * from "@tradejs/core/backtest";
|
|
|
22
24
|
|
|
23
25
|
// src/testing.ts
|
|
24
26
|
import { alignSortedCandlesByTimestamp } from "@tradejs/core/indicators";
|
|
27
|
+
import {
|
|
28
|
+
BACKTEST_EXECUTION_INTERVAL,
|
|
29
|
+
BACKTEST_LOWER_TIMEFRAME_EXECUTION_ENABLED
|
|
30
|
+
} from "@tradejs/core/constants";
|
|
31
|
+
import {
|
|
32
|
+
releaseStrategyIndicatorsReplayCache,
|
|
33
|
+
releaseStrategyReplayCache
|
|
34
|
+
} from "@tradejs/core/strategies";
|
|
25
35
|
import { getBacktestPreloadStart } from "@tradejs/core/time";
|
|
26
36
|
import { appendAiDatasetRow } from "@tradejs/infra/ai";
|
|
27
37
|
import {
|
|
@@ -33,19 +43,37 @@ import { logger } from "@tradejs/infra/logger";
|
|
|
33
43
|
|
|
34
44
|
// src/testConnector.ts
|
|
35
45
|
import { randomUUID } from "crypto";
|
|
46
|
+
import { FEE_PERCENT, INITIAL_BACKTEST_AMOUNT } from "@tradejs/core/constants";
|
|
47
|
+
import { calculateStatsFull } from "@tradejs/core/backtest";
|
|
48
|
+
import {
|
|
49
|
+
applyExecutionSlippage as applyModeledExecutionSlippage,
|
|
50
|
+
calculateExecutionSlippageBreakdown,
|
|
51
|
+
extractExecutionMarketImpactBps,
|
|
52
|
+
extractExecutionSpreadBps,
|
|
53
|
+
extractExecutionDelayRiskBps
|
|
54
|
+
} from "@tradejs/core/trade";
|
|
36
55
|
import { round } from "@tradejs/core/math";
|
|
37
|
-
var
|
|
38
|
-
var INITIAL_AMOUNT = 100;
|
|
56
|
+
var PRICE_PRECISION = 8;
|
|
39
57
|
var createTestConnector = (connector, context) => {
|
|
40
58
|
let state = {};
|
|
41
59
|
const orderLog = [];
|
|
42
60
|
const positionLog = [];
|
|
61
|
+
const fastMode = Boolean(context?.fastMode);
|
|
62
|
+
const executionCostModel = context?.executionCostModel;
|
|
63
|
+
const makerFeeRate = executionCostModel?.fees.makerRate ?? FEE_PERCENT;
|
|
64
|
+
const takerFeeRate = executionCostModel?.fees.takerRate ?? FEE_PERCENT;
|
|
65
|
+
const fundingRates = [...context?.fundingRates ?? []].sort(
|
|
66
|
+
(left, right) => left.timestamp - right.timestamp
|
|
67
|
+
);
|
|
68
|
+
const processedFundingTimestamps = /* @__PURE__ */ new Set();
|
|
43
69
|
let currentPosition = null;
|
|
44
|
-
let amount =
|
|
70
|
+
let amount = INITIAL_BACKTEST_AMOUNT;
|
|
45
71
|
let originalQty = 0;
|
|
46
72
|
let currentPositionProfit = 0;
|
|
73
|
+
let currentSignalId = null;
|
|
47
74
|
let takeProfits = [];
|
|
48
75
|
let stopLossPrice = null;
|
|
76
|
+
let currentTradeResult = null;
|
|
49
77
|
const closedSignalResults = [];
|
|
50
78
|
const logOrder = (data) => {
|
|
51
79
|
const nextEntry = {
|
|
@@ -56,10 +84,157 @@ var createTestConnector = (connector, context) => {
|
|
|
56
84
|
index: orderLog.length
|
|
57
85
|
};
|
|
58
86
|
if (nextEntry.signal) {
|
|
59
|
-
const {
|
|
60
|
-
|
|
87
|
+
const {
|
|
88
|
+
additionalIndicators: _additionalIndicators,
|
|
89
|
+
indicators: _indicators,
|
|
90
|
+
...signalWithoutHeavyContext
|
|
91
|
+
} = nextEntry.signal;
|
|
92
|
+
nextEntry.signal = signalWithoutHeavyContext;
|
|
93
|
+
}
|
|
94
|
+
if (!fastMode) {
|
|
95
|
+
orderLog.push(nextEntry);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const roundNullable = (value) => value == null ? null : round(value);
|
|
99
|
+
const roundPrice = (value) => round(value, PRICE_PRECISION);
|
|
100
|
+
const roundNullablePrice = (value) => value == null ? null : roundPrice(value);
|
|
101
|
+
const getSlippageCost = ({
|
|
102
|
+
requestedPrice,
|
|
103
|
+
executionPrice,
|
|
104
|
+
direction,
|
|
105
|
+
stage,
|
|
106
|
+
qty
|
|
107
|
+
}) => {
|
|
108
|
+
if (direction === "LONG") {
|
|
109
|
+
return stage === "entry" ? Math.max(0, executionPrice - requestedPrice) * qty : Math.max(0, requestedPrice - executionPrice) * qty;
|
|
110
|
+
}
|
|
111
|
+
return stage === "entry" ? Math.max(0, requestedPrice - executionPrice) * qty : Math.max(0, executionPrice - requestedPrice) * qty;
|
|
112
|
+
};
|
|
113
|
+
const getSlippageBps = (requestedPrice, executionPrice) => requestedPrice ? (executionPrice - requestedPrice) / requestedPrice * 1e4 : 0;
|
|
114
|
+
const getWeightedAverage = (previousValue, previousQty, nextValue, nextQty) => {
|
|
115
|
+
if (previousValue == null || previousQty <= 0) {
|
|
116
|
+
return nextValue;
|
|
117
|
+
}
|
|
118
|
+
return (previousValue * previousQty + nextValue * nextQty) / (previousQty + nextQty);
|
|
119
|
+
};
|
|
120
|
+
const finalizeTradeResult = (tradeResult, timestamp) => {
|
|
121
|
+
if (!tradeResult.exitReason) {
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
return {
|
|
125
|
+
...tradeResult,
|
|
126
|
+
exitTimestamp: tradeResult.exitTimestamp ?? timestamp,
|
|
127
|
+
exitReason: tradeResult.exitReason,
|
|
128
|
+
requestedEntryPrice: roundPrice(tradeResult.requestedEntryPrice),
|
|
129
|
+
entryPrice: roundPrice(tradeResult.entryPrice),
|
|
130
|
+
requestedExitPrice: roundNullablePrice(tradeResult.requestedExitPrice),
|
|
131
|
+
exitPrice: roundNullablePrice(tradeResult.exitPrice),
|
|
132
|
+
grossProfit: round(tradeResult.grossProfit),
|
|
133
|
+
netProfit: round(tradeResult.netProfit),
|
|
134
|
+
openFee: round(tradeResult.openFee),
|
|
135
|
+
closeFee: round(tradeResult.closeFee),
|
|
136
|
+
fundingFee: roundNullable(tradeResult.fundingFee),
|
|
137
|
+
totalFee: round(tradeResult.totalFee),
|
|
138
|
+
entrySlippagePrice: round(tradeResult.entrySlippagePrice),
|
|
139
|
+
entrySlippageBps: round(tradeResult.entrySlippageBps),
|
|
140
|
+
entryBaseSlippageBps: round(tradeResult.entryBaseSlippageBps),
|
|
141
|
+
entrySpreadBps: round(tradeResult.entrySpreadBps),
|
|
142
|
+
entrySpreadSlippageBps: round(tradeResult.entrySpreadSlippageBps),
|
|
143
|
+
entryMarketImpactBps: round(tradeResult.entryMarketImpactBps),
|
|
144
|
+
entryDelayRiskBps: roundNullable(tradeResult.entryDelayRiskBps),
|
|
145
|
+
entrySlippageCost: round(tradeResult.entrySlippageCost),
|
|
146
|
+
exitSlippagePrice: roundNullable(tradeResult.exitSlippagePrice),
|
|
147
|
+
exitSlippageBps: roundNullable(tradeResult.exitSlippageBps),
|
|
148
|
+
exitBaseSlippageBps: roundNullable(tradeResult.exitBaseSlippageBps),
|
|
149
|
+
exitSpreadBps: roundNullable(tradeResult.exitSpreadBps),
|
|
150
|
+
exitSpreadSlippageBps: roundNullable(tradeResult.exitSpreadSlippageBps),
|
|
151
|
+
exitMarketImpactBps: roundNullable(tradeResult.exitMarketImpactBps),
|
|
152
|
+
exitDelayRiskBps: roundNullable(tradeResult.exitDelayRiskBps),
|
|
153
|
+
exitSlippageCost: round(tradeResult.exitSlippageCost),
|
|
154
|
+
totalSlippageCost: round(tradeResult.totalSlippageCost),
|
|
155
|
+
qty: round(tradeResult.qty),
|
|
156
|
+
closedQty: round(tradeResult.closedQty)
|
|
157
|
+
};
|
|
158
|
+
};
|
|
159
|
+
const recordExitResult = ({
|
|
160
|
+
timestamp,
|
|
161
|
+
reason,
|
|
162
|
+
requestedPrice,
|
|
163
|
+
executionPrice,
|
|
164
|
+
qty,
|
|
165
|
+
grossProfit,
|
|
166
|
+
fee,
|
|
167
|
+
slippageBreakdown
|
|
168
|
+
}) => {
|
|
169
|
+
if (!currentTradeResult || !currentPosition) {
|
|
170
|
+
return;
|
|
61
171
|
}
|
|
62
|
-
|
|
172
|
+
const previousClosedQty = currentTradeResult.closedQty;
|
|
173
|
+
const requestedExitPrice = getWeightedAverage(
|
|
174
|
+
currentTradeResult.requestedExitPrice,
|
|
175
|
+
previousClosedQty,
|
|
176
|
+
requestedPrice,
|
|
177
|
+
qty
|
|
178
|
+
);
|
|
179
|
+
const exitPrice = getWeightedAverage(
|
|
180
|
+
currentTradeResult.exitPrice,
|
|
181
|
+
previousClosedQty,
|
|
182
|
+
executionPrice,
|
|
183
|
+
qty
|
|
184
|
+
);
|
|
185
|
+
const exitBaseSlippageBps = getWeightedAverage(
|
|
186
|
+
currentTradeResult.exitBaseSlippageBps,
|
|
187
|
+
previousClosedQty,
|
|
188
|
+
slippageBreakdown.baseSlippageBps,
|
|
189
|
+
qty
|
|
190
|
+
);
|
|
191
|
+
const exitSpreadBps = getWeightedAverage(
|
|
192
|
+
currentTradeResult.exitSpreadBps,
|
|
193
|
+
previousClosedQty,
|
|
194
|
+
slippageBreakdown.spreadBps,
|
|
195
|
+
qty
|
|
196
|
+
);
|
|
197
|
+
const exitSpreadSlippageBps = getWeightedAverage(
|
|
198
|
+
currentTradeResult.exitSpreadSlippageBps,
|
|
199
|
+
previousClosedQty,
|
|
200
|
+
slippageBreakdown.spreadSlippageBps,
|
|
201
|
+
qty
|
|
202
|
+
);
|
|
203
|
+
const exitMarketImpactBps = getWeightedAverage(
|
|
204
|
+
currentTradeResult.exitMarketImpactBps,
|
|
205
|
+
previousClosedQty,
|
|
206
|
+
slippageBreakdown.marketImpactBps,
|
|
207
|
+
qty
|
|
208
|
+
);
|
|
209
|
+
const exitDelayRiskBps = null;
|
|
210
|
+
const exitSlippageCost = currentTradeResult.exitSlippageCost + getSlippageCost({
|
|
211
|
+
requestedPrice,
|
|
212
|
+
executionPrice,
|
|
213
|
+
direction: currentPosition.direction,
|
|
214
|
+
stage: "exit",
|
|
215
|
+
qty
|
|
216
|
+
});
|
|
217
|
+
currentTradeResult = {
|
|
218
|
+
...currentTradeResult,
|
|
219
|
+
closedQty: previousClosedQty + qty,
|
|
220
|
+
exitTimestamp: timestamp,
|
|
221
|
+
exitReason: reason,
|
|
222
|
+
requestedExitPrice,
|
|
223
|
+
exitPrice,
|
|
224
|
+
grossProfit: currentTradeResult.grossProfit + grossProfit,
|
|
225
|
+
netProfit: currentTradeResult.netProfit + grossProfit - fee,
|
|
226
|
+
closeFee: currentTradeResult.closeFee + fee,
|
|
227
|
+
totalFee: currentTradeResult.openFee + currentTradeResult.closeFee + fee + (currentTradeResult.fundingFee ?? 0),
|
|
228
|
+
exitSlippagePrice: exitPrice - requestedExitPrice,
|
|
229
|
+
exitSlippageBps: getSlippageBps(requestedExitPrice, exitPrice),
|
|
230
|
+
exitBaseSlippageBps,
|
|
231
|
+
exitSpreadBps,
|
|
232
|
+
exitSpreadSlippageBps,
|
|
233
|
+
exitMarketImpactBps,
|
|
234
|
+
exitDelayRiskBps,
|
|
235
|
+
exitSlippageCost,
|
|
236
|
+
totalSlippageCost: currentTradeResult.entrySlippageCost + exitSlippageCost
|
|
237
|
+
};
|
|
63
238
|
};
|
|
64
239
|
const clearPosition = (timestamp) => {
|
|
65
240
|
takeProfits = [];
|
|
@@ -69,11 +244,12 @@ var createTestConnector = (connector, context) => {
|
|
|
69
244
|
return;
|
|
70
245
|
}
|
|
71
246
|
if (context?.mlEnabled || context?.aiEnabled) {
|
|
72
|
-
|
|
73
|
-
|
|
247
|
+
if (currentSignalId) {
|
|
248
|
+
const tradeResult = currentTradeResult ? finalizeTradeResult(currentTradeResult, timestamp) : void 0;
|
|
74
249
|
closedSignalResults.push({
|
|
75
|
-
signalId,
|
|
76
|
-
profit: currentPositionProfit
|
|
250
|
+
signalId: currentSignalId,
|
|
251
|
+
profit: round(currentPositionProfit),
|
|
252
|
+
...tradeResult ? { tradeResult } : {}
|
|
77
253
|
});
|
|
78
254
|
}
|
|
79
255
|
}
|
|
@@ -89,10 +265,91 @@ var createTestConnector = (connector, context) => {
|
|
|
89
265
|
}
|
|
90
266
|
});
|
|
91
267
|
currentPosition = null;
|
|
268
|
+
currentSignalId = null;
|
|
269
|
+
currentTradeResult = null;
|
|
92
270
|
currentPositionProfit = 0;
|
|
93
271
|
};
|
|
272
|
+
const getNetProfit = ({
|
|
273
|
+
grossProfit,
|
|
274
|
+
price,
|
|
275
|
+
qty,
|
|
276
|
+
feeRate = takerFeeRate
|
|
277
|
+
}) => {
|
|
278
|
+
const fee = price * qty * feeRate;
|
|
279
|
+
return {
|
|
280
|
+
fee,
|
|
281
|
+
profit: grossProfit - fee
|
|
282
|
+
};
|
|
283
|
+
};
|
|
284
|
+
const applyFunding = (candle) => {
|
|
285
|
+
if (!executionCostModel?.funding.enabled || !currentPosition) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
for (const point of fundingRates) {
|
|
289
|
+
if (processedFundingTimestamps.has(point.timestamp) || point.symbol.toUpperCase() !== currentPosition.symbol.toUpperCase() || point.timestamp <= currentPosition.timestamp || point.timestamp > candle.timestamp) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
processedFundingTimestamps.add(point.timestamp);
|
|
293
|
+
const notional = candle.close * currentPosition.qty;
|
|
294
|
+
const fundingCost = notional * point.rate * (currentPosition.direction === "LONG" ? 1 : -1);
|
|
295
|
+
amount -= fundingCost;
|
|
296
|
+
currentPositionProfit -= fundingCost;
|
|
297
|
+
if (currentTradeResult) {
|
|
298
|
+
currentTradeResult.fundingFee = (currentTradeResult.fundingFee ?? 0) + fundingCost;
|
|
299
|
+
currentTradeResult.netProfit -= fundingCost;
|
|
300
|
+
currentTradeResult.totalFee += fundingCost;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const getExitTimestamp = (candle) => currentPosition ? Math.max(candle.timestamp, currentPosition.timestamp) : candle.timestamp;
|
|
305
|
+
const applyExecutionSlippage = ({
|
|
306
|
+
price,
|
|
307
|
+
direction,
|
|
308
|
+
stage,
|
|
309
|
+
signal
|
|
310
|
+
}) => {
|
|
311
|
+
const modelParams = {
|
|
312
|
+
baseSlippageBps: executionCostModel?.slippage.baseBps,
|
|
313
|
+
spreadBps: extractExecutionSpreadBps(signal),
|
|
314
|
+
spreadMultiplier: executionCostModel?.slippage.spreadMultiplier,
|
|
315
|
+
marketImpactBps: extractExecutionMarketImpactBps(signal) ?? executionCostModel?.slippage.marketImpactBps,
|
|
316
|
+
delayRiskBps: stage === "entry" ? (extractExecutionDelayRiskBps(signal) ?? 0) * (executionCostModel?.slippage.delayRiskMultiplier ?? 1) : null
|
|
317
|
+
};
|
|
318
|
+
return applyModeledExecutionSlippage({
|
|
319
|
+
price,
|
|
320
|
+
direction,
|
|
321
|
+
stage,
|
|
322
|
+
...modelParams
|
|
323
|
+
});
|
|
324
|
+
};
|
|
325
|
+
const getExecutionSlippageBreakdown = ({
|
|
326
|
+
stage,
|
|
327
|
+
signal
|
|
328
|
+
}) => calculateExecutionSlippageBreakdown({
|
|
329
|
+
baseSlippageBps: executionCostModel?.slippage.baseBps,
|
|
330
|
+
spreadBps: extractExecutionSpreadBps(signal),
|
|
331
|
+
spreadMultiplier: executionCostModel?.slippage.spreadMultiplier,
|
|
332
|
+
marketImpactBps: extractExecutionMarketImpactBps(signal) ?? executionCostModel?.slippage.marketImpactBps,
|
|
333
|
+
delayRiskBps: stage === "entry" ? (extractExecutionDelayRiskBps(signal) ?? 0) * (executionCostModel?.slippage.delayRiskMultiplier ?? 1) : null
|
|
334
|
+
});
|
|
335
|
+
const getExecutionSlippageLogData = (slippageBreakdown, stage) => ({
|
|
336
|
+
executionSlippageStage: stage,
|
|
337
|
+
executionSlippageBps: round(slippageBreakdown.effectiveSlippageBps),
|
|
338
|
+
executionBaseSlippageBps: round(slippageBreakdown.baseSlippageBps),
|
|
339
|
+
executionSpreadBps: round(slippageBreakdown.spreadBps),
|
|
340
|
+
executionSpreadSlippageBps: round(slippageBreakdown.spreadSlippageBps),
|
|
341
|
+
executionMarketImpactBps: round(slippageBreakdown.marketImpactBps),
|
|
342
|
+
executionDelayRiskBps: stage === "entry" ? round(slippageBreakdown.delayRiskBps) : null
|
|
343
|
+
});
|
|
94
344
|
return {
|
|
95
345
|
__tradejsTestConnector: true,
|
|
346
|
+
capabilities: connector.capabilities,
|
|
347
|
+
universe: connector.universe,
|
|
348
|
+
accountId: connector.accountId,
|
|
349
|
+
deploymentId: connector.deploymentId,
|
|
350
|
+
listInstruments: (query) => connector.listInstruments(query),
|
|
351
|
+
getFundingRateHistory: connector.getFundingRateHistory ? (request) => connector.getFundingRateHistory(request) : void 0,
|
|
352
|
+
getTradingFeeRate: connector.getTradingFeeRate ? (symbol) => connector.getTradingFeeRate(symbol) : void 0,
|
|
96
353
|
getState: async () => state,
|
|
97
354
|
setState: async (newState) => {
|
|
98
355
|
state = {
|
|
@@ -103,15 +360,22 @@ var createTestConnector = (connector, context) => {
|
|
|
103
360
|
kline: async (options) => connector.kline(options),
|
|
104
361
|
getResult: async () => {
|
|
105
362
|
const orderLogId = randomUUID().slice(-12);
|
|
363
|
+
const fullStat = fastMode ? calculateStatsFull(positionLog) : null;
|
|
106
364
|
return {
|
|
107
|
-
stat: {
|
|
108
|
-
|
|
109
|
-
profit:
|
|
365
|
+
stat: fullStat ? {
|
|
366
|
+
...fullStat,
|
|
367
|
+
profit: fullStat.netProfit
|
|
368
|
+
} : {
|
|
369
|
+
amount: round(amount),
|
|
370
|
+
profit: round(amount - INITIAL_BACKTEST_AMOUNT),
|
|
110
371
|
orders: positionLog.length
|
|
111
372
|
},
|
|
112
373
|
orderLogId,
|
|
113
|
-
|
|
114
|
-
|
|
374
|
+
...executionCostModel ? { executionCostModel } : {},
|
|
375
|
+
...fastMode ? {} : {
|
|
376
|
+
inlineOrderLog: orderLog,
|
|
377
|
+
inlinePositionLog: positionLog
|
|
378
|
+
}
|
|
115
379
|
};
|
|
116
380
|
},
|
|
117
381
|
getPosition: async () => currentPosition || null,
|
|
@@ -137,6 +401,7 @@ var createTestConnector = (connector, context) => {
|
|
|
137
401
|
if (!candle || !currentPosition || !currentPosition.qty) {
|
|
138
402
|
return;
|
|
139
403
|
}
|
|
404
|
+
applyFunding(candle);
|
|
140
405
|
const isLong = currentPosition.direction === "LONG";
|
|
141
406
|
const entryPrice = currentPosition.price;
|
|
142
407
|
const high = candle.high;
|
|
@@ -146,47 +411,217 @@ var createTestConnector = (connector, context) => {
|
|
|
146
411
|
const targetPrice = tp.price;
|
|
147
412
|
const reached = isLong ? high >= targetPrice : low <= targetPrice;
|
|
148
413
|
if (reached) {
|
|
414
|
+
const exitTimestamp = getExitTimestamp(candle);
|
|
149
415
|
const qty = originalQty * tp.rate;
|
|
150
|
-
const
|
|
416
|
+
const slippageBreakdown = getExecutionSlippageBreakdown({
|
|
417
|
+
stage: "exit",
|
|
418
|
+
signal: currentPosition.signal
|
|
419
|
+
});
|
|
420
|
+
const executionPrice = applyExecutionSlippage({
|
|
421
|
+
price: targetPrice,
|
|
422
|
+
direction: currentPosition.direction,
|
|
423
|
+
stage: "exit",
|
|
424
|
+
signal: currentPosition.signal
|
|
425
|
+
});
|
|
426
|
+
const grossProfit = isLong ? (executionPrice - entryPrice) * qty : (entryPrice - executionPrice) * qty;
|
|
427
|
+
const { fee, profit } = getNetProfit({
|
|
428
|
+
grossProfit,
|
|
429
|
+
price: executionPrice,
|
|
430
|
+
qty
|
|
431
|
+
});
|
|
432
|
+
recordExitResult({
|
|
433
|
+
timestamp: exitTimestamp,
|
|
434
|
+
reason: "take_profit",
|
|
435
|
+
requestedPrice: targetPrice,
|
|
436
|
+
executionPrice,
|
|
437
|
+
qty,
|
|
438
|
+
grossProfit,
|
|
439
|
+
fee,
|
|
440
|
+
slippageBreakdown
|
|
441
|
+
});
|
|
151
442
|
amount += profit;
|
|
152
443
|
currentPositionProfit += profit;
|
|
153
444
|
currentPosition.qty = parseFloat(
|
|
154
445
|
(currentPosition.qty - qty).toFixed(8)
|
|
155
446
|
);
|
|
156
447
|
logOrder({
|
|
157
|
-
timestamp:
|
|
448
|
+
timestamp: exitTimestamp,
|
|
158
449
|
qty,
|
|
159
|
-
price:
|
|
450
|
+
price: executionPrice,
|
|
160
451
|
profit,
|
|
161
|
-
|
|
452
|
+
fee,
|
|
453
|
+
type: isLong ? "TAKE_PROFIT_LONG" : "TAKE_PROFIT_SHORT",
|
|
454
|
+
...getExecutionSlippageLogData(slippageBreakdown, "exit")
|
|
162
455
|
});
|
|
163
456
|
tp.done = true;
|
|
164
457
|
}
|
|
165
458
|
}
|
|
166
459
|
takeProfits = takeProfits.filter(({ done }) => !done);
|
|
167
460
|
if (currentPosition && currentPosition.qty <= 0) {
|
|
168
|
-
clearPosition(candle
|
|
461
|
+
clearPosition(getExitTimestamp(candle));
|
|
169
462
|
}
|
|
170
463
|
},
|
|
171
464
|
checkSl: async (candle) => {
|
|
172
465
|
if (!stopLossPrice || !currentPosition || !candle) {
|
|
173
466
|
return;
|
|
174
467
|
}
|
|
468
|
+
applyFunding(candle);
|
|
175
469
|
const isLong = currentPosition.direction === "LONG";
|
|
176
470
|
const hitStop = isLong ? candle.low <= stopLossPrice : candle.high >= stopLossPrice;
|
|
177
471
|
if (hitStop) {
|
|
472
|
+
const exitTimestamp = getExitTimestamp(candle);
|
|
178
473
|
const qty = currentPosition.qty;
|
|
179
|
-
const
|
|
474
|
+
const slippageBreakdown = getExecutionSlippageBreakdown({
|
|
475
|
+
stage: "exit",
|
|
476
|
+
signal: currentPosition.signal
|
|
477
|
+
});
|
|
478
|
+
const executionPrice = applyExecutionSlippage({
|
|
479
|
+
price: stopLossPrice,
|
|
480
|
+
direction: currentPosition.direction,
|
|
481
|
+
stage: "exit",
|
|
482
|
+
signal: currentPosition.signal
|
|
483
|
+
});
|
|
484
|
+
const grossProfit = isLong ? (executionPrice - currentPosition.price) * qty : (currentPosition.price - executionPrice) * qty;
|
|
485
|
+
const { fee, profit } = getNetProfit({
|
|
486
|
+
grossProfit,
|
|
487
|
+
price: executionPrice,
|
|
488
|
+
qty
|
|
489
|
+
});
|
|
490
|
+
recordExitResult({
|
|
491
|
+
timestamp: exitTimestamp,
|
|
492
|
+
reason: "stop_loss",
|
|
493
|
+
requestedPrice: stopLossPrice,
|
|
494
|
+
executionPrice,
|
|
495
|
+
qty,
|
|
496
|
+
grossProfit,
|
|
497
|
+
fee,
|
|
498
|
+
slippageBreakdown
|
|
499
|
+
});
|
|
180
500
|
amount += profit;
|
|
181
501
|
currentPositionProfit += profit;
|
|
182
502
|
logOrder({
|
|
183
|
-
timestamp:
|
|
503
|
+
timestamp: exitTimestamp,
|
|
184
504
|
qty,
|
|
185
505
|
profit,
|
|
186
|
-
price:
|
|
187
|
-
|
|
506
|
+
price: executionPrice,
|
|
507
|
+
fee,
|
|
508
|
+
type: isLong ? "STOP_LOSS_LONG" : "STOP_LOSS_SHORT",
|
|
509
|
+
...getExecutionSlippageLogData(slippageBreakdown, "exit")
|
|
188
510
|
});
|
|
189
|
-
clearPosition(
|
|
511
|
+
clearPosition(exitTimestamp);
|
|
512
|
+
}
|
|
513
|
+
},
|
|
514
|
+
checkExits: async (candle) => {
|
|
515
|
+
if (!candle || !currentPosition) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
applyFunding(candle);
|
|
519
|
+
if (stopLossPrice) {
|
|
520
|
+
const isLong2 = currentPosition.direction === "LONG";
|
|
521
|
+
const hitStop = isLong2 ? candle.low <= stopLossPrice : candle.high >= stopLossPrice;
|
|
522
|
+
if (hitStop) {
|
|
523
|
+
const exitTimestamp = getExitTimestamp(candle);
|
|
524
|
+
const qty = currentPosition.qty;
|
|
525
|
+
const slippageBreakdown = getExecutionSlippageBreakdown({
|
|
526
|
+
stage: "exit",
|
|
527
|
+
signal: currentPosition.signal
|
|
528
|
+
});
|
|
529
|
+
const executionPrice = applyExecutionSlippage({
|
|
530
|
+
price: stopLossPrice,
|
|
531
|
+
direction: currentPosition.direction,
|
|
532
|
+
stage: "exit",
|
|
533
|
+
signal: currentPosition.signal
|
|
534
|
+
});
|
|
535
|
+
const grossProfit = isLong2 ? (executionPrice - currentPosition.price) * qty : (currentPosition.price - executionPrice) * qty;
|
|
536
|
+
const { fee, profit } = getNetProfit({
|
|
537
|
+
grossProfit,
|
|
538
|
+
price: executionPrice,
|
|
539
|
+
qty
|
|
540
|
+
});
|
|
541
|
+
recordExitResult({
|
|
542
|
+
timestamp: exitTimestamp,
|
|
543
|
+
reason: "stop_loss",
|
|
544
|
+
requestedPrice: stopLossPrice,
|
|
545
|
+
executionPrice,
|
|
546
|
+
qty,
|
|
547
|
+
grossProfit,
|
|
548
|
+
fee,
|
|
549
|
+
slippageBreakdown
|
|
550
|
+
});
|
|
551
|
+
amount += profit;
|
|
552
|
+
currentPositionProfit += profit;
|
|
553
|
+
logOrder({
|
|
554
|
+
timestamp: exitTimestamp,
|
|
555
|
+
qty,
|
|
556
|
+
profit,
|
|
557
|
+
price: executionPrice,
|
|
558
|
+
fee,
|
|
559
|
+
type: isLong2 ? "STOP_LOSS_LONG" : "STOP_LOSS_SHORT",
|
|
560
|
+
...getExecutionSlippageLogData(slippageBreakdown, "exit")
|
|
561
|
+
});
|
|
562
|
+
clearPosition(exitTimestamp);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (!currentPosition || !currentPosition.qty) {
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const isLong = currentPosition.direction === "LONG";
|
|
569
|
+
const entryPrice = currentPosition.price;
|
|
570
|
+
const high = candle.high;
|
|
571
|
+
const low = candle.low;
|
|
572
|
+
for (const tp of takeProfits) {
|
|
573
|
+
if (!currentPosition || currentPosition.qty <= 0) break;
|
|
574
|
+
const targetPrice = tp.price;
|
|
575
|
+
const reached = isLong ? high >= targetPrice : low <= targetPrice;
|
|
576
|
+
if (reached) {
|
|
577
|
+
const exitTimestamp = getExitTimestamp(candle);
|
|
578
|
+
const qty = originalQty * tp.rate;
|
|
579
|
+
const slippageBreakdown = getExecutionSlippageBreakdown({
|
|
580
|
+
stage: "exit",
|
|
581
|
+
signal: currentPosition.signal
|
|
582
|
+
});
|
|
583
|
+
const executionPrice = applyExecutionSlippage({
|
|
584
|
+
price: targetPrice,
|
|
585
|
+
direction: currentPosition.direction,
|
|
586
|
+
stage: "exit",
|
|
587
|
+
signal: currentPosition.signal
|
|
588
|
+
});
|
|
589
|
+
const grossProfit = isLong ? (executionPrice - entryPrice) * qty : (entryPrice - executionPrice) * qty;
|
|
590
|
+
const { fee, profit } = getNetProfit({
|
|
591
|
+
grossProfit,
|
|
592
|
+
price: executionPrice,
|
|
593
|
+
qty
|
|
594
|
+
});
|
|
595
|
+
recordExitResult({
|
|
596
|
+
timestamp: exitTimestamp,
|
|
597
|
+
reason: "take_profit",
|
|
598
|
+
requestedPrice: targetPrice,
|
|
599
|
+
executionPrice,
|
|
600
|
+
qty,
|
|
601
|
+
grossProfit,
|
|
602
|
+
fee,
|
|
603
|
+
slippageBreakdown
|
|
604
|
+
});
|
|
605
|
+
amount += profit;
|
|
606
|
+
currentPositionProfit += profit;
|
|
607
|
+
currentPosition.qty = parseFloat(
|
|
608
|
+
(currentPosition.qty - qty).toFixed(8)
|
|
609
|
+
);
|
|
610
|
+
logOrder({
|
|
611
|
+
timestamp: exitTimestamp,
|
|
612
|
+
qty,
|
|
613
|
+
price: executionPrice,
|
|
614
|
+
profit,
|
|
615
|
+
fee,
|
|
616
|
+
type: isLong ? "TAKE_PROFIT_LONG" : "TAKE_PROFIT_SHORT",
|
|
617
|
+
...getExecutionSlippageLogData(slippageBreakdown, "exit")
|
|
618
|
+
});
|
|
619
|
+
tp.done = true;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
takeProfits = takeProfits.filter(({ done }) => !done);
|
|
623
|
+
if (currentPosition && currentPosition.qty <= 0) {
|
|
624
|
+
clearPosition(getExitTimestamp(candle));
|
|
190
625
|
}
|
|
191
626
|
},
|
|
192
627
|
placeOrder: async (order) => {
|
|
@@ -194,17 +629,77 @@ var createTestConnector = (connector, context) => {
|
|
|
194
629
|
return false;
|
|
195
630
|
}
|
|
196
631
|
const isLong = order.direction === "LONG";
|
|
197
|
-
|
|
632
|
+
const entrySlippageBreakdown = getExecutionSlippageBreakdown({
|
|
633
|
+
stage: "entry",
|
|
634
|
+
signal: order.signal
|
|
635
|
+
});
|
|
636
|
+
const entryPrice = applyExecutionSlippage({
|
|
637
|
+
price: order.price,
|
|
638
|
+
direction: order.direction,
|
|
639
|
+
stage: "entry",
|
|
640
|
+
signal: order.signal
|
|
641
|
+
});
|
|
642
|
+
currentPosition = { ...order, price: entryPrice, amount };
|
|
643
|
+
currentSignalId = typeof order.signal?.signalId === "string" && order.signal.signalId ? order.signal.signalId : null;
|
|
198
644
|
originalQty = order.qty;
|
|
199
|
-
const fee
|
|
200
|
-
|
|
645
|
+
const { fee, profit } = getNetProfit({
|
|
646
|
+
grossProfit: 0,
|
|
647
|
+
price: entryPrice,
|
|
648
|
+
qty: order.qty,
|
|
649
|
+
feeRate: order.isLimit ? makerFeeRate : takerFeeRate
|
|
650
|
+
});
|
|
651
|
+
const entrySlippageCost = getSlippageCost({
|
|
652
|
+
requestedPrice: order.price,
|
|
653
|
+
executionPrice: entryPrice,
|
|
654
|
+
direction: order.direction,
|
|
655
|
+
stage: "entry",
|
|
656
|
+
qty: order.qty
|
|
657
|
+
});
|
|
201
658
|
amount += profit;
|
|
202
659
|
currentPositionProfit = profit;
|
|
660
|
+
currentTradeResult = currentSignalId ? {
|
|
661
|
+
signalId: currentSignalId,
|
|
662
|
+
direction: order.direction,
|
|
663
|
+
qty: order.qty,
|
|
664
|
+
closedQty: 0,
|
|
665
|
+
entryTimestamp: order.timestamp,
|
|
666
|
+
exitTimestamp: null,
|
|
667
|
+
exitReason: null,
|
|
668
|
+
requestedEntryPrice: order.price,
|
|
669
|
+
entryPrice,
|
|
670
|
+
requestedExitPrice: null,
|
|
671
|
+
exitPrice: null,
|
|
672
|
+
grossProfit: 0,
|
|
673
|
+
netProfit: profit,
|
|
674
|
+
openFee: fee,
|
|
675
|
+
closeFee: 0,
|
|
676
|
+
fundingFee: executionCostModel?.funding.enabled ? 0 : null,
|
|
677
|
+
totalFee: fee,
|
|
678
|
+
entrySlippagePrice: entryPrice - order.price,
|
|
679
|
+
entrySlippageBps: getSlippageBps(order.price, entryPrice),
|
|
680
|
+
entryBaseSlippageBps: entrySlippageBreakdown.baseSlippageBps,
|
|
681
|
+
entrySpreadBps: entrySlippageBreakdown.spreadBps,
|
|
682
|
+
entrySpreadSlippageBps: entrySlippageBreakdown.spreadSlippageBps,
|
|
683
|
+
entryMarketImpactBps: entrySlippageBreakdown.marketImpactBps,
|
|
684
|
+
entryDelayRiskBps: entrySlippageBreakdown.delayRiskBps,
|
|
685
|
+
entrySlippageCost,
|
|
686
|
+
exitSlippagePrice: null,
|
|
687
|
+
exitSlippageBps: null,
|
|
688
|
+
exitBaseSlippageBps: null,
|
|
689
|
+
exitSpreadBps: null,
|
|
690
|
+
exitSpreadSlippageBps: null,
|
|
691
|
+
exitMarketImpactBps: null,
|
|
692
|
+
exitDelayRiskBps: null,
|
|
693
|
+
exitSlippageCost: 0,
|
|
694
|
+
totalSlippageCost: entrySlippageCost
|
|
695
|
+
} : null;
|
|
203
696
|
logOrder({
|
|
204
697
|
...order,
|
|
698
|
+
price: entryPrice,
|
|
205
699
|
profit,
|
|
206
700
|
fee,
|
|
207
|
-
type: isLong ? "OPEN_LONG" : "OPEN_SHORT"
|
|
701
|
+
type: isLong ? "OPEN_LONG" : "OPEN_SHORT",
|
|
702
|
+
...getExecutionSlippageLogData(entrySlippageBreakdown, "entry")
|
|
208
703
|
});
|
|
209
704
|
return true;
|
|
210
705
|
},
|
|
@@ -231,14 +726,42 @@ var createTestConnector = (connector, context) => {
|
|
|
231
726
|
return false;
|
|
232
727
|
}
|
|
233
728
|
const isLong = currentPosition.direction === "LONG";
|
|
234
|
-
const
|
|
729
|
+
const slippageBreakdown = getExecutionSlippageBreakdown({
|
|
730
|
+
stage: "exit",
|
|
731
|
+
signal: currentPosition.signal
|
|
732
|
+
});
|
|
733
|
+
const executionPrice = applyExecutionSlippage({
|
|
734
|
+
price: order.price,
|
|
735
|
+
direction: currentPosition.direction,
|
|
736
|
+
stage: "exit",
|
|
737
|
+
signal: currentPosition.signal
|
|
738
|
+
});
|
|
739
|
+
const grossProfit = isLong ? (executionPrice - currentPosition.price) * currentPosition.qty : (currentPosition.price - executionPrice) * currentPosition.qty;
|
|
740
|
+
const { fee, profit } = getNetProfit({
|
|
741
|
+
grossProfit,
|
|
742
|
+
price: executionPrice,
|
|
743
|
+
qty: currentPosition.qty
|
|
744
|
+
});
|
|
745
|
+
recordExitResult({
|
|
746
|
+
timestamp: order.timestamp,
|
|
747
|
+
reason: "exit",
|
|
748
|
+
requestedPrice: order.price,
|
|
749
|
+
executionPrice,
|
|
750
|
+
qty: currentPosition.qty,
|
|
751
|
+
grossProfit,
|
|
752
|
+
fee,
|
|
753
|
+
slippageBreakdown
|
|
754
|
+
});
|
|
235
755
|
amount += profit;
|
|
236
756
|
currentPositionProfit += profit;
|
|
237
757
|
logOrder({
|
|
238
758
|
...order,
|
|
759
|
+
price: executionPrice,
|
|
239
760
|
qty: currentPosition.qty,
|
|
240
761
|
profit,
|
|
241
|
-
|
|
762
|
+
fee,
|
|
763
|
+
type: isLong ? "CLOSE_LONG" : "CLOSE_SHORT",
|
|
764
|
+
...getExecutionSlippageLogData(slippageBreakdown, "exit")
|
|
242
765
|
});
|
|
243
766
|
clearPosition(order.timestamp);
|
|
244
767
|
return true;
|
|
@@ -249,10 +772,200 @@ var createTestConnector = (connector, context) => {
|
|
|
249
772
|
};
|
|
250
773
|
};
|
|
251
774
|
|
|
775
|
+
// src/executionCosts.ts
|
|
776
|
+
import {
|
|
777
|
+
BACKTEST_BASE_SLIPPAGE_BPS,
|
|
778
|
+
BACKTEST_DELAY_RISK_MULTIPLIER,
|
|
779
|
+
BACKTEST_MARKET_IMPACT_BPS,
|
|
780
|
+
BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER,
|
|
781
|
+
FEE_PERCENT as FEE_PERCENT2
|
|
782
|
+
} from "@tradejs/core/constants";
|
|
783
|
+
var finiteOr = (value, fallback) => {
|
|
784
|
+
const parsed = Number(value);
|
|
785
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
786
|
+
};
|
|
787
|
+
var feeCache = /* @__PURE__ */ new WeakMap();
|
|
788
|
+
var fundingCache = /* @__PURE__ */ new WeakMap();
|
|
789
|
+
var loadTradingFee = async (connector, symbol) => {
|
|
790
|
+
if (!connector.getTradingFeeRate) return null;
|
|
791
|
+
let cache = feeCache.get(connector);
|
|
792
|
+
if (!cache) {
|
|
793
|
+
cache = /* @__PURE__ */ new Map();
|
|
794
|
+
feeCache.set(connector, cache);
|
|
795
|
+
}
|
|
796
|
+
const key = symbol.toUpperCase();
|
|
797
|
+
const cached = cache.get(key);
|
|
798
|
+
if (cached) return cached;
|
|
799
|
+
const rate = await connector.getTradingFeeRate(symbol);
|
|
800
|
+
cache.set(key, rate);
|
|
801
|
+
return rate;
|
|
802
|
+
};
|
|
803
|
+
var loadFundingRates = (connector, symbol, startTime, endTime) => {
|
|
804
|
+
if (!connector.getFundingRateHistory) return Promise.resolve([]);
|
|
805
|
+
let cache = fundingCache.get(connector);
|
|
806
|
+
if (!cache) {
|
|
807
|
+
cache = /* @__PURE__ */ new Map();
|
|
808
|
+
fundingCache.set(connector, cache);
|
|
809
|
+
}
|
|
810
|
+
const key = `${symbol.toUpperCase()}:${startTime}:${endTime}`;
|
|
811
|
+
const cached = cache.get(key);
|
|
812
|
+
if (cached) return cached;
|
|
813
|
+
const pending = connector.getFundingRateHistory({ symbol, startTime, endTime }).catch(() => []);
|
|
814
|
+
cache.set(key, pending);
|
|
815
|
+
return pending;
|
|
816
|
+
};
|
|
817
|
+
var resolveExecutionCosts = async (params) => {
|
|
818
|
+
const { connector, symbol, config, startTime, endTime, instrument } = params;
|
|
819
|
+
const cacheOnly = config.EXECUTION_COSTS_CACHE_ONLY === true;
|
|
820
|
+
const hasConfiguredFees = Number.isFinite(Number(config.MAKER_FEE_RATE)) && Number.isFinite(Number(config.TAKER_FEE_RATE));
|
|
821
|
+
const exchangeFees = !cacheOnly && !hasConfiguredFees && connector.getTradingFeeRate ? await loadTradingFee(connector, symbol).catch(() => null) : null;
|
|
822
|
+
const makerRate = hasConfiguredFees ? Number(config.MAKER_FEE_RATE) : exchangeFees?.makerRate ?? FEE_PERCENT2;
|
|
823
|
+
const takerRate = hasConfiguredFees ? Number(config.TAKER_FEE_RATE) : exchangeFees?.takerRate ?? FEE_PERCENT2;
|
|
824
|
+
const fundingEnabled = config.FUNDING_ENABLED !== false && !cacheOnly && typeof connector.getFundingRateHistory === "function";
|
|
825
|
+
const fundingRates = fundingEnabled ? await loadFundingRates(connector, symbol, startTime, endTime) : [];
|
|
826
|
+
const requestedLeverage = Math.max(1, finiteOr(config.LEVERAGE, 10));
|
|
827
|
+
const venueMaxLeverage = Number(instrument?.venueMetadata?.maxLeverage);
|
|
828
|
+
const maxAllowed = Number.isFinite(venueMaxLeverage) ? venueMaxLeverage : null;
|
|
829
|
+
const effectiveLeverage = maxAllowed == null ? requestedLeverage : Math.min(requestedLeverage, maxAllowed);
|
|
830
|
+
const feeSource = hasConfiguredFees ? "config" : exchangeFees?.source ?? "fallback";
|
|
831
|
+
const fundingSource = !fundingEnabled ? cacheOnly ? "fallback" : "disabled" : fundingRates.length ? "historical" : "unavailable";
|
|
832
|
+
const usesFallback = feeSource === "fallback" || fundingEnabled && fundingSource === "unavailable" || config.SLIPPAGE_BASE_BPS == null && config.SLIPPAGE_SPREAD_MULTIPLIER == null && config.SLIPPAGE_MARKET_IMPACT_BPS == null;
|
|
833
|
+
return {
|
|
834
|
+
model: {
|
|
835
|
+
fees: { makerRate, takerRate, source: feeSource },
|
|
836
|
+
funding: {
|
|
837
|
+
enabled: fundingEnabled,
|
|
838
|
+
source: fundingSource,
|
|
839
|
+
points: fundingRates.length,
|
|
840
|
+
fromTimestamp: fundingRates[0]?.timestamp ?? null,
|
|
841
|
+
toTimestamp: fundingRates.at(-1)?.timestamp ?? null
|
|
842
|
+
},
|
|
843
|
+
slippage: {
|
|
844
|
+
baseBps: finiteOr(config.SLIPPAGE_BASE_BPS, BACKTEST_BASE_SLIPPAGE_BPS),
|
|
845
|
+
spreadMultiplier: finiteOr(
|
|
846
|
+
config.SLIPPAGE_SPREAD_MULTIPLIER,
|
|
847
|
+
BACKTEST_SPREAD_SLIPPAGE_MULTIPLIER
|
|
848
|
+
),
|
|
849
|
+
marketImpactBps: finiteOr(
|
|
850
|
+
config.SLIPPAGE_MARKET_IMPACT_BPS,
|
|
851
|
+
BACKTEST_MARKET_IMPACT_BPS
|
|
852
|
+
),
|
|
853
|
+
delayRiskMultiplier: finiteOr(
|
|
854
|
+
config.SLIPPAGE_DELAY_RISK_MULTIPLIER,
|
|
855
|
+
BACKTEST_DELAY_RISK_MULTIPLIER
|
|
856
|
+
),
|
|
857
|
+
source: config.SLIPPAGE_BASE_BPS != null || config.SLIPPAGE_SPREAD_MULTIPLIER != null || config.SLIPPAGE_MARKET_IMPACT_BPS != null ? "config" : "fallback"
|
|
858
|
+
},
|
|
859
|
+
leverage: {
|
|
860
|
+
requested: requestedLeverage,
|
|
861
|
+
effective: effectiveLeverage,
|
|
862
|
+
maxAllowed
|
|
863
|
+
},
|
|
864
|
+
quality: usesFallback ? "fallback" : fundingEnabled && fundingRates.length ? "full" : "partial",
|
|
865
|
+
capturedAt: Date.now()
|
|
866
|
+
},
|
|
867
|
+
fundingRates
|
|
868
|
+
};
|
|
869
|
+
};
|
|
870
|
+
|
|
252
871
|
// src/testing.ts
|
|
872
|
+
var isBacktestEntryDelayControlCode = (value) => typeof value === "string" && value.startsWith("BACKTEST_ENTRY_DELAY_");
|
|
873
|
+
var CLOSED_RESULT_FLUSH_INTERVAL = 500;
|
|
874
|
+
var DEFAULT_STRATEGY_CANDLE_TIMEOUT_MS = 6e4;
|
|
875
|
+
var resolvePositiveInt = (value, fallback) => {
|
|
876
|
+
const parsed = parseInt(String(value ?? ""), 10);
|
|
877
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
878
|
+
};
|
|
879
|
+
var getStrategyCandleTimeoutMs = () => resolvePositiveInt(
|
|
880
|
+
process.env.BACKTEST_STRATEGY_CANDLE_TIMEOUT_MS,
|
|
881
|
+
DEFAULT_STRATEGY_CANDLE_TIMEOUT_MS
|
|
882
|
+
);
|
|
883
|
+
var getEffectiveTimeoutMs = (baseTimeoutMs, stageTimeoutMs) => {
|
|
884
|
+
const base = baseTimeoutMs && baseTimeoutMs > 0 ? Math.trunc(baseTimeoutMs) : null;
|
|
885
|
+
const stage = stageTimeoutMs && stageTimeoutMs > 0 ? Math.trunc(stageTimeoutMs) : null;
|
|
886
|
+
if (base == null) return stage;
|
|
887
|
+
if (stage == null) return base;
|
|
888
|
+
return Math.min(base, stage);
|
|
889
|
+
};
|
|
890
|
+
var buildCandleByTimestamp = (candles) => new Map(
|
|
891
|
+
(candles ?? []).filter((candle) => typeof candle?.timestamp === "number").map((candle) => [candle.timestamp, candle])
|
|
892
|
+
);
|
|
893
|
+
var buildBacktestDatasetMetadata = ({
|
|
894
|
+
backtestRunId,
|
|
895
|
+
backtestTestKey,
|
|
896
|
+
chunkId
|
|
897
|
+
}) => {
|
|
898
|
+
if (!backtestRunId || !backtestTestKey || !chunkId) {
|
|
899
|
+
return {};
|
|
900
|
+
}
|
|
901
|
+
return {
|
|
902
|
+
backtestRunId,
|
|
903
|
+
backtestTestKey,
|
|
904
|
+
backtestChunkId: chunkId
|
|
905
|
+
};
|
|
906
|
+
};
|
|
907
|
+
var cloneAiPayloadSignal = (signal) => {
|
|
908
|
+
const cloneValue = (value) => {
|
|
909
|
+
if (value == null) {
|
|
910
|
+
return value;
|
|
911
|
+
}
|
|
912
|
+
if (typeof structuredClone === "function") {
|
|
913
|
+
return structuredClone(value);
|
|
914
|
+
}
|
|
915
|
+
return JSON.parse(JSON.stringify(value));
|
|
916
|
+
};
|
|
917
|
+
return {
|
|
918
|
+
...signal,
|
|
919
|
+
figures: cloneValue(signal.figures),
|
|
920
|
+
indicators: cloneValue(signal.indicators),
|
|
921
|
+
additionalIndicators: cloneValue(signal.additionalIndicators)
|
|
922
|
+
};
|
|
923
|
+
};
|
|
924
|
+
var buildReplaySignalEvaluationRecord = ({
|
|
925
|
+
signal,
|
|
926
|
+
testId,
|
|
927
|
+
userName,
|
|
928
|
+
strategyName,
|
|
929
|
+
symbol,
|
|
930
|
+
interval,
|
|
931
|
+
candle
|
|
932
|
+
}) => {
|
|
933
|
+
if (!signal || typeof signal === "string") {
|
|
934
|
+
return {
|
|
935
|
+
evaluationId: `${testId}:${strategyName}:${symbol}:${candle.timestamp}`,
|
|
936
|
+
userName,
|
|
937
|
+
strategy: strategyName,
|
|
938
|
+
symbol,
|
|
939
|
+
interval,
|
|
940
|
+
timestamp: candle.timestamp,
|
|
941
|
+
evaluatedAt: candle.timestamp,
|
|
942
|
+
status: "skip",
|
|
943
|
+
reason: typeof signal === "string" && signal.trim() ? signal : "NO_SIGNAL"
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
const signalTimestamp = typeof signal.timestamp === "number" && Number.isFinite(signal.timestamp) ? signal.timestamp : candle.timestamp;
|
|
947
|
+
return {
|
|
948
|
+
evaluationId: `${signal.signalId || testId}:${strategyName}:${symbol}:${signalTimestamp}`,
|
|
949
|
+
userName,
|
|
950
|
+
strategy: signal.strategy || strategyName,
|
|
951
|
+
symbol: signal.symbol || symbol,
|
|
952
|
+
interval: signal.interval || interval,
|
|
953
|
+
timestamp: signalTimestamp,
|
|
954
|
+
evaluatedAt: candle.timestamp,
|
|
955
|
+
status: "signal",
|
|
956
|
+
reason: signal.orderSkipReason || signal.orderStatus,
|
|
957
|
+
signalId: signal.signalId,
|
|
958
|
+
direction: signal.direction,
|
|
959
|
+
orderStatus: signal.orderStatus,
|
|
960
|
+
orderSkipReason: signal.orderSkipReason,
|
|
961
|
+
aiAnalysis: signal.aiAnalysis ?? null,
|
|
962
|
+
ml: signal.ml
|
|
963
|
+
};
|
|
964
|
+
};
|
|
253
965
|
var createTestingKlineCacheState = () => ({
|
|
254
966
|
coinKlineCache: /* @__PURE__ */ new Map(),
|
|
255
967
|
btcKlineCache: /* @__PURE__ */ new Map(),
|
|
968
|
+
ethKlineCache: /* @__PURE__ */ new Map(),
|
|
256
969
|
btcBinanceKlineCache: /* @__PURE__ */ new Map(),
|
|
257
970
|
btcCoinbaseKlineCache: /* @__PURE__ */ new Map(),
|
|
258
971
|
preparedDataCache: /* @__PURE__ */ new Map(),
|
|
@@ -279,11 +992,15 @@ var getKlineCacheKey = (params) => {
|
|
|
279
992
|
preloadStart,
|
|
280
993
|
end,
|
|
281
994
|
interval,
|
|
282
|
-
cacheOnly
|
|
995
|
+
cacheOnly,
|
|
996
|
+
universe,
|
|
997
|
+
accountId
|
|
283
998
|
} = params;
|
|
284
999
|
return [
|
|
285
1000
|
userName,
|
|
286
1001
|
connectorName,
|
|
1002
|
+
universe ?? "crypto",
|
|
1003
|
+
accountId ?? "default",
|
|
287
1004
|
symbol,
|
|
288
1005
|
preloadStart,
|
|
289
1006
|
end,
|
|
@@ -301,21 +1018,47 @@ var getPreparedDataCacheKey = (params) => {
|
|
|
301
1018
|
end,
|
|
302
1019
|
interval,
|
|
303
1020
|
btcBinanceConnectorName,
|
|
304
|
-
btcCoinbaseConnectorName
|
|
1021
|
+
btcCoinbaseConnectorName,
|
|
1022
|
+
backtestExecutionInterval,
|
|
1023
|
+
universe,
|
|
1024
|
+
accountId
|
|
305
1025
|
} = params;
|
|
306
1026
|
return [
|
|
307
1027
|
userName,
|
|
308
1028
|
connectorName,
|
|
1029
|
+
universe ?? "crypto",
|
|
1030
|
+
accountId ?? "default",
|
|
309
1031
|
symbol,
|
|
310
1032
|
preloadStart,
|
|
311
1033
|
start,
|
|
312
1034
|
end,
|
|
313
1035
|
interval,
|
|
1036
|
+
backtestExecutionInterval,
|
|
314
1037
|
btcBinanceConnectorName,
|
|
315
1038
|
btcCoinbaseConnectorName
|
|
316
1039
|
].join(":");
|
|
317
1040
|
};
|
|
318
|
-
var getConnectorCacheKey = (params) => [
|
|
1041
|
+
var getConnectorCacheKey = (params) => [
|
|
1042
|
+
params.userName,
|
|
1043
|
+
params.connectorName,
|
|
1044
|
+
params.universe ?? "crypto",
|
|
1045
|
+
params.accountId ?? "default"
|
|
1046
|
+
].join(":");
|
|
1047
|
+
var BACKTEST_INTERVAL = "15";
|
|
1048
|
+
var resolveBacktestExecutionInterval = (interval) => {
|
|
1049
|
+
const normalized = String(interval);
|
|
1050
|
+
if (normalized === "15") {
|
|
1051
|
+
return BACKTEST_EXECUTION_INTERVAL;
|
|
1052
|
+
}
|
|
1053
|
+
if (normalized === "60") {
|
|
1054
|
+
return "15";
|
|
1055
|
+
}
|
|
1056
|
+
return null;
|
|
1057
|
+
};
|
|
1058
|
+
var resolveIntervalMs = (interval) => {
|
|
1059
|
+
const intervalMinutes = Number(interval);
|
|
1060
|
+
return Number.isFinite(intervalMinutes) && intervalMinutes > 0 ? intervalMinutes * 6e4 : Number(BACKTEST_INTERVAL) * 6e4;
|
|
1061
|
+
};
|
|
319
1062
|
var splitCandlesForTesting = (candles, start, preloadStart) => {
|
|
320
1063
|
const prevData = [];
|
|
321
1064
|
const testData = [];
|
|
@@ -329,9 +1072,56 @@ var splitCandlesForTesting = (candles, start, preloadStart) => {
|
|
|
329
1072
|
}
|
|
330
1073
|
return { prevData, testData };
|
|
331
1074
|
};
|
|
1075
|
+
var getCurrentOpenTimestamp = (interval) => {
|
|
1076
|
+
const intervalMs = resolveIntervalMs(interval);
|
|
1077
|
+
return Math.floor(Date.now() / intervalMs) * intervalMs;
|
|
1078
|
+
};
|
|
1079
|
+
var filterClosedAlignedCandles = (data, btcData, interval) => {
|
|
1080
|
+
const currentOpenTimestamp = getCurrentOpenTimestamp(interval);
|
|
1081
|
+
const closedData = [];
|
|
1082
|
+
const closedBtcData = [];
|
|
1083
|
+
for (let index = 0; index < data.length; index += 1) {
|
|
1084
|
+
const candle = data[index];
|
|
1085
|
+
const btcCandle = btcData[index];
|
|
1086
|
+
if (!candle || !btcCandle || candle.timestamp >= currentOpenTimestamp) {
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
closedData.push(candle);
|
|
1090
|
+
closedBtcData.push(btcCandle);
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
data: closedData,
|
|
1094
|
+
btcData: closedBtcData
|
|
1095
|
+
};
|
|
1096
|
+
};
|
|
1097
|
+
var shouldLoadBacktestExecutionCandles = (interval) => {
|
|
1098
|
+
if (!BACKTEST_LOWER_TIMEFRAME_EXECUTION_ENABLED) {
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
const executionInterval = resolveBacktestExecutionInterval(interval);
|
|
1102
|
+
if (!executionInterval) {
|
|
1103
|
+
return false;
|
|
1104
|
+
}
|
|
1105
|
+
const primaryMs = resolveIntervalMs(interval);
|
|
1106
|
+
const executionMs = resolveIntervalMs(executionInterval);
|
|
1107
|
+
return executionMs > 0 && executionMs < primaryMs;
|
|
1108
|
+
};
|
|
332
1109
|
var getCachedConnector = async (params) => {
|
|
333
|
-
const {
|
|
334
|
-
|
|
1110
|
+
const {
|
|
1111
|
+
state,
|
|
1112
|
+
projectRoot,
|
|
1113
|
+
userName,
|
|
1114
|
+
connectorName,
|
|
1115
|
+
universe,
|
|
1116
|
+
accountId,
|
|
1117
|
+
deploymentId
|
|
1118
|
+
} = params;
|
|
1119
|
+
const cacheKey = getConnectorCacheKey({
|
|
1120
|
+
userName,
|
|
1121
|
+
connectorName,
|
|
1122
|
+
universe,
|
|
1123
|
+
accountId
|
|
1124
|
+
});
|
|
335
1125
|
const cachedConnector = state.connectorCache.get(cacheKey);
|
|
336
1126
|
if (cachedConnector) {
|
|
337
1127
|
return cachedConnector;
|
|
@@ -344,7 +1134,10 @@ var getCachedConnector = async (params) => {
|
|
|
344
1134
|
return void 0;
|
|
345
1135
|
}
|
|
346
1136
|
const connector = await connectorCreator({
|
|
347
|
-
userName
|
|
1137
|
+
userName,
|
|
1138
|
+
universe,
|
|
1139
|
+
accountId,
|
|
1140
|
+
deploymentId
|
|
348
1141
|
});
|
|
349
1142
|
state.connectorCache.set(cacheKey, connector);
|
|
350
1143
|
return connector;
|
|
@@ -359,107 +1152,64 @@ var resetTestingKlineCache = (cwd) => {
|
|
|
359
1152
|
getTradejsProjectCwd(normalizedCwd)
|
|
360
1153
|
);
|
|
361
1154
|
};
|
|
362
|
-
var
|
|
363
|
-
userName,
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
connectorName,
|
|
372
|
-
ml = false,
|
|
373
|
-
ai = false,
|
|
374
|
-
chunkId = "single",
|
|
375
|
-
timeoutMs
|
|
376
|
-
}) => {
|
|
377
|
-
if (!start) {
|
|
378
|
-
throw new Error("no start");
|
|
379
|
-
}
|
|
380
|
-
const preloadStart = getBacktestPreloadStart(start);
|
|
381
|
-
const startedAt = Date.now();
|
|
382
|
-
const formatTimeoutMessage = (stage) => `Test ${name} (${symbol}) timed out after ${timeoutMs}ms during ${stage}`;
|
|
383
|
-
const getRemainingTimeoutMs = (stage) => {
|
|
384
|
-
if (!timeoutMs || timeoutMs <= 0) {
|
|
385
|
-
return null;
|
|
386
|
-
}
|
|
387
|
-
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
388
|
-
if (remainingMs <= 0) {
|
|
389
|
-
throw new Error(formatTimeoutMessage(stage));
|
|
390
|
-
}
|
|
391
|
-
return remainingMs;
|
|
392
|
-
};
|
|
393
|
-
const throwIfTimedOut = (stage) => {
|
|
394
|
-
getRemainingTimeoutMs(stage);
|
|
395
|
-
};
|
|
396
|
-
const withTimeout = async (stage, promise) => {
|
|
397
|
-
const remainingMs = getRemainingTimeoutMs(stage);
|
|
398
|
-
if (remainingMs == null) {
|
|
399
|
-
return promise;
|
|
1155
|
+
var releaseTestingSymbolCache = (params) => {
|
|
1156
|
+
const { cwd, userName, connectorName, symbol } = params;
|
|
1157
|
+
const { state } = getTestingKlineCacheState(cwd);
|
|
1158
|
+
const connectorPrefix = [userName, connectorName].join(":") + ":";
|
|
1159
|
+
for (const cache of [state.coinKlineCache, state.preparedDataCache]) {
|
|
1160
|
+
for (const key of cache.keys()) {
|
|
1161
|
+
if (key.startsWith(connectorPrefix) && key.split(":").includes(symbol)) {
|
|
1162
|
+
cache.delete(key);
|
|
1163
|
+
}
|
|
400
1164
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
var prepareTestingData = async (params) => {
|
|
1168
|
+
const {
|
|
1169
|
+
state,
|
|
1170
|
+
projectRoot,
|
|
1171
|
+
userName,
|
|
1172
|
+
connectorName,
|
|
1173
|
+
symbol,
|
|
1174
|
+
preloadStart,
|
|
1175
|
+
start,
|
|
1176
|
+
end,
|
|
1177
|
+
interval,
|
|
1178
|
+
universe = "crypto",
|
|
1179
|
+
accountId,
|
|
1180
|
+
deploymentId
|
|
1181
|
+
} = params;
|
|
1182
|
+
const binanceConnector = universe === "crypto" ? await getCachedConnector({
|
|
1183
|
+
state,
|
|
1184
|
+
projectRoot,
|
|
1185
|
+
userName,
|
|
1186
|
+
connectorName: BUILTIN_CONNECTOR_NAMES.Binance
|
|
1187
|
+
}) : void 0;
|
|
1188
|
+
const coinbaseConnector = universe === "crypto" ? await getCachedConnector({
|
|
1189
|
+
state,
|
|
1190
|
+
projectRoot,
|
|
1191
|
+
userName,
|
|
1192
|
+
connectorName: BUILTIN_CONNECTOR_NAMES.Coinbase
|
|
1193
|
+
}) : void 0;
|
|
1194
|
+
const cacheOnly = true;
|
|
1195
|
+
const connector = await getCachedConnector({
|
|
1196
|
+
state,
|
|
1197
|
+
projectRoot,
|
|
1198
|
+
userName,
|
|
1199
|
+
connectorName,
|
|
1200
|
+
universe,
|
|
1201
|
+
accountId,
|
|
1202
|
+
deploymentId
|
|
1203
|
+
});
|
|
427
1204
|
if (!connector) {
|
|
428
1205
|
throw new Error(`Unknown connector: ${connectorName}`);
|
|
429
1206
|
}
|
|
430
|
-
const strategyCreator = await withTimeout(
|
|
431
|
-
"strategy lookup",
|
|
432
|
-
getStrategyCreator(strategyName, projectRoot)
|
|
433
|
-
);
|
|
434
|
-
if (!strategyCreator) {
|
|
435
|
-
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
436
|
-
}
|
|
437
|
-
const binanceConnector = await withTimeout(
|
|
438
|
-
"binance connector init",
|
|
439
|
-
getCachedConnector({
|
|
440
|
-
state,
|
|
441
|
-
projectRoot,
|
|
442
|
-
userName,
|
|
443
|
-
connectorName: BUILTIN_CONNECTOR_NAMES.Binance
|
|
444
|
-
})
|
|
445
|
-
);
|
|
446
|
-
const coinbaseConnector = await withTimeout(
|
|
447
|
-
"coinbase connector init",
|
|
448
|
-
getCachedConnector({
|
|
449
|
-
state,
|
|
450
|
-
projectRoot,
|
|
451
|
-
userName,
|
|
452
|
-
connectorName: BUILTIN_CONNECTOR_NAMES.Coinbase
|
|
453
|
-
})
|
|
454
|
-
);
|
|
455
1207
|
if (!binanceConnector || !coinbaseConnector) {
|
|
456
1208
|
logger.warn(
|
|
457
1209
|
"Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
|
|
458
1210
|
connectorName
|
|
459
1211
|
);
|
|
460
1212
|
}
|
|
461
|
-
const interval = "15";
|
|
462
|
-
const cacheOnly = true;
|
|
463
1213
|
const coinCacheKey = getKlineCacheKey({
|
|
464
1214
|
userName,
|
|
465
1215
|
connectorName,
|
|
@@ -467,7 +1217,9 @@ var testing = async ({
|
|
|
467
1217
|
preloadStart,
|
|
468
1218
|
end,
|
|
469
1219
|
interval,
|
|
470
|
-
cacheOnly
|
|
1220
|
+
cacheOnly,
|
|
1221
|
+
universe,
|
|
1222
|
+
accountId
|
|
471
1223
|
});
|
|
472
1224
|
const btcCacheKey = getKlineCacheKey({
|
|
473
1225
|
userName,
|
|
@@ -476,12 +1228,23 @@ var testing = async ({
|
|
|
476
1228
|
preloadStart,
|
|
477
1229
|
end,
|
|
478
1230
|
interval,
|
|
479
|
-
cacheOnly
|
|
1231
|
+
cacheOnly,
|
|
1232
|
+
universe,
|
|
1233
|
+
accountId
|
|
1234
|
+
});
|
|
1235
|
+
const ethCacheKey = getKlineCacheKey({
|
|
1236
|
+
userName,
|
|
1237
|
+
connectorName,
|
|
1238
|
+
symbol: "ETHUSDT",
|
|
1239
|
+
preloadStart,
|
|
1240
|
+
end,
|
|
1241
|
+
interval,
|
|
1242
|
+
cacheOnly,
|
|
1243
|
+
universe,
|
|
1244
|
+
accountId
|
|
480
1245
|
});
|
|
481
1246
|
const btcBinanceConnectorName = binanceConnector ? BUILTIN_CONNECTOR_NAMES.Binance : connectorName;
|
|
482
1247
|
const btcCoinbaseConnectorName = coinbaseConnector ? BUILTIN_CONNECTOR_NAMES.Coinbase : connectorName;
|
|
483
|
-
const cachedCoinData = state.coinKlineCache.get(coinCacheKey);
|
|
484
|
-
const cachedBtcData = state.btcKlineCache.get(btcCacheKey);
|
|
485
1248
|
const btcBinanceCacheKey = getKlineCacheKey({
|
|
486
1249
|
userName,
|
|
487
1250
|
connectorName: btcBinanceConnectorName,
|
|
@@ -489,7 +1252,8 @@ var testing = async ({
|
|
|
489
1252
|
preloadStart,
|
|
490
1253
|
end,
|
|
491
1254
|
interval,
|
|
492
|
-
cacheOnly
|
|
1255
|
+
cacheOnly,
|
|
1256
|
+
universe: "crypto"
|
|
493
1257
|
});
|
|
494
1258
|
const btcCoinbaseCacheKey = getKlineCacheKey({
|
|
495
1259
|
userName,
|
|
@@ -498,10 +1262,41 @@ var testing = async ({
|
|
|
498
1262
|
preloadStart,
|
|
499
1263
|
end,
|
|
500
1264
|
interval,
|
|
501
|
-
cacheOnly
|
|
1265
|
+
cacheOnly,
|
|
1266
|
+
universe: "crypto"
|
|
502
1267
|
});
|
|
1268
|
+
const cachedCoinData = state.coinKlineCache.get(coinCacheKey);
|
|
1269
|
+
const cachedBtcData = state.btcKlineCache.get(btcCacheKey);
|
|
1270
|
+
const cachedEthData = state.ethKlineCache.get(ethCacheKey);
|
|
503
1271
|
const cachedBtcBinanceData = state.btcBinanceKlineCache.get(btcBinanceCacheKey);
|
|
504
1272
|
const cachedBtcCoinbaseData = state.btcCoinbaseKlineCache.get(btcCoinbaseCacheKey);
|
|
1273
|
+
const backtestExecutionInterval = resolveBacktestExecutionInterval(interval);
|
|
1274
|
+
const backtestExecutionCacheInterval = backtestExecutionInterval ?? interval;
|
|
1275
|
+
const shouldLoadExecutionCandles = backtestExecutionInterval != null && shouldLoadBacktestExecutionCandles(interval);
|
|
1276
|
+
const executionCoinCacheKey = getKlineCacheKey({
|
|
1277
|
+
userName,
|
|
1278
|
+
connectorName,
|
|
1279
|
+
symbol,
|
|
1280
|
+
preloadStart,
|
|
1281
|
+
end,
|
|
1282
|
+
interval: backtestExecutionCacheInterval,
|
|
1283
|
+
cacheOnly,
|
|
1284
|
+
universe,
|
|
1285
|
+
accountId
|
|
1286
|
+
});
|
|
1287
|
+
const executionBtcCacheKey = getKlineCacheKey({
|
|
1288
|
+
userName,
|
|
1289
|
+
connectorName,
|
|
1290
|
+
symbol: "BTCUSDT",
|
|
1291
|
+
preloadStart,
|
|
1292
|
+
end,
|
|
1293
|
+
interval: backtestExecutionCacheInterval,
|
|
1294
|
+
cacheOnly,
|
|
1295
|
+
universe,
|
|
1296
|
+
accountId
|
|
1297
|
+
});
|
|
1298
|
+
const cachedExecutionCoinData = shouldLoadExecutionCandles ? state.coinKlineCache.get(executionCoinCacheKey) : void 0;
|
|
1299
|
+
const cachedExecutionBtcData = shouldLoadExecutionCandles ? state.btcKlineCache.get(executionBtcCacheKey) : void 0;
|
|
505
1300
|
const preparedDataCacheKey = getPreparedDataCacheKey({
|
|
506
1301
|
userName,
|
|
507
1302
|
connectorName,
|
|
@@ -510,118 +1305,407 @@ var testing = async ({
|
|
|
510
1305
|
start,
|
|
511
1306
|
end,
|
|
512
1307
|
interval,
|
|
1308
|
+
backtestExecutionInterval: backtestExecutionCacheInterval,
|
|
513
1309
|
btcBinanceConnectorName,
|
|
514
|
-
btcCoinbaseConnectorName
|
|
1310
|
+
btcCoinbaseConnectorName,
|
|
1311
|
+
universe,
|
|
1312
|
+
accountId
|
|
515
1313
|
});
|
|
516
|
-
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1314
|
+
const cachedPreparedData = state.preparedDataCache.get(preparedDataCacheKey);
|
|
1315
|
+
if (cachedPreparedData) {
|
|
1316
|
+
return cachedPreparedData;
|
|
1317
|
+
}
|
|
1318
|
+
const btcDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedBtcData ? Promise.resolve(cachedBtcData) : connector.kline({
|
|
1319
|
+
symbol: "BTCUSDT",
|
|
1320
|
+
start: preloadStart,
|
|
1321
|
+
end,
|
|
1322
|
+
interval,
|
|
1323
|
+
silent: true,
|
|
1324
|
+
cacheOnly
|
|
1325
|
+
});
|
|
1326
|
+
const ethDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedEthData ? Promise.resolve(cachedEthData) : connector.kline({
|
|
1327
|
+
symbol: "ETHUSDT",
|
|
1328
|
+
start: preloadStart,
|
|
1329
|
+
end,
|
|
1330
|
+
interval,
|
|
1331
|
+
silent: true,
|
|
1332
|
+
cacheOnly
|
|
1333
|
+
});
|
|
1334
|
+
const btcBinanceDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedBtcBinanceData ? Promise.resolve(cachedBtcBinanceData) : btcBinanceCacheKey === btcCacheKey ? btcDataPromise : (binanceConnector ?? connector).kline({
|
|
1335
|
+
symbol: "BTCUSDT",
|
|
1336
|
+
start: preloadStart,
|
|
1337
|
+
end,
|
|
1338
|
+
interval,
|
|
1339
|
+
silent: true,
|
|
1340
|
+
cacheOnly
|
|
1341
|
+
});
|
|
1342
|
+
const btcCoinbaseDataPromise = universe === "tradfi" ? Promise.resolve([]) : cachedBtcCoinbaseData ? Promise.resolve(cachedBtcCoinbaseData) : btcCoinbaseCacheKey === btcCacheKey ? btcDataPromise : (coinbaseConnector ?? connector).kline({
|
|
1343
|
+
symbol: "BTCUSDT",
|
|
1344
|
+
start: preloadStart,
|
|
1345
|
+
end,
|
|
1346
|
+
interval,
|
|
1347
|
+
silent: true,
|
|
1348
|
+
cacheOnly
|
|
1349
|
+
});
|
|
1350
|
+
const executionDataPromise = !shouldLoadExecutionCandles ? Promise.resolve([]) : cachedExecutionCoinData ? Promise.resolve(cachedExecutionCoinData) : connector.kline({
|
|
1351
|
+
symbol,
|
|
1352
|
+
start: preloadStart,
|
|
1353
|
+
end,
|
|
1354
|
+
interval: backtestExecutionInterval,
|
|
1355
|
+
silent: true,
|
|
1356
|
+
cacheOnly
|
|
1357
|
+
});
|
|
1358
|
+
const executionBtcDataPromise = universe === "tradfi" || !shouldLoadExecutionCandles ? Promise.resolve([]) : cachedExecutionBtcData ? Promise.resolve(cachedExecutionBtcData) : connector.kline({
|
|
1359
|
+
symbol: "BTCUSDT",
|
|
1360
|
+
start: preloadStart,
|
|
1361
|
+
end,
|
|
1362
|
+
interval: backtestExecutionInterval,
|
|
1363
|
+
silent: true,
|
|
1364
|
+
cacheOnly
|
|
1365
|
+
});
|
|
1366
|
+
const [
|
|
1367
|
+
dataRaw,
|
|
1368
|
+
btcDataRaw,
|
|
1369
|
+
ethDataRaw,
|
|
1370
|
+
btcBinanceDataRaw,
|
|
1371
|
+
btcCoinbaseDataRaw,
|
|
1372
|
+
executionDataRaw,
|
|
1373
|
+
executionBtcDataRaw
|
|
1374
|
+
] = await Promise.all([
|
|
1375
|
+
cachedCoinData ? Promise.resolve(cachedCoinData) : connector.kline({
|
|
1376
|
+
symbol,
|
|
1377
|
+
start: preloadStart,
|
|
1378
|
+
end,
|
|
1379
|
+
interval,
|
|
1380
|
+
silent: true,
|
|
1381
|
+
cacheOnly
|
|
1382
|
+
}),
|
|
1383
|
+
btcDataPromise,
|
|
1384
|
+
ethDataPromise,
|
|
1385
|
+
btcBinanceDataPromise,
|
|
1386
|
+
btcCoinbaseDataPromise,
|
|
1387
|
+
executionDataPromise,
|
|
1388
|
+
executionBtcDataPromise
|
|
1389
|
+
]);
|
|
1390
|
+
if (!cachedCoinData) {
|
|
1391
|
+
state.coinKlineCache.set(coinCacheKey, dataRaw);
|
|
1392
|
+
}
|
|
1393
|
+
if (!cachedBtcData) {
|
|
1394
|
+
state.btcKlineCache.set(btcCacheKey, btcDataRaw);
|
|
1395
|
+
}
|
|
1396
|
+
if (!cachedEthData) {
|
|
1397
|
+
state.ethKlineCache.set(ethCacheKey, ethDataRaw);
|
|
1398
|
+
}
|
|
1399
|
+
if (!cachedBtcBinanceData) {
|
|
1400
|
+
state.btcBinanceKlineCache.set(btcBinanceCacheKey, btcBinanceDataRaw);
|
|
1401
|
+
}
|
|
1402
|
+
if (!cachedBtcCoinbaseData) {
|
|
1403
|
+
state.btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseDataRaw);
|
|
1404
|
+
}
|
|
1405
|
+
if (shouldLoadExecutionCandles && !cachedExecutionCoinData) {
|
|
1406
|
+
state.coinKlineCache.set(executionCoinCacheKey, executionDataRaw);
|
|
1407
|
+
}
|
|
1408
|
+
if (shouldLoadExecutionCandles && !cachedExecutionBtcData) {
|
|
1409
|
+
state.btcKlineCache.set(executionBtcCacheKey, executionBtcDataRaw);
|
|
1410
|
+
}
|
|
1411
|
+
const aligned = universe === "tradfi" ? { alignedCoinCandles: dataRaw, alignedBtcCandles: dataRaw } : alignSortedCandlesByTimestamp(dataRaw, btcDataRaw);
|
|
1412
|
+
const { data, btcData } = filterClosedAlignedCandles(
|
|
1413
|
+
aligned.alignedCoinCandles,
|
|
1414
|
+
aligned.alignedBtcCandles,
|
|
1415
|
+
interval
|
|
1416
|
+
);
|
|
1417
|
+
const btcBinanceData = universe === "tradfi" ? [] : alignSortedCandlesByTimestamp(data, btcBinanceDataRaw).alignedBtcCandles;
|
|
1418
|
+
const btcCoinbaseData = universe === "tradfi" ? [] : alignSortedCandlesByTimestamp(data, btcCoinbaseDataRaw).alignedBtcCandles;
|
|
1419
|
+
const ethData = universe === "tradfi" ? [] : alignSortedCandlesByTimestamp(data, ethDataRaw).alignedBtcCandles;
|
|
1420
|
+
const alignedExecution = shouldLoadExecutionCandles ? universe === "tradfi" ? {
|
|
1421
|
+
alignedCoinCandles: executionDataRaw,
|
|
1422
|
+
alignedBtcCandles: executionDataRaw
|
|
1423
|
+
} : alignSortedCandlesByTimestamp(executionDataRaw, executionBtcDataRaw) : { alignedCoinCandles: [], alignedBtcCandles: [] };
|
|
1424
|
+
const { data: backtestExecutionData, btcData: backtestExecutionBtcData } = filterClosedAlignedCandles(
|
|
1425
|
+
alignedExecution.alignedCoinCandles,
|
|
1426
|
+
alignedExecution.alignedBtcCandles,
|
|
1427
|
+
backtestExecutionCacheInterval
|
|
1428
|
+
);
|
|
1429
|
+
const backtestExecutionDataByTimestamp = buildCandleByTimestamp(
|
|
1430
|
+
backtestExecutionData
|
|
1431
|
+
);
|
|
1432
|
+
const backtestExecutionBtcDataByTimestamp = buildCandleByTimestamp(
|
|
1433
|
+
backtestExecutionBtcData
|
|
1434
|
+
);
|
|
1435
|
+
const { prevData, testData } = splitCandlesForTesting(
|
|
1436
|
+
data,
|
|
1437
|
+
start,
|
|
1438
|
+
preloadStart
|
|
1439
|
+
);
|
|
1440
|
+
const { prevData: btcPrevData, testData: btcTestData } = splitCandlesForTesting(btcData, start, preloadStart);
|
|
1441
|
+
const { prevData: ethPrevData, testData: ethTestData } = splitCandlesForTesting(ethData, start, preloadStart);
|
|
1442
|
+
const preparedData = {
|
|
1443
|
+
data,
|
|
1444
|
+
btcData,
|
|
1445
|
+
ethData,
|
|
1446
|
+
prevData,
|
|
1447
|
+
btcPrevData,
|
|
1448
|
+
ethPrevData,
|
|
1449
|
+
testData,
|
|
1450
|
+
btcTestData,
|
|
1451
|
+
ethTestData,
|
|
1452
|
+
btcBinanceData,
|
|
1453
|
+
btcCoinbaseData,
|
|
1454
|
+
backtestExecutionInterval: backtestExecutionCacheInterval,
|
|
1455
|
+
backtestExecutionData,
|
|
1456
|
+
backtestExecutionBtcData,
|
|
1457
|
+
backtestExecutionDataByTimestamp,
|
|
1458
|
+
backtestExecutionBtcDataByTimestamp
|
|
1459
|
+
};
|
|
1460
|
+
state.preparedDataCache.set(preparedDataCacheKey, preparedData);
|
|
1461
|
+
return preparedData;
|
|
1462
|
+
};
|
|
1463
|
+
var canRunTestsInSharedCandleLoop = (tests) => {
|
|
1464
|
+
if (tests.length <= 1) {
|
|
1465
|
+
return false;
|
|
1466
|
+
}
|
|
1467
|
+
const first = tests[0];
|
|
1468
|
+
const firstStart = first.options?.start;
|
|
1469
|
+
const firstEnd = first.options?.end;
|
|
1470
|
+
return tests.every(
|
|
1471
|
+
(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)
|
|
1472
|
+
);
|
|
1473
|
+
};
|
|
1474
|
+
var testing = async ({
|
|
1475
|
+
userName,
|
|
1476
|
+
symbol,
|
|
1477
|
+
options: { start, end },
|
|
1478
|
+
name,
|
|
1479
|
+
testId,
|
|
1480
|
+
testSuiteId,
|
|
1481
|
+
configId,
|
|
1482
|
+
strategyName,
|
|
1483
|
+
strategyConfig,
|
|
1484
|
+
connectorName,
|
|
1485
|
+
universe = "crypto",
|
|
1486
|
+
assetClass,
|
|
1487
|
+
instrument: requestedInstrument,
|
|
1488
|
+
accountId,
|
|
1489
|
+
deploymentId,
|
|
1490
|
+
policyProfileId,
|
|
1491
|
+
interval = BACKTEST_INTERVAL,
|
|
1492
|
+
ml = false,
|
|
1493
|
+
ai = false,
|
|
1494
|
+
fast = false,
|
|
1495
|
+
collectReplaySignalEvaluations = false,
|
|
1496
|
+
chunkId = "single",
|
|
1497
|
+
backtestRunId,
|
|
1498
|
+
backtestTestKey,
|
|
1499
|
+
timeoutMs
|
|
1500
|
+
}) => {
|
|
1501
|
+
if (!start) {
|
|
1502
|
+
throw new Error("no start");
|
|
1503
|
+
}
|
|
1504
|
+
const preloadStart = getBacktestPreloadStart(start);
|
|
1505
|
+
const startedAt = Date.now();
|
|
1506
|
+
let activeStageStartedAt = startedAt;
|
|
1507
|
+
let lastProgressSentAt = 0;
|
|
1508
|
+
let lastProgressSignature = "";
|
|
1509
|
+
let currentCandleIndex = 0;
|
|
1510
|
+
let totalCandles = 0;
|
|
1511
|
+
const strategyCandleTimeoutMs = getStrategyCandleTimeoutMs();
|
|
1512
|
+
const formatTimeoutMessage = (stage, stageTimeoutMs) => `Test ${name} (${symbol}) timed out after ${stageTimeoutMs}ms during ${stage}`;
|
|
1513
|
+
const emitProgress = (stage, options = {}) => {
|
|
1514
|
+
const now = Date.now();
|
|
1515
|
+
const candleIndex = typeof options.candleIndex === "number" ? options.candleIndex : currentCandleIndex;
|
|
1516
|
+
const candleTotal = typeof options.candleTotal === "number" ? options.candleTotal : totalCandles;
|
|
1517
|
+
const signature = [
|
|
1518
|
+
stage,
|
|
1519
|
+
candleIndex,
|
|
1520
|
+
candleTotal,
|
|
1521
|
+
Math.floor((now - activeStageStartedAt) / 5e3)
|
|
1522
|
+
].join(":");
|
|
1523
|
+
if (!options.force) {
|
|
1524
|
+
if (signature === lastProgressSignature) {
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
if (now - lastProgressSentAt < 4e3) {
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
560
1530
|
}
|
|
561
|
-
|
|
562
|
-
|
|
1531
|
+
lastProgressSentAt = now;
|
|
1532
|
+
lastProgressSignature = signature;
|
|
1533
|
+
process.send?.({
|
|
1534
|
+
progress: true,
|
|
1535
|
+
testName: name,
|
|
1536
|
+
symbol,
|
|
1537
|
+
strategyName,
|
|
1538
|
+
stage,
|
|
1539
|
+
candleIndex,
|
|
1540
|
+
candleTotal,
|
|
1541
|
+
elapsedMs: now - startedAt,
|
|
1542
|
+
stageElapsedMs: now - activeStageStartedAt
|
|
1543
|
+
});
|
|
1544
|
+
};
|
|
1545
|
+
const getStageTimeoutMs = () => {
|
|
1546
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
1547
|
+
return null;
|
|
563
1548
|
}
|
|
564
|
-
|
|
565
|
-
|
|
1549
|
+
return timeoutMs;
|
|
1550
|
+
};
|
|
1551
|
+
const throwIfTimedOut = (stage) => {
|
|
1552
|
+
if (getStageTimeoutMs() == null) {
|
|
1553
|
+
return;
|
|
566
1554
|
}
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
);
|
|
574
|
-
const { prevData: btcCoinbasePrevDataRaw } = splitCandlesForTesting(
|
|
575
|
-
btcCoinbaseData,
|
|
576
|
-
start,
|
|
577
|
-
preloadStart
|
|
1555
|
+
emitProgress(stage);
|
|
1556
|
+
};
|
|
1557
|
+
const withTimeout = async (stage, promise, stageTimeoutOverrideMs = null) => {
|
|
1558
|
+
const stageTimeoutMs = getEffectiveTimeoutMs(
|
|
1559
|
+
getStageTimeoutMs() ?? void 0,
|
|
1560
|
+
stageTimeoutOverrideMs
|
|
578
1561
|
);
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
1562
|
+
if (stageTimeoutMs == null) {
|
|
1563
|
+
return promise;
|
|
1564
|
+
}
|
|
1565
|
+
activeStageStartedAt = Date.now();
|
|
1566
|
+
emitProgress(stage, { force: true });
|
|
1567
|
+
return await new Promise((resolve, reject) => {
|
|
1568
|
+
const heartbeat = setInterval(() => {
|
|
1569
|
+
emitProgress(stage);
|
|
1570
|
+
}, 5e3);
|
|
1571
|
+
const timer = setTimeout(() => {
|
|
1572
|
+
clearInterval(heartbeat);
|
|
1573
|
+
reject(new Error(formatTimeoutMessage(stage, stageTimeoutMs)));
|
|
1574
|
+
}, stageTimeoutMs);
|
|
1575
|
+
promise.then(
|
|
1576
|
+
(value) => {
|
|
1577
|
+
clearInterval(heartbeat);
|
|
1578
|
+
clearTimeout(timer);
|
|
1579
|
+
resolve(value);
|
|
1580
|
+
},
|
|
1581
|
+
(error) => {
|
|
1582
|
+
clearInterval(heartbeat);
|
|
1583
|
+
clearTimeout(timer);
|
|
1584
|
+
reject(error);
|
|
1585
|
+
}
|
|
1586
|
+
);
|
|
1587
|
+
});
|
|
1588
|
+
};
|
|
1589
|
+
const runStage = (stage, fn) => {
|
|
1590
|
+
if (getStageTimeoutMs() == null) {
|
|
1591
|
+
return fn();
|
|
1592
|
+
}
|
|
1593
|
+
return withTimeout(stage, fn());
|
|
1594
|
+
};
|
|
1595
|
+
const runStrategyCandleStage = (stage, fn) => withTimeout(stage, fn(), strategyCandleTimeoutMs);
|
|
1596
|
+
const { projectRoot, state } = getTestingKlineCacheState();
|
|
1597
|
+
const connector = await withTimeout(
|
|
1598
|
+
"connector init",
|
|
1599
|
+
getCachedConnector({
|
|
1600
|
+
state,
|
|
1601
|
+
projectRoot,
|
|
1602
|
+
userName,
|
|
1603
|
+
connectorName,
|
|
1604
|
+
universe,
|
|
1605
|
+
accountId,
|
|
1606
|
+
deploymentId
|
|
1607
|
+
})
|
|
1608
|
+
);
|
|
1609
|
+
if (!connector) {
|
|
1610
|
+
throw new Error(`Unknown connector: ${connectorName}`);
|
|
1611
|
+
}
|
|
1612
|
+
const strategyCreator = await withTimeout(
|
|
1613
|
+
"strategy lookup",
|
|
1614
|
+
getStrategyCreator(strategyName, projectRoot)
|
|
1615
|
+
);
|
|
1616
|
+
if (!strategyCreator) {
|
|
1617
|
+
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
592
1618
|
}
|
|
1619
|
+
const preparedData = await withTimeout(
|
|
1620
|
+
"kline preload",
|
|
1621
|
+
prepareTestingData({
|
|
1622
|
+
state,
|
|
1623
|
+
projectRoot,
|
|
1624
|
+
userName,
|
|
1625
|
+
connectorName,
|
|
1626
|
+
symbol,
|
|
1627
|
+
preloadStart,
|
|
1628
|
+
start,
|
|
1629
|
+
end,
|
|
1630
|
+
interval,
|
|
1631
|
+
universe,
|
|
1632
|
+
accountId,
|
|
1633
|
+
deploymentId
|
|
1634
|
+
})
|
|
1635
|
+
);
|
|
593
1636
|
if (!preparedData) {
|
|
594
1637
|
throw new Error("Prepared backtest data not available");
|
|
595
1638
|
}
|
|
596
1639
|
const {
|
|
597
1640
|
prevData,
|
|
598
1641
|
btcPrevData,
|
|
1642
|
+
ethPrevData,
|
|
1643
|
+
ethTestData,
|
|
599
1644
|
testData,
|
|
600
1645
|
btcTestData,
|
|
601
|
-
|
|
602
|
-
|
|
1646
|
+
btcBinanceData,
|
|
1647
|
+
btcCoinbaseData,
|
|
1648
|
+
backtestExecutionInterval,
|
|
1649
|
+
backtestExecutionData,
|
|
1650
|
+
backtestExecutionBtcData,
|
|
1651
|
+
backtestExecutionDataByTimestamp,
|
|
1652
|
+
backtestExecutionBtcDataByTimestamp
|
|
603
1653
|
} = preparedData;
|
|
1654
|
+
const runtimePrevData = prevData.slice();
|
|
1655
|
+
const runtimeBtcPrevData = btcPrevData.slice();
|
|
1656
|
+
const runtimeEthData = [...ethPrevData, ...ethTestData];
|
|
1657
|
+
totalCandles = testData.length;
|
|
1658
|
+
const instrument = requestedInstrument;
|
|
1659
|
+
const { model: executionCostModel, fundingRates } = await resolveExecutionCosts({
|
|
1660
|
+
connector,
|
|
1661
|
+
symbol,
|
|
1662
|
+
config: strategyConfig,
|
|
1663
|
+
startTime: start,
|
|
1664
|
+
endTime: end,
|
|
1665
|
+
instrument
|
|
1666
|
+
});
|
|
604
1667
|
const testConnector = createTestConnector(connector, {
|
|
605
1668
|
userName,
|
|
606
1669
|
mlEnabled: ml,
|
|
607
|
-
aiEnabled: ai
|
|
1670
|
+
aiEnabled: ai,
|
|
1671
|
+
fastMode: fast,
|
|
1672
|
+
executionCostModel,
|
|
1673
|
+
fundingRates
|
|
608
1674
|
});
|
|
609
1675
|
const strategy = await withTimeout(
|
|
610
1676
|
"strategy init",
|
|
611
1677
|
strategyCreator({
|
|
612
1678
|
userName,
|
|
613
|
-
|
|
1679
|
+
connectorName,
|
|
1680
|
+
universe,
|
|
1681
|
+
assetClass: assetClass ?? instrument?.assetClass,
|
|
1682
|
+
instrument,
|
|
1683
|
+
accountId,
|
|
1684
|
+
deploymentId,
|
|
1685
|
+
policyProfileId,
|
|
1686
|
+
config: {
|
|
1687
|
+
...strategyConfig,
|
|
1688
|
+
INTERVAL: interval
|
|
1689
|
+
},
|
|
614
1690
|
symbol,
|
|
615
|
-
data:
|
|
616
|
-
btcData:
|
|
617
|
-
|
|
618
|
-
|
|
1691
|
+
data: runtimePrevData,
|
|
1692
|
+
btcData: runtimeBtcPrevData,
|
|
1693
|
+
ethData: runtimeEthData,
|
|
1694
|
+
btcBinanceData,
|
|
1695
|
+
btcCoinbaseData,
|
|
1696
|
+
backtestExecutionMarketData: {
|
|
1697
|
+
interval: backtestExecutionInterval,
|
|
1698
|
+
data: backtestExecutionData,
|
|
1699
|
+
btcData: backtestExecutionBtcData,
|
|
1700
|
+
dataByTimestamp: backtestExecutionDataByTimestamp,
|
|
1701
|
+
btcDataByTimestamp: backtestExecutionBtcDataByTimestamp
|
|
1702
|
+
},
|
|
619
1703
|
connector: testConnector
|
|
620
1704
|
})
|
|
621
1705
|
);
|
|
622
1706
|
const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
|
|
623
1707
|
const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
|
|
624
|
-
const replaySignalEvaluations = [];
|
|
1708
|
+
const replaySignalEvaluations = collectReplaySignalEvaluations ? [] : null;
|
|
625
1709
|
const flushClosedResultsBatch = async () => {
|
|
626
1710
|
if (!ml && !ai) return;
|
|
627
1711
|
const batch = await testConnector.drainMlResultsBatch();
|
|
@@ -633,7 +1717,14 @@ var testing = async ({
|
|
|
633
1717
|
const fullRow = buildMlTrainingRow(payload, {
|
|
634
1718
|
profit: resultRecord.profit
|
|
635
1719
|
});
|
|
636
|
-
const row =
|
|
1720
|
+
const row = {
|
|
1721
|
+
...trimMlTrainingRowWindows(fullRow, 5),
|
|
1722
|
+
...buildBacktestDatasetMetadata({
|
|
1723
|
+
backtestRunId,
|
|
1724
|
+
backtestTestKey,
|
|
1725
|
+
chunkId
|
|
1726
|
+
})
|
|
1727
|
+
};
|
|
637
1728
|
await appendMlDatasetRow({
|
|
638
1729
|
strategyName,
|
|
639
1730
|
chunkId,
|
|
@@ -643,62 +1734,54 @@ var testing = async ({
|
|
|
643
1734
|
const aiRowBase = pendingAiRowBySignalId.get(resultRecord.signalId);
|
|
644
1735
|
if (aiRowBase) {
|
|
645
1736
|
pendingAiRowBySignalId.delete(resultRecord.signalId);
|
|
1737
|
+
const { signal: aiSignal, ...rowBase } = aiRowBase;
|
|
646
1738
|
await appendAiDatasetRow({
|
|
647
1739
|
strategyName,
|
|
648
1740
|
chunkId,
|
|
649
1741
|
row: {
|
|
650
|
-
...
|
|
651
|
-
|
|
1742
|
+
...rowBase,
|
|
1743
|
+
payload: buildAiPayload(aiSignal),
|
|
1744
|
+
profit: resultRecord.profit,
|
|
1745
|
+
tradeResult: resultRecord.tradeResult
|
|
652
1746
|
}
|
|
653
1747
|
});
|
|
654
1748
|
}
|
|
655
1749
|
}
|
|
656
1750
|
};
|
|
657
|
-
|
|
658
|
-
if (
|
|
659
|
-
|
|
1751
|
+
const processSignal = async (signal, candle) => {
|
|
1752
|
+
if (isBacktestEntryDelayControlCode(signal)) {
|
|
1753
|
+
return;
|
|
660
1754
|
}
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
strategy: strategyName,
|
|
674
|
-
symbol,
|
|
675
|
-
interval,
|
|
676
|
-
timestamp: candle.timestamp,
|
|
677
|
-
evaluatedAt: candle.timestamp,
|
|
678
|
-
status: "skip",
|
|
679
|
-
reason: typeof signal === "string" && signal.trim() ? signal : "NO_SIGNAL"
|
|
680
|
-
});
|
|
681
|
-
} else {
|
|
682
|
-
replaySignalEvaluations.push({
|
|
683
|
-
evaluationId: `${signal.signalId || testId}:${strategyName}:${symbol}:${signal.timestamp || candle.timestamp}`,
|
|
684
|
-
userName,
|
|
685
|
-
strategy: signal.strategy || strategyName,
|
|
686
|
-
symbol: signal.symbol || symbol,
|
|
687
|
-
interval: signal.interval || interval,
|
|
688
|
-
timestamp: typeof signal.timestamp === "number" && Number.isFinite(signal.timestamp) ? signal.timestamp : candle.timestamp,
|
|
689
|
-
evaluatedAt: candle.timestamp,
|
|
690
|
-
status: "signal",
|
|
691
|
-
reason: signal.orderSkipReason || signal.orderStatus,
|
|
692
|
-
signalId: signal.signalId,
|
|
693
|
-
direction: signal.direction,
|
|
694
|
-
orderStatus: signal.orderStatus,
|
|
695
|
-
orderSkipReason: signal.orderSkipReason,
|
|
696
|
-
aiAnalysis: signal.aiAnalysis ?? null,
|
|
697
|
-
ml: signal.ml
|
|
698
|
-
});
|
|
1755
|
+
if (replaySignalEvaluations) {
|
|
1756
|
+
replaySignalEvaluations.push(
|
|
1757
|
+
buildReplaySignalEvaluationRecord({
|
|
1758
|
+
signal,
|
|
1759
|
+
testId,
|
|
1760
|
+
userName,
|
|
1761
|
+
strategyName,
|
|
1762
|
+
symbol,
|
|
1763
|
+
interval,
|
|
1764
|
+
candle
|
|
1765
|
+
})
|
|
1766
|
+
);
|
|
699
1767
|
}
|
|
700
1768
|
const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (ml || ai);
|
|
701
1769
|
if (shouldCapturePayload) {
|
|
1770
|
+
await withTimeout(
|
|
1771
|
+
"binance market context",
|
|
1772
|
+
enrichSignalWithBinanceMarketContext({
|
|
1773
|
+
signal,
|
|
1774
|
+
env: "BACKTEST"
|
|
1775
|
+
})
|
|
1776
|
+
);
|
|
1777
|
+
await withTimeout(
|
|
1778
|
+
"coinmarketcap context",
|
|
1779
|
+
enrichSignalWithCoinMarketCapContext({
|
|
1780
|
+
signal,
|
|
1781
|
+
env: "BACKTEST",
|
|
1782
|
+
enabled: Boolean(ml || ai)
|
|
1783
|
+
})
|
|
1784
|
+
);
|
|
702
1785
|
await withTimeout(
|
|
703
1786
|
"derivatives context",
|
|
704
1787
|
enrichSignalWithDerivativesContext({
|
|
@@ -715,6 +1798,7 @@ var testing = async ({
|
|
|
715
1798
|
testId,
|
|
716
1799
|
testSuiteId,
|
|
717
1800
|
testName: name,
|
|
1801
|
+
configId,
|
|
718
1802
|
symbol,
|
|
719
1803
|
strategyName,
|
|
720
1804
|
strategyConfig,
|
|
@@ -730,23 +1814,515 @@ var testing = async ({
|
|
|
730
1814
|
symbol: signal.symbol || symbol,
|
|
731
1815
|
direction: signal.direction,
|
|
732
1816
|
timestamp: signal.timestamp,
|
|
733
|
-
|
|
1817
|
+
signal: cloneAiPayloadSignal(signal),
|
|
734
1818
|
testId,
|
|
735
1819
|
testSuiteId,
|
|
736
1820
|
testName: name,
|
|
737
|
-
|
|
1821
|
+
configId,
|
|
1822
|
+
connectorName,
|
|
1823
|
+
...buildBacktestDatasetMetadata({
|
|
1824
|
+
backtestRunId,
|
|
1825
|
+
backtestTestKey,
|
|
1826
|
+
chunkId
|
|
1827
|
+
})
|
|
738
1828
|
});
|
|
739
1829
|
}
|
|
1830
|
+
};
|
|
1831
|
+
for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
|
|
1832
|
+
if (candleIndex % 25 === 0) {
|
|
1833
|
+
throwIfTimedOut("candle loop");
|
|
1834
|
+
}
|
|
1835
|
+
currentCandleIndex = candleIndex + 1;
|
|
1836
|
+
emitProgress("candle loop", {
|
|
1837
|
+
force: candleIndex === 0 || currentCandleIndex === totalCandles
|
|
1838
|
+
});
|
|
1839
|
+
const candle = testData[candleIndex];
|
|
1840
|
+
const btcCandle = btcTestData[candleIndex];
|
|
1841
|
+
const delayedSignal = await runStrategyCandleStage(
|
|
1842
|
+
"delayed entry",
|
|
1843
|
+
async () => strategy.__tradejsFlushBacktestDelayedEntry?.(candle, btcCandle)
|
|
1844
|
+
);
|
|
1845
|
+
if (delayedSignal && typeof delayedSignal !== "string") {
|
|
1846
|
+
await processSignal(delayedSignal, candle);
|
|
1847
|
+
}
|
|
1848
|
+
await runStage("exit checks", () => testConnector.checkExits(candle));
|
|
1849
|
+
const signal = await runStrategyCandleStage(
|
|
1850
|
+
"strategy signal",
|
|
1851
|
+
() => strategy(candle, btcCandle)
|
|
1852
|
+
);
|
|
1853
|
+
await processSignal(signal, candle);
|
|
1854
|
+
if ((candleIndex + 1) % CLOSED_RESULT_FLUSH_INTERVAL === 0) {
|
|
1855
|
+
await withTimeout("flush closed results", flushClosedResultsBatch());
|
|
1856
|
+
}
|
|
740
1857
|
}
|
|
741
1858
|
await withTimeout("flush closed results", flushClosedResultsBatch());
|
|
742
1859
|
const result = await withTimeout("collect result", testConnector.getResult());
|
|
743
|
-
return {
|
|
1860
|
+
return replaySignalEvaluations ? {
|
|
744
1861
|
...result,
|
|
745
1862
|
inlineReplaySignalEvaluations: replaySignalEvaluations
|
|
1863
|
+
} : result;
|
|
1864
|
+
};
|
|
1865
|
+
var testingGroupInSharedCandleLoop = async (tests) => {
|
|
1866
|
+
if (!canRunTestsInSharedCandleLoop(tests)) {
|
|
1867
|
+
const results = [];
|
|
1868
|
+
for (const test of tests) {
|
|
1869
|
+
const result = await testing(test);
|
|
1870
|
+
if (result) {
|
|
1871
|
+
results.push({ test, result });
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
return results;
|
|
1875
|
+
}
|
|
1876
|
+
const first = tests[0];
|
|
1877
|
+
const {
|
|
1878
|
+
userName,
|
|
1879
|
+
symbol,
|
|
1880
|
+
options: { start, end },
|
|
1881
|
+
strategyName,
|
|
1882
|
+
connectorName,
|
|
1883
|
+
universe = "crypto",
|
|
1884
|
+
accountId,
|
|
1885
|
+
deploymentId,
|
|
1886
|
+
interval = BACKTEST_INTERVAL,
|
|
1887
|
+
ml = false,
|
|
1888
|
+
ai = false,
|
|
1889
|
+
fast = false,
|
|
1890
|
+
collectReplaySignalEvaluations = false,
|
|
1891
|
+
chunkId = "single",
|
|
1892
|
+
timeoutMs
|
|
1893
|
+
} = first;
|
|
1894
|
+
if (!start) {
|
|
1895
|
+
throw new Error("no start");
|
|
1896
|
+
}
|
|
1897
|
+
const preloadStart = getBacktestPreloadStart(start);
|
|
1898
|
+
const startedAt = Date.now();
|
|
1899
|
+
let activeStageStartedAt = startedAt;
|
|
1900
|
+
let lastProgressSentAt = 0;
|
|
1901
|
+
let lastProgressSignature = "";
|
|
1902
|
+
let currentCandleIndex = 0;
|
|
1903
|
+
let totalCandles = 0;
|
|
1904
|
+
const strategyCandleTimeoutMs = getStrategyCandleTimeoutMs();
|
|
1905
|
+
const formatTimeoutMessage = (stage, stageTimeoutMs) => `Test group ${strategyName}/${symbol} timed out after ${stageTimeoutMs}ms during ${stage}`;
|
|
1906
|
+
const emitProgress = (stage, options = {}) => {
|
|
1907
|
+
const now = Date.now();
|
|
1908
|
+
const candleIndex = typeof options.candleIndex === "number" ? options.candleIndex : currentCandleIndex;
|
|
1909
|
+
const candleTotal = typeof options.candleTotal === "number" ? options.candleTotal : totalCandles;
|
|
1910
|
+
const signature = [
|
|
1911
|
+
stage,
|
|
1912
|
+
candleIndex,
|
|
1913
|
+
candleTotal,
|
|
1914
|
+
Math.floor((now - activeStageStartedAt) / 5e3)
|
|
1915
|
+
].join(":");
|
|
1916
|
+
if (!options.force) {
|
|
1917
|
+
if (signature === lastProgressSignature) {
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
if (now - lastProgressSentAt < 4e3) {
|
|
1921
|
+
return;
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
lastProgressSentAt = now;
|
|
1925
|
+
lastProgressSignature = signature;
|
|
1926
|
+
process.send?.({
|
|
1927
|
+
progress: true,
|
|
1928
|
+
testName: first.name,
|
|
1929
|
+
symbol,
|
|
1930
|
+
strategyName,
|
|
1931
|
+
stage,
|
|
1932
|
+
candleIndex,
|
|
1933
|
+
candleTotal,
|
|
1934
|
+
elapsedMs: now - startedAt,
|
|
1935
|
+
stageElapsedMs: now - activeStageStartedAt
|
|
1936
|
+
});
|
|
1937
|
+
};
|
|
1938
|
+
const getStageTimeoutMs = () => {
|
|
1939
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
1940
|
+
return null;
|
|
1941
|
+
}
|
|
1942
|
+
return timeoutMs;
|
|
1943
|
+
};
|
|
1944
|
+
const throwIfTimedOut = (stage) => {
|
|
1945
|
+
if (getStageTimeoutMs() == null) {
|
|
1946
|
+
return;
|
|
1947
|
+
}
|
|
1948
|
+
emitProgress(stage);
|
|
746
1949
|
};
|
|
1950
|
+
const withTimeout = async (stage, promise, stageTimeoutOverrideMs = null) => {
|
|
1951
|
+
const stageTimeoutMs = getEffectiveTimeoutMs(
|
|
1952
|
+
getStageTimeoutMs() ?? void 0,
|
|
1953
|
+
stageTimeoutOverrideMs
|
|
1954
|
+
);
|
|
1955
|
+
if (stageTimeoutMs == null) {
|
|
1956
|
+
return promise;
|
|
1957
|
+
}
|
|
1958
|
+
activeStageStartedAt = Date.now();
|
|
1959
|
+
emitProgress(stage, { force: true });
|
|
1960
|
+
return await new Promise((resolve, reject) => {
|
|
1961
|
+
const heartbeat = setInterval(() => {
|
|
1962
|
+
emitProgress(stage);
|
|
1963
|
+
}, 5e3);
|
|
1964
|
+
const timer = setTimeout(() => {
|
|
1965
|
+
clearInterval(heartbeat);
|
|
1966
|
+
reject(new Error(formatTimeoutMessage(stage, stageTimeoutMs)));
|
|
1967
|
+
}, stageTimeoutMs);
|
|
1968
|
+
promise.then(
|
|
1969
|
+
(value) => {
|
|
1970
|
+
clearInterval(heartbeat);
|
|
1971
|
+
clearTimeout(timer);
|
|
1972
|
+
resolve(value);
|
|
1973
|
+
},
|
|
1974
|
+
(error) => {
|
|
1975
|
+
clearInterval(heartbeat);
|
|
1976
|
+
clearTimeout(timer);
|
|
1977
|
+
reject(error);
|
|
1978
|
+
}
|
|
1979
|
+
);
|
|
1980
|
+
});
|
|
1981
|
+
};
|
|
1982
|
+
const runStage = (stage, fn) => {
|
|
1983
|
+
if (getStageTimeoutMs() == null) {
|
|
1984
|
+
return fn();
|
|
1985
|
+
}
|
|
1986
|
+
return withTimeout(stage, fn());
|
|
1987
|
+
};
|
|
1988
|
+
const runStrategyCandleStage = (stage, fn) => withTimeout(stage, fn(), strategyCandleTimeoutMs);
|
|
1989
|
+
const { projectRoot, state } = getTestingKlineCacheState();
|
|
1990
|
+
const connector = await withTimeout(
|
|
1991
|
+
"connector init",
|
|
1992
|
+
getCachedConnector({
|
|
1993
|
+
state,
|
|
1994
|
+
projectRoot,
|
|
1995
|
+
userName,
|
|
1996
|
+
connectorName,
|
|
1997
|
+
universe,
|
|
1998
|
+
accountId,
|
|
1999
|
+
deploymentId
|
|
2000
|
+
})
|
|
2001
|
+
);
|
|
2002
|
+
if (!connector) {
|
|
2003
|
+
throw new Error(`Unknown connector: ${connectorName}`);
|
|
2004
|
+
}
|
|
2005
|
+
const strategyCreator = await withTimeout(
|
|
2006
|
+
"strategy lookup",
|
|
2007
|
+
getStrategyCreator(strategyName, projectRoot)
|
|
2008
|
+
);
|
|
2009
|
+
if (!strategyCreator) {
|
|
2010
|
+
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
2011
|
+
}
|
|
2012
|
+
const preparedData = await withTimeout(
|
|
2013
|
+
"kline preload",
|
|
2014
|
+
prepareTestingData({
|
|
2015
|
+
state,
|
|
2016
|
+
projectRoot,
|
|
2017
|
+
userName,
|
|
2018
|
+
connectorName,
|
|
2019
|
+
symbol,
|
|
2020
|
+
preloadStart,
|
|
2021
|
+
start,
|
|
2022
|
+
end,
|
|
2023
|
+
interval,
|
|
2024
|
+
universe,
|
|
2025
|
+
accountId,
|
|
2026
|
+
deploymentId
|
|
2027
|
+
})
|
|
2028
|
+
);
|
|
2029
|
+
if (!preparedData) {
|
|
2030
|
+
throw new Error("Prepared backtest data not available");
|
|
2031
|
+
}
|
|
2032
|
+
const {
|
|
2033
|
+
prevData,
|
|
2034
|
+
btcPrevData,
|
|
2035
|
+
ethPrevData,
|
|
2036
|
+
ethTestData,
|
|
2037
|
+
testData,
|
|
2038
|
+
btcTestData,
|
|
2039
|
+
btcBinanceData,
|
|
2040
|
+
btcCoinbaseData,
|
|
2041
|
+
backtestExecutionInterval,
|
|
2042
|
+
backtestExecutionData,
|
|
2043
|
+
backtestExecutionBtcData,
|
|
2044
|
+
backtestExecutionDataByTimestamp,
|
|
2045
|
+
backtestExecutionBtcDataByTimestamp
|
|
2046
|
+
} = preparedData;
|
|
2047
|
+
totalCandles = testData.length;
|
|
2048
|
+
const sharedIndicatorsReplayKey = [
|
|
2049
|
+
"shared",
|
|
2050
|
+
userName,
|
|
2051
|
+
connectorName,
|
|
2052
|
+
strategyName,
|
|
2053
|
+
symbol,
|
|
2054
|
+
interval,
|
|
2055
|
+
start,
|
|
2056
|
+
end,
|
|
2057
|
+
chunkId
|
|
2058
|
+
].join(":");
|
|
2059
|
+
const runners = [];
|
|
2060
|
+
try {
|
|
2061
|
+
for (const test of tests) {
|
|
2062
|
+
const instrument = test.instrument;
|
|
2063
|
+
const { model: executionCostModel, fundingRates } = await resolveExecutionCosts({
|
|
2064
|
+
connector,
|
|
2065
|
+
symbol: test.symbol,
|
|
2066
|
+
config: test.strategyConfig,
|
|
2067
|
+
startTime: start,
|
|
2068
|
+
endTime: end,
|
|
2069
|
+
instrument
|
|
2070
|
+
});
|
|
2071
|
+
const testConnector = createTestConnector(connector, {
|
|
2072
|
+
userName: test.userName,
|
|
2073
|
+
mlEnabled: test.ml,
|
|
2074
|
+
aiEnabled: test.ai,
|
|
2075
|
+
fastMode: test.fast,
|
|
2076
|
+
executionCostModel,
|
|
2077
|
+
fundingRates
|
|
2078
|
+
});
|
|
2079
|
+
const strategy = await withTimeout(
|
|
2080
|
+
"strategy init",
|
|
2081
|
+
strategyCreator({
|
|
2082
|
+
userName: test.userName,
|
|
2083
|
+
connectorName: test.connectorName,
|
|
2084
|
+
universe: test.universe ?? universe,
|
|
2085
|
+
assetClass: test.assetClass ?? instrument?.assetClass,
|
|
2086
|
+
instrument,
|
|
2087
|
+
accountId: test.accountId ?? accountId,
|
|
2088
|
+
deploymentId: test.deploymentId ?? deploymentId,
|
|
2089
|
+
policyProfileId: test.policyProfileId,
|
|
2090
|
+
config: {
|
|
2091
|
+
...test.strategyConfig,
|
|
2092
|
+
INTERVAL: test.interval ?? interval
|
|
2093
|
+
},
|
|
2094
|
+
symbol: test.symbol,
|
|
2095
|
+
data: prevData.slice(),
|
|
2096
|
+
btcData: btcPrevData.slice(),
|
|
2097
|
+
ethData: [...ethPrevData, ...ethTestData],
|
|
2098
|
+
btcBinanceData,
|
|
2099
|
+
btcCoinbaseData,
|
|
2100
|
+
backtestExecutionMarketData: {
|
|
2101
|
+
interval: backtestExecutionInterval,
|
|
2102
|
+
data: backtestExecutionData,
|
|
2103
|
+
btcData: backtestExecutionBtcData,
|
|
2104
|
+
dataByTimestamp: backtestExecutionDataByTimestamp,
|
|
2105
|
+
btcDataByTimestamp: backtestExecutionBtcDataByTimestamp
|
|
2106
|
+
},
|
|
2107
|
+
connector: testConnector,
|
|
2108
|
+
sharedIndicatorsReplayKey
|
|
2109
|
+
})
|
|
2110
|
+
);
|
|
2111
|
+
runners.push({
|
|
2112
|
+
test,
|
|
2113
|
+
strategy,
|
|
2114
|
+
testConnector,
|
|
2115
|
+
pendingMlPayloadBySignalId: /* @__PURE__ */ new Map(),
|
|
2116
|
+
pendingAiRowBySignalId: /* @__PURE__ */ new Map(),
|
|
2117
|
+
replaySignalEvaluations: collectReplaySignalEvaluations ? [] : null
|
|
2118
|
+
});
|
|
2119
|
+
}
|
|
2120
|
+
const flushClosedResultsBatch = async (runner) => {
|
|
2121
|
+
if (!runner.test.ml && !runner.test.ai) return;
|
|
2122
|
+
const batch = await runner.testConnector.drainMlResultsBatch();
|
|
2123
|
+
if (!batch.length) return;
|
|
2124
|
+
for (const resultRecord of batch) {
|
|
2125
|
+
const payload = runner.pendingMlPayloadBySignalId.get(
|
|
2126
|
+
resultRecord.signalId
|
|
2127
|
+
);
|
|
2128
|
+
if (payload) {
|
|
2129
|
+
runner.pendingMlPayloadBySignalId.delete(resultRecord.signalId);
|
|
2130
|
+
const fullRow = buildMlTrainingRow(payload, {
|
|
2131
|
+
profit: resultRecord.profit
|
|
2132
|
+
});
|
|
2133
|
+
const resolvedChunkId = runner.test.chunkId ?? "single";
|
|
2134
|
+
const row = {
|
|
2135
|
+
...trimMlTrainingRowWindows(fullRow, 5),
|
|
2136
|
+
...buildBacktestDatasetMetadata({
|
|
2137
|
+
backtestRunId: runner.test.backtestRunId,
|
|
2138
|
+
backtestTestKey: runner.test.backtestTestKey,
|
|
2139
|
+
chunkId: resolvedChunkId
|
|
2140
|
+
})
|
|
2141
|
+
};
|
|
2142
|
+
await appendMlDatasetRow({
|
|
2143
|
+
strategyName: runner.test.strategyName,
|
|
2144
|
+
chunkId: resolvedChunkId,
|
|
2145
|
+
row
|
|
2146
|
+
});
|
|
2147
|
+
}
|
|
2148
|
+
const aiRowBase = runner.pendingAiRowBySignalId.get(
|
|
2149
|
+
resultRecord.signalId
|
|
2150
|
+
);
|
|
2151
|
+
if (aiRowBase) {
|
|
2152
|
+
runner.pendingAiRowBySignalId.delete(resultRecord.signalId);
|
|
2153
|
+
const { signal: aiSignal, ...rowBase } = aiRowBase;
|
|
2154
|
+
const resolvedChunkId = runner.test.chunkId ?? "single";
|
|
2155
|
+
await appendAiDatasetRow({
|
|
2156
|
+
strategyName: runner.test.strategyName,
|
|
2157
|
+
chunkId: resolvedChunkId,
|
|
2158
|
+
row: {
|
|
2159
|
+
...rowBase,
|
|
2160
|
+
payload: buildAiPayload(aiSignal),
|
|
2161
|
+
profit: resultRecord.profit,
|
|
2162
|
+
tradeResult: resultRecord.tradeResult
|
|
2163
|
+
}
|
|
2164
|
+
});
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
};
|
|
2168
|
+
const processRunnerSignal = async (runner, signal, candle) => {
|
|
2169
|
+
if (isBacktestEntryDelayControlCode(signal)) {
|
|
2170
|
+
return;
|
|
2171
|
+
}
|
|
2172
|
+
const { test } = runner;
|
|
2173
|
+
if (runner.replaySignalEvaluations) {
|
|
2174
|
+
runner.replaySignalEvaluations.push(
|
|
2175
|
+
buildReplaySignalEvaluationRecord({
|
|
2176
|
+
signal,
|
|
2177
|
+
testId: test.testId,
|
|
2178
|
+
userName: test.userName,
|
|
2179
|
+
strategyName: test.strategyName,
|
|
2180
|
+
symbol: test.symbol,
|
|
2181
|
+
interval: test.interval ?? interval,
|
|
2182
|
+
candle
|
|
2183
|
+
})
|
|
2184
|
+
);
|
|
2185
|
+
}
|
|
2186
|
+
const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (test.ml || test.ai);
|
|
2187
|
+
if (shouldCapturePayload) {
|
|
2188
|
+
await withTimeout(
|
|
2189
|
+
"binance market context",
|
|
2190
|
+
enrichSignalWithBinanceMarketContext({
|
|
2191
|
+
signal,
|
|
2192
|
+
env: "BACKTEST"
|
|
2193
|
+
})
|
|
2194
|
+
);
|
|
2195
|
+
await withTimeout(
|
|
2196
|
+
"coinmarketcap context",
|
|
2197
|
+
enrichSignalWithCoinMarketCapContext({
|
|
2198
|
+
signal,
|
|
2199
|
+
env: "BACKTEST",
|
|
2200
|
+
enabled: Boolean(test.ml || test.ai)
|
|
2201
|
+
})
|
|
2202
|
+
);
|
|
2203
|
+
await withTimeout(
|
|
2204
|
+
"derivatives context",
|
|
2205
|
+
enrichSignalWithDerivativesContext({
|
|
2206
|
+
signal,
|
|
2207
|
+
env: "BACKTEST"
|
|
2208
|
+
})
|
|
2209
|
+
);
|
|
2210
|
+
}
|
|
2211
|
+
if (test.ml && signal && typeof signal !== "string" && signal.signalId) {
|
|
2212
|
+
const payload = buildMlPayload({
|
|
2213
|
+
signal,
|
|
2214
|
+
context: {
|
|
2215
|
+
userName: test.userName,
|
|
2216
|
+
testId: test.testId,
|
|
2217
|
+
testSuiteId: test.testSuiteId,
|
|
2218
|
+
testName: test.name,
|
|
2219
|
+
configId: test.configId,
|
|
2220
|
+
symbol: test.symbol,
|
|
2221
|
+
strategyName: test.strategyName,
|
|
2222
|
+
strategyConfig: test.strategyConfig,
|
|
2223
|
+
connectorName: test.connectorName
|
|
2224
|
+
}
|
|
2225
|
+
});
|
|
2226
|
+
runner.pendingMlPayloadBySignalId.set(signal.signalId, payload);
|
|
2227
|
+
}
|
|
2228
|
+
if (test.ai && signal && typeof signal !== "string" && signal.signalId) {
|
|
2229
|
+
runner.pendingAiRowBySignalId.set(signal.signalId, {
|
|
2230
|
+
signalId: signal.signalId,
|
|
2231
|
+
strategyName: signal.strategy || test.strategyName,
|
|
2232
|
+
symbol: signal.symbol || test.symbol,
|
|
2233
|
+
direction: signal.direction,
|
|
2234
|
+
timestamp: signal.timestamp,
|
|
2235
|
+
signal: cloneAiPayloadSignal(signal),
|
|
2236
|
+
testId: test.testId,
|
|
2237
|
+
testSuiteId: test.testSuiteId,
|
|
2238
|
+
testName: test.name,
|
|
2239
|
+
configId: test.configId,
|
|
2240
|
+
connectorName: test.connectorName,
|
|
2241
|
+
...buildBacktestDatasetMetadata({
|
|
2242
|
+
backtestRunId: test.backtestRunId,
|
|
2243
|
+
backtestTestKey: test.backtestTestKey,
|
|
2244
|
+
chunkId: test.chunkId ?? "single"
|
|
2245
|
+
})
|
|
2246
|
+
});
|
|
2247
|
+
}
|
|
2248
|
+
};
|
|
2249
|
+
for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
|
|
2250
|
+
if (candleIndex % 25 === 0) {
|
|
2251
|
+
throwIfTimedOut("candle loop");
|
|
2252
|
+
}
|
|
2253
|
+
currentCandleIndex = candleIndex + 1;
|
|
2254
|
+
emitProgress("candle loop", {
|
|
2255
|
+
force: candleIndex === 0 || currentCandleIndex === totalCandles
|
|
2256
|
+
});
|
|
2257
|
+
const candle = testData[candleIndex];
|
|
2258
|
+
const btcCandle = btcTestData[candleIndex];
|
|
2259
|
+
const detectorNoSignalByKey = /* @__PURE__ */ new Map();
|
|
2260
|
+
for (const runner of runners) {
|
|
2261
|
+
const { test, testConnector, strategy } = runner;
|
|
2262
|
+
const delayedSignal = await runStrategyCandleStage(
|
|
2263
|
+
"delayed entry",
|
|
2264
|
+
async () => strategy.__tradejsFlushBacktestDelayedEntry?.(candle, btcCandle)
|
|
2265
|
+
);
|
|
2266
|
+
if (delayedSignal && typeof delayedSignal !== "string") {
|
|
2267
|
+
await processRunnerSignal(runner, delayedSignal, candle);
|
|
2268
|
+
}
|
|
2269
|
+
await runStage("exit checks", () => testConnector.checkExits(candle));
|
|
2270
|
+
const detectorFanoutKey = strategy.detectorFanoutKey;
|
|
2271
|
+
const detectorSkipCode = detectorFanoutKey ? detectorNoSignalByKey.get(detectorFanoutKey) : void 0;
|
|
2272
|
+
const signal = await runStrategyCandleStage(
|
|
2273
|
+
detectorSkipCode ? "strategy detector skip" : "strategy signal",
|
|
2274
|
+
() => detectorSkipCode && strategy.canFastAdvanceDetectorNoSignal && strategy.advanceDetectorNoSignal ? strategy.advanceDetectorNoSignal(
|
|
2275
|
+
candle,
|
|
2276
|
+
btcCandle,
|
|
2277
|
+
detectorSkipCode
|
|
2278
|
+
) : detectorSkipCode && strategy.skipDetectorNoSignal ? strategy.skipDetectorNoSignal(
|
|
2279
|
+
candle,
|
|
2280
|
+
btcCandle,
|
|
2281
|
+
detectorSkipCode
|
|
2282
|
+
) : strategy(candle, btcCandle)
|
|
2283
|
+
);
|
|
2284
|
+
if (detectorFanoutKey && strategy.detectorNoSignalSkipReason && typeof signal === "string" && signal === strategy.detectorNoSignalSkipReason) {
|
|
2285
|
+
detectorNoSignalByKey.set(detectorFanoutKey, signal);
|
|
2286
|
+
}
|
|
2287
|
+
await processRunnerSignal(runner, signal, candle);
|
|
2288
|
+
}
|
|
2289
|
+
if ((candleIndex + 1) % CLOSED_RESULT_FLUSH_INTERVAL === 0) {
|
|
2290
|
+
await withTimeout(
|
|
2291
|
+
"flush closed results",
|
|
2292
|
+
Promise.all(runners.map((runner) => flushClosedResultsBatch(runner)))
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
const results = [];
|
|
2297
|
+
for (const runner of runners) {
|
|
2298
|
+
await withTimeout(
|
|
2299
|
+
"flush closed results",
|
|
2300
|
+
flushClosedResultsBatch(runner)
|
|
2301
|
+
);
|
|
2302
|
+
const result = await withTimeout(
|
|
2303
|
+
"collect result",
|
|
2304
|
+
runner.testConnector.getResult()
|
|
2305
|
+
);
|
|
2306
|
+
results.push({
|
|
2307
|
+
test: runner.test,
|
|
2308
|
+
result: runner.replaySignalEvaluations ? {
|
|
2309
|
+
...result,
|
|
2310
|
+
inlineReplaySignalEvaluations: runner.replaySignalEvaluations
|
|
2311
|
+
} : result
|
|
2312
|
+
});
|
|
2313
|
+
}
|
|
2314
|
+
return results;
|
|
2315
|
+
} finally {
|
|
2316
|
+
releaseStrategyIndicatorsReplayCache(sharedIndicatorsReplayKey);
|
|
2317
|
+
releaseStrategyReplayCache(sharedIndicatorsReplayKey);
|
|
2318
|
+
}
|
|
747
2319
|
};
|
|
748
2320
|
export {
|
|
2321
|
+
canRunTestsInSharedCandleLoop,
|
|
749
2322
|
createTestConnector,
|
|
2323
|
+
releaseTestingSymbolCache,
|
|
750
2324
|
resetTestingKlineCache,
|
|
751
|
-
|
|
2325
|
+
resolveExecutionCosts,
|
|
2326
|
+
testing,
|
|
2327
|
+
testingGroupInSharedCandleLoop
|
|
752
2328
|
};
|