@tradejs/node 1.0.5 → 1.0.8
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 +31 -0
- package/dist/ai.d.ts +31 -0
- package/dist/ai.js +1074 -0
- package/dist/ai.mjs +35 -0
- package/dist/backtest.js +713 -5689
- package/dist/backtest.mjs +418 -180
- package/dist/chunk-2JKX3DM7.mjs +619 -0
- package/dist/{chunk-7ICOZAKA.mjs → chunk-H6LIYHU4.mjs} +29 -11
- package/dist/{chunk-CIY64D57.mjs → chunk-JMDYEKIO.mjs} +1 -1
- package/dist/chunk-JRRG3YQG.mjs +154 -0
- package/dist/{chunk-P2ZUWONT.mjs → chunk-JU77QVJ3.mjs} +9 -2
- package/dist/{chunk-SCMBUEGK.mjs → chunk-KMJQQ53K.mjs} +1 -1
- package/dist/{chunk-GKDBAF3A.mjs → chunk-KZDHZ56N.mjs} +25 -7
- package/dist/{chunk-ZY6ULOWK.mjs → chunk-WGOYR6AB.mjs} +1 -1
- package/dist/cli.d.mts +23 -5
- package/dist/cli.d.ts +23 -5
- package/dist/cli.js +1091 -232
- package/dist/cli.mjs +342 -108
- package/dist/connectors.js +7 -2
- package/dist/connectors.mjs +2 -2
- package/dist/constants.js +1 -1
- package/dist/constants.mjs +1 -1
- package/dist/pine.d.mts +10 -6
- package/dist/pine.d.ts +10 -6
- package/dist/pine.js +40 -18
- package/dist/pine.mjs +21 -13
- package/dist/registry.js +7 -2
- package/dist/registry.mjs +2 -2
- package/dist/strategies.d.mts +28 -19
- package/dist/strategies.d.ts +28 -19
- package/dist/strategies.js +7690 -6349
- package/dist/strategies.mjs +1060 -169
- package/package.json +11 -6
- package/dist/ai-G7ATN4YL.mjs +0 -19
- package/dist/chunk-LMAKIC3C.mjs +0 -294
- package/dist/chunk-RBE4PZER.mjs +0 -49
package/dist/backtest.mjs
CHANGED
|
@@ -1,30 +1,29 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BUILTIN_CONNECTOR_NAMES,
|
|
3
3
|
getConnectorCreatorByName
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JMDYEKIO.mjs";
|
|
5
5
|
import {
|
|
6
|
-
buildMlPayload
|
|
7
|
-
|
|
6
|
+
buildMlPayload,
|
|
7
|
+
enrichSignalWithDerivativesContext
|
|
8
|
+
} from "./chunk-JRRG3YQG.mjs";
|
|
8
9
|
import {
|
|
9
|
-
|
|
10
|
-
} from "./chunk-
|
|
10
|
+
buildAiPayload
|
|
11
|
+
} from "./chunk-2JKX3DM7.mjs";
|
|
11
12
|
import {
|
|
12
13
|
getStrategyCreator
|
|
13
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-WGOYR6AB.mjs";
|
|
14
15
|
import {
|
|
15
16
|
getTradejsProjectCwd
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import
|
|
18
|
-
__toESM
|
|
19
|
-
} from "./chunk-6DZX6EAA.mjs";
|
|
17
|
+
} from "./chunk-JU77QVJ3.mjs";
|
|
18
|
+
import "./chunk-6DZX6EAA.mjs";
|
|
20
19
|
|
|
21
20
|
// src/backtest.ts
|
|
22
21
|
export * from "@tradejs/core/backtest";
|
|
23
22
|
|
|
24
23
|
// src/testing.ts
|
|
25
24
|
import { alignSortedCandlesByTimestamp } from "@tradejs/core/indicators";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
25
|
+
import { getBacktestPreloadStart } from "@tradejs/core/time";
|
|
26
|
+
import { appendAiDatasetRow } from "@tradejs/infra/ai";
|
|
28
27
|
import {
|
|
29
28
|
appendMlDatasetRow,
|
|
30
29
|
buildMlTrainingRow,
|
|
@@ -33,15 +32,11 @@ import {
|
|
|
33
32
|
import { logger } from "@tradejs/infra/logger";
|
|
34
33
|
|
|
35
34
|
// src/testConnector.ts
|
|
36
|
-
var import_lodash = __toESM(require_lodash());
|
|
37
35
|
import { randomUUID } from "crypto";
|
|
38
|
-
import { TTL_1D } from "@tradejs/core/constants";
|
|
39
36
|
import { round } from "@tradejs/core/math";
|
|
40
|
-
import { redisKeys, setData } from "@tradejs/infra/redis";
|
|
41
37
|
var FEE = 5e-3;
|
|
42
38
|
var INITIAL_AMOUNT = 100;
|
|
43
39
|
var createTestConnector = (connector, context) => {
|
|
44
|
-
const userName = context?.userName;
|
|
45
40
|
let state = {};
|
|
46
41
|
const orderLog = [];
|
|
47
42
|
const positionLog = [];
|
|
@@ -51,7 +46,7 @@ var createTestConnector = (connector, context) => {
|
|
|
51
46
|
let currentPositionProfit = 0;
|
|
52
47
|
let takeProfits = [];
|
|
53
48
|
let stopLossPrice = null;
|
|
54
|
-
const
|
|
49
|
+
const closedSignalResults = [];
|
|
55
50
|
const logOrder = (data) => {
|
|
56
51
|
const nextEntry = {
|
|
57
52
|
...currentPosition || {},
|
|
@@ -61,7 +56,8 @@ var createTestConnector = (connector, context) => {
|
|
|
61
56
|
index: orderLog.length
|
|
62
57
|
};
|
|
63
58
|
if (nextEntry.signal) {
|
|
64
|
-
|
|
59
|
+
const { indicators: _indicators, ...signalWithoutIndicators } = nextEntry.signal;
|
|
60
|
+
nextEntry.signal = signalWithoutIndicators;
|
|
65
61
|
}
|
|
66
62
|
orderLog.push(nextEntry);
|
|
67
63
|
};
|
|
@@ -72,10 +68,10 @@ var createTestConnector = (connector, context) => {
|
|
|
72
68
|
if (!currentPosition) {
|
|
73
69
|
return;
|
|
74
70
|
}
|
|
75
|
-
if (context?.mlEnabled) {
|
|
71
|
+
if (context?.mlEnabled || context?.aiEnabled) {
|
|
76
72
|
const signalId = currentPosition.signal?.signalId;
|
|
77
73
|
if (signalId) {
|
|
78
|
-
|
|
74
|
+
closedSignalResults.push({
|
|
79
75
|
signalId,
|
|
80
76
|
profit: currentPositionProfit
|
|
81
77
|
});
|
|
@@ -96,6 +92,7 @@ var createTestConnector = (connector, context) => {
|
|
|
96
92
|
currentPositionProfit = 0;
|
|
97
93
|
};
|
|
98
94
|
return {
|
|
95
|
+
__tradejsTestConnector: true,
|
|
99
96
|
getState: async () => state,
|
|
100
97
|
setState: async (newState) => {
|
|
101
98
|
state = {
|
|
@@ -106,33 +103,38 @@ var createTestConnector = (connector, context) => {
|
|
|
106
103
|
kline: async (options) => connector.kline(options),
|
|
107
104
|
getResult: async () => {
|
|
108
105
|
const orderLogId = randomUUID().slice(-12);
|
|
109
|
-
const cacheUserName = userName || "root";
|
|
110
|
-
await setData(
|
|
111
|
-
redisKeys.cacheOrders(cacheUserName, orderLogId),
|
|
112
|
-
orderLog,
|
|
113
|
-
{
|
|
114
|
-
expire: TTL_1D
|
|
115
|
-
}
|
|
116
|
-
);
|
|
117
|
-
await setData(
|
|
118
|
-
redisKeys.cachePositions(cacheUserName, orderLogId),
|
|
119
|
-
positionLog,
|
|
120
|
-
{
|
|
121
|
-
expire: TTL_1D
|
|
122
|
-
}
|
|
123
|
-
);
|
|
124
106
|
return {
|
|
125
107
|
stat: {
|
|
126
108
|
amount,
|
|
127
109
|
profit: amount - INITIAL_AMOUNT,
|
|
128
110
|
orders: positionLog.length
|
|
129
111
|
},
|
|
130
|
-
orderLogId
|
|
112
|
+
orderLogId,
|
|
113
|
+
inlineOrderLog: [...orderLog],
|
|
114
|
+
inlinePositionLog: [...positionLog]
|
|
131
115
|
};
|
|
132
116
|
},
|
|
133
117
|
getPosition: async () => currentPosition || null,
|
|
118
|
+
getOpenPositionPnl: async () => {
|
|
119
|
+
if (typeof connector.getOpenPositionPnl === "function") {
|
|
120
|
+
return connector.getOpenPositionPnl();
|
|
121
|
+
}
|
|
122
|
+
if (!currentPosition) {
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
return [
|
|
126
|
+
{
|
|
127
|
+
symbol: currentPosition.symbol,
|
|
128
|
+
qty: currentPosition.qty,
|
|
129
|
+
price: currentPosition.price,
|
|
130
|
+
currentPrice: currentPosition.price,
|
|
131
|
+
unrealizedPnl: 0,
|
|
132
|
+
direction: currentPosition.direction
|
|
133
|
+
}
|
|
134
|
+
];
|
|
135
|
+
},
|
|
134
136
|
checkTp: async (candle) => {
|
|
135
|
-
if (
|
|
137
|
+
if (!candle || !currentPosition || !currentPosition.qty) {
|
|
136
138
|
return;
|
|
137
139
|
}
|
|
138
140
|
const isLong = currentPosition.direction === "LONG";
|
|
@@ -167,7 +169,7 @@ var createTestConnector = (connector, context) => {
|
|
|
167
169
|
}
|
|
168
170
|
},
|
|
169
171
|
checkSl: async (candle) => {
|
|
170
|
-
if (!stopLossPrice || !currentPosition ||
|
|
172
|
+
if (!stopLossPrice || !currentPosition || !candle) {
|
|
171
173
|
return;
|
|
172
174
|
}
|
|
173
175
|
const isLong = currentPosition.direction === "LONG";
|
|
@@ -187,13 +189,11 @@ var createTestConnector = (connector, context) => {
|
|
|
187
189
|
clearPosition(candle.timestamp);
|
|
188
190
|
}
|
|
189
191
|
},
|
|
190
|
-
placeOrder: async (order
|
|
192
|
+
placeOrder: async (order) => {
|
|
191
193
|
if (currentPosition) {
|
|
192
194
|
return false;
|
|
193
195
|
}
|
|
194
196
|
const isLong = order.direction === "LONG";
|
|
195
|
-
takeProfits = import_lodash.default.cloneDeep(tp);
|
|
196
|
-
stopLossPrice = slPrice || null;
|
|
197
197
|
currentPosition = { ...order, amount };
|
|
198
198
|
originalQty = order.qty;
|
|
199
199
|
const fee = order.price * order.qty * FEE;
|
|
@@ -208,6 +208,24 @@ var createTestConnector = (connector, context) => {
|
|
|
208
208
|
});
|
|
209
209
|
return true;
|
|
210
210
|
},
|
|
211
|
+
setTakeProfits: async ({ takeProfits: nextTakeProfits }) => {
|
|
212
|
+
if (!currentPosition) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
takeProfits = Array.isArray(nextTakeProfits) ? nextTakeProfits.map((tp) => ({ ...tp })) : [];
|
|
216
|
+
return true;
|
|
217
|
+
},
|
|
218
|
+
setStopLoss: async ({ stopLossPrice: nextStopLossPrice }) => {
|
|
219
|
+
if (!currentPosition) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
stopLossPrice = nextStopLossPrice || null;
|
|
223
|
+
currentPosition = {
|
|
224
|
+
...currentPosition,
|
|
225
|
+
...stopLossPrice != null ? { slPrice: stopLossPrice } : {}
|
|
226
|
+
};
|
|
227
|
+
return true;
|
|
228
|
+
},
|
|
211
229
|
closePosition: async (order) => {
|
|
212
230
|
if (!currentPosition) {
|
|
213
231
|
return false;
|
|
@@ -227,17 +245,18 @@ var createTestConnector = (connector, context) => {
|
|
|
227
245
|
},
|
|
228
246
|
getTickers: connector.getTickers,
|
|
229
247
|
getPositions: connector.getPositions,
|
|
230
|
-
drainMlResultsBatch: async () =>
|
|
248
|
+
drainMlResultsBatch: async () => closedSignalResults.splice(0)
|
|
231
249
|
};
|
|
232
250
|
};
|
|
233
251
|
|
|
234
252
|
// src/testing.ts
|
|
235
|
-
var preloadStart = getTimestamp(PRELOAD_DAYS);
|
|
236
253
|
var createTestingKlineCacheState = () => ({
|
|
237
254
|
coinKlineCache: /* @__PURE__ */ new Map(),
|
|
238
255
|
btcKlineCache: /* @__PURE__ */ new Map(),
|
|
239
256
|
btcBinanceKlineCache: /* @__PURE__ */ new Map(),
|
|
240
|
-
btcCoinbaseKlineCache: /* @__PURE__ */ new Map()
|
|
257
|
+
btcCoinbaseKlineCache: /* @__PURE__ */ new Map(),
|
|
258
|
+
preparedDataCache: /* @__PURE__ */ new Map(),
|
|
259
|
+
connectorCache: /* @__PURE__ */ new Map()
|
|
241
260
|
});
|
|
242
261
|
var testingKlineCacheStateByProjectRoot = /* @__PURE__ */ new Map();
|
|
243
262
|
var getTestingKlineCacheState = (cwd = getTradejsProjectCwd()) => {
|
|
@@ -253,7 +272,15 @@ var getTestingKlineCacheState = (cwd = getTradejsProjectCwd()) => {
|
|
|
253
272
|
};
|
|
254
273
|
};
|
|
255
274
|
var getKlineCacheKey = (params) => {
|
|
256
|
-
const {
|
|
275
|
+
const {
|
|
276
|
+
userName,
|
|
277
|
+
connectorName,
|
|
278
|
+
symbol,
|
|
279
|
+
preloadStart,
|
|
280
|
+
end,
|
|
281
|
+
interval,
|
|
282
|
+
cacheOnly
|
|
283
|
+
} = params;
|
|
257
284
|
return [
|
|
258
285
|
userName,
|
|
259
286
|
connectorName,
|
|
@@ -264,6 +291,64 @@ var getKlineCacheKey = (params) => {
|
|
|
264
291
|
cacheOnly ? 1 : 0
|
|
265
292
|
].join(":");
|
|
266
293
|
};
|
|
294
|
+
var getPreparedDataCacheKey = (params) => {
|
|
295
|
+
const {
|
|
296
|
+
userName,
|
|
297
|
+
connectorName,
|
|
298
|
+
symbol,
|
|
299
|
+
preloadStart,
|
|
300
|
+
start,
|
|
301
|
+
end,
|
|
302
|
+
interval,
|
|
303
|
+
btcBinanceConnectorName,
|
|
304
|
+
btcCoinbaseConnectorName
|
|
305
|
+
} = params;
|
|
306
|
+
return [
|
|
307
|
+
userName,
|
|
308
|
+
connectorName,
|
|
309
|
+
symbol,
|
|
310
|
+
preloadStart,
|
|
311
|
+
start,
|
|
312
|
+
end,
|
|
313
|
+
interval,
|
|
314
|
+
btcBinanceConnectorName,
|
|
315
|
+
btcCoinbaseConnectorName
|
|
316
|
+
].join(":");
|
|
317
|
+
};
|
|
318
|
+
var getConnectorCacheKey = (params) => [params.userName, params.connectorName].join(":");
|
|
319
|
+
var splitCandlesForTesting = (candles, start, preloadStart) => {
|
|
320
|
+
const prevData = [];
|
|
321
|
+
const testData = [];
|
|
322
|
+
for (const candle of candles) {
|
|
323
|
+
if (candle.timestamp < preloadStart) continue;
|
|
324
|
+
if (candle.timestamp < start) {
|
|
325
|
+
prevData.push(candle);
|
|
326
|
+
} else {
|
|
327
|
+
testData.push(candle);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return { prevData, testData };
|
|
331
|
+
};
|
|
332
|
+
var getCachedConnector = async (params) => {
|
|
333
|
+
const { state, projectRoot, userName, connectorName } = params;
|
|
334
|
+
const cacheKey = getConnectorCacheKey({ userName, connectorName });
|
|
335
|
+
const cachedConnector = state.connectorCache.get(cacheKey);
|
|
336
|
+
if (cachedConnector) {
|
|
337
|
+
return cachedConnector;
|
|
338
|
+
}
|
|
339
|
+
const connectorCreator = await getConnectorCreatorByName(
|
|
340
|
+
connectorName,
|
|
341
|
+
projectRoot
|
|
342
|
+
);
|
|
343
|
+
if (!connectorCreator) {
|
|
344
|
+
return void 0;
|
|
345
|
+
}
|
|
346
|
+
const connector = await connectorCreator({
|
|
347
|
+
userName
|
|
348
|
+
});
|
|
349
|
+
state.connectorCache.set(cacheKey, connector);
|
|
350
|
+
return connector;
|
|
351
|
+
};
|
|
267
352
|
var resetTestingKlineCache = (cwd) => {
|
|
268
353
|
const normalizedCwd = String(cwd ?? "").trim();
|
|
269
354
|
if (!normalizedCwd) {
|
|
@@ -285,35 +370,89 @@ var testing = async ({
|
|
|
285
370
|
strategyConfig,
|
|
286
371
|
connectorName,
|
|
287
372
|
ml = false,
|
|
288
|
-
|
|
373
|
+
ai = false,
|
|
374
|
+
chunkId = "single",
|
|
375
|
+
timeoutMs
|
|
289
376
|
}) => {
|
|
290
377
|
if (!start) {
|
|
291
378
|
throw new Error("no start");
|
|
292
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;
|
|
400
|
+
}
|
|
401
|
+
return await new Promise((resolve, reject) => {
|
|
402
|
+
const timer = setTimeout(() => {
|
|
403
|
+
reject(new Error(formatTimeoutMessage(stage)));
|
|
404
|
+
}, remainingMs);
|
|
405
|
+
promise.then(
|
|
406
|
+
(value) => {
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
resolve(value);
|
|
409
|
+
},
|
|
410
|
+
(error) => {
|
|
411
|
+
clearTimeout(timer);
|
|
412
|
+
reject(error);
|
|
413
|
+
}
|
|
414
|
+
);
|
|
415
|
+
});
|
|
416
|
+
};
|
|
293
417
|
const { projectRoot, state } = getTestingKlineCacheState();
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
418
|
+
const connector = await withTimeout(
|
|
419
|
+
"connector init",
|
|
420
|
+
getCachedConnector({
|
|
421
|
+
state,
|
|
422
|
+
projectRoot,
|
|
423
|
+
userName,
|
|
424
|
+
connectorName
|
|
425
|
+
})
|
|
297
426
|
);
|
|
298
|
-
if (!
|
|
427
|
+
if (!connector) {
|
|
299
428
|
throw new Error(`Unknown connector: ${connectorName}`);
|
|
300
429
|
}
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
430
|
+
const strategyCreator = await withTimeout(
|
|
431
|
+
"strategy lookup",
|
|
432
|
+
getStrategyCreator(strategyName, projectRoot)
|
|
433
|
+
);
|
|
305
434
|
if (!strategyCreator) {
|
|
306
435
|
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
307
436
|
}
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
437
|
+
const binanceConnector = await withTimeout(
|
|
438
|
+
"binance connector init",
|
|
439
|
+
getCachedConnector({
|
|
440
|
+
state,
|
|
441
|
+
projectRoot,
|
|
442
|
+
userName,
|
|
443
|
+
connectorName: BUILTIN_CONNECTOR_NAMES.Binance
|
|
444
|
+
})
|
|
311
445
|
);
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
446
|
+
const coinbaseConnector = await withTimeout(
|
|
447
|
+
"coinbase connector init",
|
|
448
|
+
getCachedConnector({
|
|
449
|
+
state,
|
|
450
|
+
projectRoot,
|
|
451
|
+
userName,
|
|
452
|
+
connectorName: BUILTIN_CONNECTOR_NAMES.Coinbase
|
|
453
|
+
})
|
|
315
454
|
);
|
|
316
|
-
if (!
|
|
455
|
+
if (!binanceConnector || !coinbaseConnector) {
|
|
317
456
|
logger.warn(
|
|
318
457
|
"Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
|
|
319
458
|
connectorName
|
|
@@ -325,6 +464,7 @@ var testing = async ({
|
|
|
325
464
|
userName,
|
|
326
465
|
connectorName,
|
|
327
466
|
symbol,
|
|
467
|
+
preloadStart,
|
|
328
468
|
end,
|
|
329
469
|
interval,
|
|
330
470
|
cacheOnly
|
|
@@ -333,160 +473,240 @@ var testing = async ({
|
|
|
333
473
|
userName,
|
|
334
474
|
connectorName,
|
|
335
475
|
symbol: "BTCUSDT",
|
|
476
|
+
preloadStart,
|
|
336
477
|
end,
|
|
337
478
|
interval,
|
|
338
479
|
cacheOnly
|
|
339
480
|
});
|
|
481
|
+
const btcBinanceConnectorName = binanceConnector ? BUILTIN_CONNECTOR_NAMES.Binance : connectorName;
|
|
482
|
+
const btcCoinbaseConnectorName = coinbaseConnector ? BUILTIN_CONNECTOR_NAMES.Coinbase : connectorName;
|
|
340
483
|
const cachedCoinData = state.coinKlineCache.get(coinCacheKey);
|
|
341
484
|
const cachedBtcData = state.btcKlineCache.get(btcCacheKey);
|
|
342
485
|
const btcBinanceCacheKey = getKlineCacheKey({
|
|
343
486
|
userName,
|
|
344
|
-
connectorName:
|
|
487
|
+
connectorName: btcBinanceConnectorName,
|
|
345
488
|
symbol: "BTCUSDT",
|
|
489
|
+
preloadStart,
|
|
346
490
|
end,
|
|
347
491
|
interval,
|
|
348
492
|
cacheOnly
|
|
349
493
|
});
|
|
350
494
|
const btcCoinbaseCacheKey = getKlineCacheKey({
|
|
351
495
|
userName,
|
|
352
|
-
connectorName:
|
|
496
|
+
connectorName: btcCoinbaseConnectorName,
|
|
353
497
|
symbol: "BTCUSDT",
|
|
498
|
+
preloadStart,
|
|
354
499
|
end,
|
|
355
500
|
interval,
|
|
356
501
|
cacheOnly
|
|
357
502
|
});
|
|
358
503
|
const cachedBtcBinanceData = state.btcBinanceKlineCache.get(btcBinanceCacheKey);
|
|
359
504
|
const cachedBtcCoinbaseData = state.btcCoinbaseKlineCache.get(btcCoinbaseCacheKey);
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
505
|
+
const preparedDataCacheKey = getPreparedDataCacheKey({
|
|
506
|
+
userName,
|
|
507
|
+
connectorName,
|
|
508
|
+
symbol,
|
|
509
|
+
preloadStart,
|
|
510
|
+
start,
|
|
511
|
+
end,
|
|
512
|
+
interval,
|
|
513
|
+
btcBinanceConnectorName,
|
|
514
|
+
btcCoinbaseConnectorName
|
|
515
|
+
});
|
|
516
|
+
let preparedData = state.preparedDataCache.get(preparedDataCacheKey);
|
|
517
|
+
if (!preparedData) {
|
|
518
|
+
const [data, btcData, btcBinanceData, btcCoinbaseData] = await withTimeout(
|
|
519
|
+
"kline preload",
|
|
520
|
+
Promise.all([
|
|
521
|
+
cachedCoinData ? Promise.resolve(cachedCoinData) : connector.kline({
|
|
522
|
+
symbol,
|
|
523
|
+
start: preloadStart,
|
|
524
|
+
end,
|
|
525
|
+
interval,
|
|
526
|
+
silent: true,
|
|
527
|
+
cacheOnly
|
|
528
|
+
}),
|
|
529
|
+
cachedBtcData ? Promise.resolve(cachedBtcData) : connector.kline({
|
|
530
|
+
symbol: "BTCUSDT",
|
|
531
|
+
start: preloadStart,
|
|
532
|
+
end,
|
|
533
|
+
interval,
|
|
534
|
+
silent: true,
|
|
535
|
+
cacheOnly
|
|
536
|
+
}),
|
|
537
|
+
cachedBtcBinanceData ? Promise.resolve(cachedBtcBinanceData) : (binanceConnector ?? connector).kline({
|
|
538
|
+
symbol: "BTCUSDT",
|
|
539
|
+
start: preloadStart,
|
|
540
|
+
end,
|
|
541
|
+
interval,
|
|
542
|
+
silent: true,
|
|
543
|
+
cacheOnly
|
|
544
|
+
}),
|
|
545
|
+
cachedBtcCoinbaseData ? Promise.resolve(cachedBtcCoinbaseData) : (coinbaseConnector ?? connector).kline({
|
|
546
|
+
symbol: "BTCUSDT",
|
|
547
|
+
start: preloadStart,
|
|
548
|
+
end,
|
|
549
|
+
interval,
|
|
550
|
+
silent: true,
|
|
551
|
+
cacheOnly
|
|
552
|
+
})
|
|
553
|
+
])
|
|
554
|
+
);
|
|
555
|
+
if (!cachedCoinData) {
|
|
556
|
+
state.coinKlineCache.set(coinCacheKey, data);
|
|
557
|
+
}
|
|
558
|
+
if (!cachedBtcData) {
|
|
559
|
+
state.btcKlineCache.set(btcCacheKey, btcData);
|
|
560
|
+
}
|
|
561
|
+
if (!cachedBtcBinanceData) {
|
|
562
|
+
state.btcBinanceKlineCache.set(btcBinanceCacheKey, btcBinanceData);
|
|
563
|
+
}
|
|
564
|
+
if (!cachedBtcCoinbaseData) {
|
|
565
|
+
state.btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseData);
|
|
566
|
+
}
|
|
567
|
+
const { prevData: prevDataRaw, testData: testDataRaw } = splitCandlesForTesting(data, start, preloadStart);
|
|
568
|
+
const { prevData: btcPrevDataRaw, testData: btcTestDataRaw } = splitCandlesForTesting(btcData, start, preloadStart);
|
|
569
|
+
const { prevData: btcBinancePrevDataRaw } = splitCandlesForTesting(
|
|
570
|
+
btcBinanceData,
|
|
571
|
+
start,
|
|
572
|
+
preloadStart
|
|
573
|
+
);
|
|
574
|
+
const { prevData: btcCoinbasePrevDataRaw } = splitCandlesForTesting(
|
|
575
|
+
btcCoinbaseData,
|
|
576
|
+
start,
|
|
577
|
+
preloadStart
|
|
578
|
+
);
|
|
579
|
+
const { alignedCoinCandles: prevData2, alignedBtcCandles: btcPrevData2 } = alignSortedCandlesByTimestamp(prevDataRaw, btcPrevDataRaw);
|
|
580
|
+
const { alignedCoinCandles: testData2, alignedBtcCandles: btcTestData2 } = alignSortedCandlesByTimestamp(testDataRaw, btcTestDataRaw);
|
|
581
|
+
const { alignedBtcCandles: btcBinancePrevData2 } = alignSortedCandlesByTimestamp(prevDataRaw, btcBinancePrevDataRaw);
|
|
582
|
+
const { alignedBtcCandles: btcCoinbasePrevData2 } = alignSortedCandlesByTimestamp(prevDataRaw, btcCoinbasePrevDataRaw);
|
|
583
|
+
preparedData = {
|
|
584
|
+
prevData: prevData2,
|
|
585
|
+
btcPrevData: btcPrevData2,
|
|
586
|
+
testData: testData2,
|
|
587
|
+
btcTestData: btcTestData2,
|
|
588
|
+
btcBinancePrevData: btcBinancePrevData2,
|
|
589
|
+
btcCoinbasePrevData: btcCoinbasePrevData2
|
|
590
|
+
};
|
|
591
|
+
state.preparedDataCache.set(preparedDataCacheKey, preparedData);
|
|
420
592
|
}
|
|
421
|
-
if (!
|
|
422
|
-
|
|
593
|
+
if (!preparedData) {
|
|
594
|
+
throw new Error("Prepared backtest data not available");
|
|
423
595
|
}
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
);
|
|
433
|
-
const btcTestDataRaw = btcData.filter(
|
|
434
|
-
(candle) => candle.timestamp >= start
|
|
435
|
-
);
|
|
436
|
-
const { alignedCoinCandles: prevData, alignedBtcCandles: btcPrevData } = alignSortedCandlesByTimestamp(prevDataRaw, btcPrevDataRaw);
|
|
437
|
-
const { alignedCoinCandles: testData, alignedBtcCandles: btcTestData } = alignSortedCandlesByTimestamp(testDataRaw, btcTestDataRaw);
|
|
438
|
-
const { alignedBtcCandles: btcBinancePrevData } = alignSortedCandlesByTimestamp(
|
|
439
|
-
prevDataRaw,
|
|
440
|
-
btcBinanceData.filter(
|
|
441
|
-
(candle) => candle.timestamp >= preloadStart && candle.timestamp < start
|
|
442
|
-
)
|
|
443
|
-
);
|
|
444
|
-
const { alignedBtcCandles: btcCoinbasePrevData } = alignSortedCandlesByTimestamp(
|
|
445
|
-
prevDataRaw,
|
|
446
|
-
btcCoinbaseData.filter(
|
|
447
|
-
(candle) => candle.timestamp >= preloadStart && candle.timestamp < start
|
|
448
|
-
)
|
|
449
|
-
);
|
|
596
|
+
const {
|
|
597
|
+
prevData,
|
|
598
|
+
btcPrevData,
|
|
599
|
+
testData,
|
|
600
|
+
btcTestData,
|
|
601
|
+
btcBinancePrevData,
|
|
602
|
+
btcCoinbasePrevData
|
|
603
|
+
} = preparedData;
|
|
450
604
|
const testConnector = createTestConnector(connector, {
|
|
451
605
|
userName,
|
|
452
|
-
mlEnabled: ml
|
|
453
|
-
|
|
454
|
-
const strategy = await strategyCreator({
|
|
455
|
-
userName,
|
|
456
|
-
config: strategyConfig,
|
|
457
|
-
symbol,
|
|
458
|
-
data: prevData,
|
|
459
|
-
btcData: btcPrevData,
|
|
460
|
-
btcBinanceData: btcBinancePrevData,
|
|
461
|
-
btcCoinbaseData: btcCoinbasePrevData,
|
|
462
|
-
connector: testConnector
|
|
606
|
+
mlEnabled: ml,
|
|
607
|
+
aiEnabled: ai
|
|
463
608
|
});
|
|
609
|
+
const strategy = await withTimeout(
|
|
610
|
+
"strategy init",
|
|
611
|
+
strategyCreator({
|
|
612
|
+
userName,
|
|
613
|
+
config: strategyConfig,
|
|
614
|
+
symbol,
|
|
615
|
+
data: prevData,
|
|
616
|
+
btcData: btcPrevData,
|
|
617
|
+
btcBinanceData: btcBinancePrevData,
|
|
618
|
+
btcCoinbaseData: btcCoinbasePrevData,
|
|
619
|
+
connector: testConnector
|
|
620
|
+
})
|
|
621
|
+
);
|
|
464
622
|
const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
|
|
465
|
-
const
|
|
466
|
-
|
|
623
|
+
const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
|
|
624
|
+
const replaySignalEvaluations = [];
|
|
625
|
+
const flushClosedResultsBatch = async () => {
|
|
626
|
+
if (!ml && !ai) return;
|
|
467
627
|
const batch = await testConnector.drainMlResultsBatch();
|
|
468
628
|
if (!batch.length) return;
|
|
469
629
|
for (const resultRecord of batch) {
|
|
470
630
|
const payload = pendingMlPayloadBySignalId.get(resultRecord.signalId);
|
|
471
|
-
if (
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
631
|
+
if (payload) {
|
|
632
|
+
pendingMlPayloadBySignalId.delete(resultRecord.signalId);
|
|
633
|
+
const fullRow = buildMlTrainingRow(payload, {
|
|
634
|
+
profit: resultRecord.profit
|
|
635
|
+
});
|
|
636
|
+
const row = trimMlTrainingRowWindows(fullRow, 5);
|
|
637
|
+
await appendMlDatasetRow({
|
|
638
|
+
strategyName,
|
|
639
|
+
chunkId,
|
|
640
|
+
row
|
|
641
|
+
});
|
|
642
|
+
}
|
|
643
|
+
const aiRowBase = pendingAiRowBySignalId.get(resultRecord.signalId);
|
|
644
|
+
if (aiRowBase) {
|
|
645
|
+
pendingAiRowBySignalId.delete(resultRecord.signalId);
|
|
646
|
+
await appendAiDatasetRow({
|
|
647
|
+
strategyName,
|
|
648
|
+
chunkId,
|
|
649
|
+
row: {
|
|
650
|
+
...aiRowBase,
|
|
651
|
+
profit: resultRecord.profit
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
}
|
|
482
655
|
}
|
|
483
656
|
};
|
|
484
657
|
for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
|
|
658
|
+
if (candleIndex % 25 === 0) {
|
|
659
|
+
throwIfTimedOut("candle loop");
|
|
660
|
+
}
|
|
485
661
|
const candle = testData[candleIndex];
|
|
486
662
|
const btcCandle = btcTestData[candleIndex];
|
|
487
|
-
await testConnector.checkSl(candle);
|
|
488
|
-
await testConnector.checkTp(candle);
|
|
489
|
-
const signal = await
|
|
663
|
+
await withTimeout("stop-loss check", testConnector.checkSl(candle));
|
|
664
|
+
await withTimeout("take-profit check", testConnector.checkTp(candle));
|
|
665
|
+
const signal = await withTimeout(
|
|
666
|
+
"strategy signal",
|
|
667
|
+
strategy(candle, btcCandle)
|
|
668
|
+
);
|
|
669
|
+
if (!signal || typeof signal === "string") {
|
|
670
|
+
replaySignalEvaluations.push({
|
|
671
|
+
evaluationId: `${testId}:${strategyName}:${symbol}:${candle.timestamp}`,
|
|
672
|
+
userName,
|
|
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
|
+
});
|
|
699
|
+
}
|
|
700
|
+
const shouldCapturePayload = signal && typeof signal !== "string" && signal.signalId && (ml || ai);
|
|
701
|
+
if (shouldCapturePayload) {
|
|
702
|
+
await withTimeout(
|
|
703
|
+
"derivatives context",
|
|
704
|
+
enrichSignalWithDerivativesContext({
|
|
705
|
+
signal,
|
|
706
|
+
env: "BACKTEST"
|
|
707
|
+
})
|
|
708
|
+
);
|
|
709
|
+
}
|
|
490
710
|
if (ml && signal && typeof signal !== "string" && signal.signalId) {
|
|
491
711
|
const payload = buildMlPayload({
|
|
492
712
|
signal,
|
|
@@ -503,9 +723,27 @@ var testing = async ({
|
|
|
503
723
|
});
|
|
504
724
|
pendingMlPayloadBySignalId.set(signal.signalId, payload);
|
|
505
725
|
}
|
|
726
|
+
if (ai && signal && typeof signal !== "string" && signal.signalId) {
|
|
727
|
+
pendingAiRowBySignalId.set(signal.signalId, {
|
|
728
|
+
signalId: signal.signalId,
|
|
729
|
+
strategyName: signal.strategy || strategyName,
|
|
730
|
+
symbol: signal.symbol || symbol,
|
|
731
|
+
direction: signal.direction,
|
|
732
|
+
timestamp: signal.timestamp,
|
|
733
|
+
payload: buildAiPayload(signal),
|
|
734
|
+
testId,
|
|
735
|
+
testSuiteId,
|
|
736
|
+
testName: name,
|
|
737
|
+
connectorName
|
|
738
|
+
});
|
|
739
|
+
}
|
|
506
740
|
}
|
|
507
|
-
await
|
|
508
|
-
|
|
741
|
+
await withTimeout("flush closed results", flushClosedResultsBatch());
|
|
742
|
+
const result = await withTimeout("collect result", testConnector.getResult());
|
|
743
|
+
return {
|
|
744
|
+
...result,
|
|
745
|
+
inlineReplaySignalEvaluations: replaySignalEvaluations
|
|
746
|
+
};
|
|
509
747
|
};
|
|
510
748
|
export {
|
|
511
749
|
createTestConnector,
|