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