@tradejs/node 1.0.5 → 1.0.6
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 +926 -0
- package/dist/ai.mjs +35 -0
- package/dist/backtest.js +408 -5687
- package/dist/backtest.mjs +331 -175
- package/dist/chunk-72FKXJ2I.mjs +473 -0
- package/dist/{chunk-P2ZUWONT.mjs → chunk-CGJ2UU6H.mjs} +1 -1
- package/dist/{chunk-ZY6ULOWK.mjs → chunk-EOZSJKUM.mjs} +1 -1
- package/dist/{chunk-7ICOZAKA.mjs → chunk-H6LIYHU4.mjs} +29 -11
- package/dist/{chunk-RBE4PZER.mjs → chunk-PQH5PAFC.mjs} +1 -1
- package/dist/{chunk-CIY64D57.mjs → chunk-XW5L327F.mjs} +1 -1
- package/dist/cli.d.mts +4 -4
- package/dist/cli.d.ts +4 -4
- package/dist/cli.js +711 -125
- package/dist/cli.mjs +199 -71
- package/dist/connectors.js +1 -1
- package/dist/connectors.mjs +2 -2
- 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 +1 -1
- package/dist/registry.mjs +2 -2
- package/dist/strategies.d.mts +6 -12
- package/dist/strategies.d.ts +6 -12
- package/dist/strategies.js +426 -83
- package/dist/strategies.mjs +200 -34
- package/package.json +9 -4
- package/dist/ai-G7ATN4YL.mjs +0 -19
- package/dist/chunk-LMAKIC3C.mjs +0 -294
package/dist/backtest.mjs
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
import {
|
|
2
2
|
BUILTIN_CONNECTOR_NAMES,
|
|
3
3
|
getConnectorCreatorByName
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-XW5L327F.mjs";
|
|
5
5
|
import {
|
|
6
6
|
buildMlPayload
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-PQH5PAFC.mjs";
|
|
8
8
|
import {
|
|
9
|
-
|
|
10
|
-
} from "./chunk-
|
|
9
|
+
buildAiPayload
|
|
10
|
+
} from "./chunk-72FKXJ2I.mjs";
|
|
11
11
|
import {
|
|
12
12
|
getStrategyCreator
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-EOZSJKUM.mjs";
|
|
14
14
|
import {
|
|
15
15
|
getTradejsProjectCwd
|
|
16
|
-
} from "./chunk-
|
|
17
|
-
import
|
|
18
|
-
__toESM
|
|
19
|
-
} from "./chunk-6DZX6EAA.mjs";
|
|
16
|
+
} from "./chunk-CGJ2UU6H.mjs";
|
|
17
|
+
import "./chunk-6DZX6EAA.mjs";
|
|
20
18
|
|
|
21
19
|
// src/backtest.ts
|
|
22
20
|
export * from "@tradejs/core/backtest";
|
|
@@ -25,6 +23,7 @@ export * from "@tradejs/core/backtest";
|
|
|
25
23
|
import { alignSortedCandlesByTimestamp } from "@tradejs/core/indicators";
|
|
26
24
|
import { PRELOAD_DAYS } from "@tradejs/core/constants";
|
|
27
25
|
import { getTimestamp } 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
|
});
|
|
@@ -106,33 +102,20 @@ var createTestConnector = (connector, context) => {
|
|
|
106
102
|
kline: async (options) => connector.kline(options),
|
|
107
103
|
getResult: async () => {
|
|
108
104
|
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
105
|
return {
|
|
125
106
|
stat: {
|
|
126
107
|
amount,
|
|
127
108
|
profit: amount - INITIAL_AMOUNT,
|
|
128
109
|
orders: positionLog.length
|
|
129
110
|
},
|
|
130
|
-
orderLogId
|
|
111
|
+
orderLogId,
|
|
112
|
+
inlineOrderLog: [...orderLog],
|
|
113
|
+
inlinePositionLog: [...positionLog]
|
|
131
114
|
};
|
|
132
115
|
},
|
|
133
116
|
getPosition: async () => currentPosition || null,
|
|
134
117
|
checkTp: async (candle) => {
|
|
135
|
-
if (
|
|
118
|
+
if (!candle || !currentPosition || !currentPosition.qty) {
|
|
136
119
|
return;
|
|
137
120
|
}
|
|
138
121
|
const isLong = currentPosition.direction === "LONG";
|
|
@@ -167,7 +150,7 @@ var createTestConnector = (connector, context) => {
|
|
|
167
150
|
}
|
|
168
151
|
},
|
|
169
152
|
checkSl: async (candle) => {
|
|
170
|
-
if (!stopLossPrice || !currentPosition ||
|
|
153
|
+
if (!stopLossPrice || !currentPosition || !candle) {
|
|
171
154
|
return;
|
|
172
155
|
}
|
|
173
156
|
const isLong = currentPosition.direction === "LONG";
|
|
@@ -187,13 +170,11 @@ var createTestConnector = (connector, context) => {
|
|
|
187
170
|
clearPosition(candle.timestamp);
|
|
188
171
|
}
|
|
189
172
|
},
|
|
190
|
-
placeOrder: async (order
|
|
173
|
+
placeOrder: async (order) => {
|
|
191
174
|
if (currentPosition) {
|
|
192
175
|
return false;
|
|
193
176
|
}
|
|
194
177
|
const isLong = order.direction === "LONG";
|
|
195
|
-
takeProfits = import_lodash.default.cloneDeep(tp);
|
|
196
|
-
stopLossPrice = slPrice || null;
|
|
197
178
|
currentPosition = { ...order, amount };
|
|
198
179
|
originalQty = order.qty;
|
|
199
180
|
const fee = order.price * order.qty * FEE;
|
|
@@ -208,6 +189,24 @@ var createTestConnector = (connector, context) => {
|
|
|
208
189
|
});
|
|
209
190
|
return true;
|
|
210
191
|
},
|
|
192
|
+
setTakeProfits: async ({ takeProfits: nextTakeProfits }) => {
|
|
193
|
+
if (!currentPosition) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
takeProfits = Array.isArray(nextTakeProfits) ? nextTakeProfits.map((tp) => ({ ...tp })) : [];
|
|
197
|
+
return true;
|
|
198
|
+
},
|
|
199
|
+
setStopLoss: async ({ stopLossPrice: nextStopLossPrice }) => {
|
|
200
|
+
if (!currentPosition) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
stopLossPrice = nextStopLossPrice || null;
|
|
204
|
+
currentPosition = {
|
|
205
|
+
...currentPosition,
|
|
206
|
+
...stopLossPrice != null ? { slPrice: stopLossPrice } : {}
|
|
207
|
+
};
|
|
208
|
+
return true;
|
|
209
|
+
},
|
|
211
210
|
closePosition: async (order) => {
|
|
212
211
|
if (!currentPosition) {
|
|
213
212
|
return false;
|
|
@@ -227,7 +226,7 @@ var createTestConnector = (connector, context) => {
|
|
|
227
226
|
},
|
|
228
227
|
getTickers: connector.getTickers,
|
|
229
228
|
getPositions: connector.getPositions,
|
|
230
|
-
drainMlResultsBatch: async () =>
|
|
229
|
+
drainMlResultsBatch: async () => closedSignalResults.splice(0)
|
|
231
230
|
};
|
|
232
231
|
};
|
|
233
232
|
|
|
@@ -237,7 +236,9 @@ var createTestingKlineCacheState = () => ({
|
|
|
237
236
|
coinKlineCache: /* @__PURE__ */ new Map(),
|
|
238
237
|
btcKlineCache: /* @__PURE__ */ new Map(),
|
|
239
238
|
btcBinanceKlineCache: /* @__PURE__ */ new Map(),
|
|
240
|
-
btcCoinbaseKlineCache: /* @__PURE__ */ new Map()
|
|
239
|
+
btcCoinbaseKlineCache: /* @__PURE__ */ new Map(),
|
|
240
|
+
preparedDataCache: /* @__PURE__ */ new Map(),
|
|
241
|
+
connectorCache: /* @__PURE__ */ new Map()
|
|
241
242
|
});
|
|
242
243
|
var testingKlineCacheStateByProjectRoot = /* @__PURE__ */ new Map();
|
|
243
244
|
var getTestingKlineCacheState = (cwd = getTradejsProjectCwd()) => {
|
|
@@ -264,6 +265,62 @@ var getKlineCacheKey = (params) => {
|
|
|
264
265
|
cacheOnly ? 1 : 0
|
|
265
266
|
].join(":");
|
|
266
267
|
};
|
|
268
|
+
var getPreparedDataCacheKey = (params) => {
|
|
269
|
+
const {
|
|
270
|
+
userName,
|
|
271
|
+
connectorName,
|
|
272
|
+
symbol,
|
|
273
|
+
start,
|
|
274
|
+
end,
|
|
275
|
+
interval,
|
|
276
|
+
btcBinanceConnectorName,
|
|
277
|
+
btcCoinbaseConnectorName
|
|
278
|
+
} = params;
|
|
279
|
+
return [
|
|
280
|
+
userName,
|
|
281
|
+
connectorName,
|
|
282
|
+
symbol,
|
|
283
|
+
start,
|
|
284
|
+
end,
|
|
285
|
+
interval,
|
|
286
|
+
btcBinanceConnectorName,
|
|
287
|
+
btcCoinbaseConnectorName
|
|
288
|
+
].join(":");
|
|
289
|
+
};
|
|
290
|
+
var getConnectorCacheKey = (params) => [params.userName, params.connectorName].join(":");
|
|
291
|
+
var splitCandlesForTesting = (candles, start) => {
|
|
292
|
+
const prevData = [];
|
|
293
|
+
const testData = [];
|
|
294
|
+
for (const candle of candles) {
|
|
295
|
+
if (candle.timestamp < preloadStart) continue;
|
|
296
|
+
if (candle.timestamp < start) {
|
|
297
|
+
prevData.push(candle);
|
|
298
|
+
} else {
|
|
299
|
+
testData.push(candle);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return { prevData, testData };
|
|
303
|
+
};
|
|
304
|
+
var getCachedConnector = async (params) => {
|
|
305
|
+
const { state, projectRoot, userName, connectorName } = params;
|
|
306
|
+
const cacheKey = getConnectorCacheKey({ userName, connectorName });
|
|
307
|
+
const cachedConnector = state.connectorCache.get(cacheKey);
|
|
308
|
+
if (cachedConnector) {
|
|
309
|
+
return cachedConnector;
|
|
310
|
+
}
|
|
311
|
+
const connectorCreator = await getConnectorCreatorByName(
|
|
312
|
+
connectorName,
|
|
313
|
+
projectRoot
|
|
314
|
+
);
|
|
315
|
+
if (!connectorCreator) {
|
|
316
|
+
return void 0;
|
|
317
|
+
}
|
|
318
|
+
const connector = await connectorCreator({
|
|
319
|
+
userName
|
|
320
|
+
});
|
|
321
|
+
state.connectorCache.set(cacheKey, connector);
|
|
322
|
+
return connector;
|
|
323
|
+
};
|
|
267
324
|
var resetTestingKlineCache = (cwd) => {
|
|
268
325
|
const normalizedCwd = String(cwd ?? "").trim();
|
|
269
326
|
if (!normalizedCwd) {
|
|
@@ -285,35 +342,88 @@ var testing = async ({
|
|
|
285
342
|
strategyConfig,
|
|
286
343
|
connectorName,
|
|
287
344
|
ml = false,
|
|
288
|
-
|
|
345
|
+
ai = false,
|
|
346
|
+
chunkId = "single",
|
|
347
|
+
timeoutMs
|
|
289
348
|
}) => {
|
|
290
349
|
if (!start) {
|
|
291
350
|
throw new Error("no start");
|
|
292
351
|
}
|
|
352
|
+
const startedAt = Date.now();
|
|
353
|
+
const formatTimeoutMessage = (stage) => `Test ${name} (${symbol}) timed out after ${timeoutMs}ms during ${stage}`;
|
|
354
|
+
const getRemainingTimeoutMs = (stage) => {
|
|
355
|
+
if (!timeoutMs || timeoutMs <= 0) {
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
const remainingMs = timeoutMs - (Date.now() - startedAt);
|
|
359
|
+
if (remainingMs <= 0) {
|
|
360
|
+
throw new Error(formatTimeoutMessage(stage));
|
|
361
|
+
}
|
|
362
|
+
return remainingMs;
|
|
363
|
+
};
|
|
364
|
+
const throwIfTimedOut = (stage) => {
|
|
365
|
+
getRemainingTimeoutMs(stage);
|
|
366
|
+
};
|
|
367
|
+
const withTimeout = async (stage, promise) => {
|
|
368
|
+
const remainingMs = getRemainingTimeoutMs(stage);
|
|
369
|
+
if (remainingMs == null) {
|
|
370
|
+
return promise;
|
|
371
|
+
}
|
|
372
|
+
return await new Promise((resolve, reject) => {
|
|
373
|
+
const timer = setTimeout(() => {
|
|
374
|
+
reject(new Error(formatTimeoutMessage(stage)));
|
|
375
|
+
}, remainingMs);
|
|
376
|
+
promise.then(
|
|
377
|
+
(value) => {
|
|
378
|
+
clearTimeout(timer);
|
|
379
|
+
resolve(value);
|
|
380
|
+
},
|
|
381
|
+
(error) => {
|
|
382
|
+
clearTimeout(timer);
|
|
383
|
+
reject(error);
|
|
384
|
+
}
|
|
385
|
+
);
|
|
386
|
+
});
|
|
387
|
+
};
|
|
293
388
|
const { projectRoot, state } = getTestingKlineCacheState();
|
|
294
|
-
const
|
|
295
|
-
|
|
296
|
-
|
|
389
|
+
const connector = await withTimeout(
|
|
390
|
+
"connector init",
|
|
391
|
+
getCachedConnector({
|
|
392
|
+
state,
|
|
393
|
+
projectRoot,
|
|
394
|
+
userName,
|
|
395
|
+
connectorName
|
|
396
|
+
})
|
|
297
397
|
);
|
|
298
|
-
if (!
|
|
398
|
+
if (!connector) {
|
|
299
399
|
throw new Error(`Unknown connector: ${connectorName}`);
|
|
300
400
|
}
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
401
|
+
const strategyCreator = await withTimeout(
|
|
402
|
+
"strategy lookup",
|
|
403
|
+
getStrategyCreator(strategyName, projectRoot)
|
|
404
|
+
);
|
|
305
405
|
if (!strategyCreator) {
|
|
306
406
|
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
307
407
|
}
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
408
|
+
const binanceConnector = await withTimeout(
|
|
409
|
+
"binance connector init",
|
|
410
|
+
getCachedConnector({
|
|
411
|
+
state,
|
|
412
|
+
projectRoot,
|
|
413
|
+
userName,
|
|
414
|
+
connectorName: BUILTIN_CONNECTOR_NAMES.Binance
|
|
415
|
+
})
|
|
311
416
|
);
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
417
|
+
const coinbaseConnector = await withTimeout(
|
|
418
|
+
"coinbase connector init",
|
|
419
|
+
getCachedConnector({
|
|
420
|
+
state,
|
|
421
|
+
projectRoot,
|
|
422
|
+
userName,
|
|
423
|
+
connectorName: BUILTIN_CONNECTOR_NAMES.Coinbase
|
|
424
|
+
})
|
|
315
425
|
);
|
|
316
|
-
if (!
|
|
426
|
+
if (!binanceConnector || !coinbaseConnector) {
|
|
317
427
|
logger.warn(
|
|
318
428
|
"Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
|
|
319
429
|
connectorName
|
|
@@ -337,11 +447,13 @@ var testing = async ({
|
|
|
337
447
|
interval,
|
|
338
448
|
cacheOnly
|
|
339
449
|
});
|
|
450
|
+
const btcBinanceConnectorName = binanceConnector ? BUILTIN_CONNECTOR_NAMES.Binance : connectorName;
|
|
451
|
+
const btcCoinbaseConnectorName = coinbaseConnector ? BUILTIN_CONNECTOR_NAMES.Coinbase : connectorName;
|
|
340
452
|
const cachedCoinData = state.coinKlineCache.get(coinCacheKey);
|
|
341
453
|
const cachedBtcData = state.btcKlineCache.get(btcCacheKey);
|
|
342
454
|
const btcBinanceCacheKey = getKlineCacheKey({
|
|
343
455
|
userName,
|
|
344
|
-
connectorName:
|
|
456
|
+
connectorName: btcBinanceConnectorName,
|
|
345
457
|
symbol: "BTCUSDT",
|
|
346
458
|
end,
|
|
347
459
|
interval,
|
|
@@ -349,7 +461,7 @@ var testing = async ({
|
|
|
349
461
|
});
|
|
350
462
|
const btcCoinbaseCacheKey = getKlineCacheKey({
|
|
351
463
|
userName,
|
|
352
|
-
connectorName:
|
|
464
|
+
connectorName: btcCoinbaseConnectorName,
|
|
353
465
|
symbol: "BTCUSDT",
|
|
354
466
|
end,
|
|
355
467
|
interval,
|
|
@@ -357,136 +469,166 @@ var testing = async ({
|
|
|
357
469
|
});
|
|
358
470
|
const cachedBtcBinanceData = state.btcBinanceKlineCache.get(btcBinanceCacheKey);
|
|
359
471
|
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
|
-
|
|
472
|
+
const preparedDataCacheKey = getPreparedDataCacheKey({
|
|
473
|
+
userName,
|
|
474
|
+
connectorName,
|
|
475
|
+
symbol,
|
|
476
|
+
start,
|
|
477
|
+
end,
|
|
478
|
+
interval,
|
|
479
|
+
btcBinanceConnectorName,
|
|
480
|
+
btcCoinbaseConnectorName
|
|
481
|
+
});
|
|
482
|
+
let preparedData = state.preparedDataCache.get(preparedDataCacheKey);
|
|
483
|
+
if (!preparedData) {
|
|
484
|
+
const [data, btcData, btcBinanceData, btcCoinbaseData] = await withTimeout(
|
|
485
|
+
"kline preload",
|
|
486
|
+
Promise.all([
|
|
487
|
+
cachedCoinData ? Promise.resolve(cachedCoinData) : connector.kline({
|
|
488
|
+
symbol,
|
|
489
|
+
start: preloadStart,
|
|
490
|
+
end,
|
|
491
|
+
interval,
|
|
492
|
+
silent: true,
|
|
493
|
+
cacheOnly
|
|
494
|
+
}),
|
|
495
|
+
cachedBtcData ? Promise.resolve(cachedBtcData) : connector.kline({
|
|
496
|
+
symbol: "BTCUSDT",
|
|
497
|
+
start: preloadStart,
|
|
498
|
+
end,
|
|
499
|
+
interval,
|
|
500
|
+
silent: true,
|
|
501
|
+
cacheOnly
|
|
502
|
+
}),
|
|
503
|
+
cachedBtcBinanceData ? Promise.resolve(cachedBtcBinanceData) : (binanceConnector ?? connector).kline({
|
|
504
|
+
symbol: "BTCUSDT",
|
|
505
|
+
start: preloadStart,
|
|
506
|
+
end,
|
|
507
|
+
interval,
|
|
508
|
+
silent: true,
|
|
509
|
+
cacheOnly
|
|
510
|
+
}),
|
|
511
|
+
cachedBtcCoinbaseData ? Promise.resolve(cachedBtcCoinbaseData) : (coinbaseConnector ?? connector).kline({
|
|
512
|
+
symbol: "BTCUSDT",
|
|
513
|
+
start: preloadStart,
|
|
514
|
+
end,
|
|
515
|
+
interval,
|
|
516
|
+
silent: true,
|
|
517
|
+
cacheOnly
|
|
518
|
+
})
|
|
519
|
+
])
|
|
520
|
+
);
|
|
521
|
+
if (!cachedCoinData) {
|
|
522
|
+
state.coinKlineCache.set(coinCacheKey, data);
|
|
523
|
+
}
|
|
524
|
+
if (!cachedBtcData) {
|
|
525
|
+
state.btcKlineCache.set(btcCacheKey, btcData);
|
|
526
|
+
}
|
|
527
|
+
if (!cachedBtcBinanceData) {
|
|
528
|
+
state.btcBinanceKlineCache.set(btcBinanceCacheKey, btcBinanceData);
|
|
529
|
+
}
|
|
530
|
+
if (!cachedBtcCoinbaseData) {
|
|
531
|
+
state.btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseData);
|
|
532
|
+
}
|
|
533
|
+
const { prevData: prevDataRaw, testData: testDataRaw } = splitCandlesForTesting(data, start);
|
|
534
|
+
const { prevData: btcPrevDataRaw, testData: btcTestDataRaw } = splitCandlesForTesting(btcData, start);
|
|
535
|
+
const { prevData: btcBinancePrevDataRaw } = splitCandlesForTesting(
|
|
536
|
+
btcBinanceData,
|
|
537
|
+
start
|
|
538
|
+
);
|
|
539
|
+
const { prevData: btcCoinbasePrevDataRaw } = splitCandlesForTesting(
|
|
540
|
+
btcCoinbaseData,
|
|
541
|
+
start
|
|
542
|
+
);
|
|
543
|
+
const { alignedCoinCandles: prevData2, alignedBtcCandles: btcPrevData2 } = alignSortedCandlesByTimestamp(prevDataRaw, btcPrevDataRaw);
|
|
544
|
+
const { alignedCoinCandles: testData2, alignedBtcCandles: btcTestData2 } = alignSortedCandlesByTimestamp(testDataRaw, btcTestDataRaw);
|
|
545
|
+
const { alignedBtcCandles: btcBinancePrevData2 } = alignSortedCandlesByTimestamp(prevDataRaw, btcBinancePrevDataRaw);
|
|
546
|
+
const { alignedBtcCandles: btcCoinbasePrevData2 } = alignSortedCandlesByTimestamp(prevDataRaw, btcCoinbasePrevDataRaw);
|
|
547
|
+
preparedData = {
|
|
548
|
+
prevData: prevData2,
|
|
549
|
+
btcPrevData: btcPrevData2,
|
|
550
|
+
testData: testData2,
|
|
551
|
+
btcTestData: btcTestData2,
|
|
552
|
+
btcBinancePrevData: btcBinancePrevData2,
|
|
553
|
+
btcCoinbasePrevData: btcCoinbasePrevData2
|
|
554
|
+
};
|
|
555
|
+
state.preparedDataCache.set(preparedDataCacheKey, preparedData);
|
|
420
556
|
}
|
|
421
|
-
if (!
|
|
422
|
-
|
|
557
|
+
if (!preparedData) {
|
|
558
|
+
throw new Error("Prepared backtest data not available");
|
|
423
559
|
}
|
|
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
|
-
);
|
|
560
|
+
const {
|
|
561
|
+
prevData,
|
|
562
|
+
btcPrevData,
|
|
563
|
+
testData,
|
|
564
|
+
btcTestData,
|
|
565
|
+
btcBinancePrevData,
|
|
566
|
+
btcCoinbasePrevData
|
|
567
|
+
} = preparedData;
|
|
450
568
|
const testConnector = createTestConnector(connector, {
|
|
451
569
|
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
|
|
570
|
+
mlEnabled: ml,
|
|
571
|
+
aiEnabled: ai
|
|
463
572
|
});
|
|
573
|
+
const strategy = await withTimeout(
|
|
574
|
+
"strategy init",
|
|
575
|
+
strategyCreator({
|
|
576
|
+
userName,
|
|
577
|
+
config: strategyConfig,
|
|
578
|
+
symbol,
|
|
579
|
+
data: prevData,
|
|
580
|
+
btcData: btcPrevData,
|
|
581
|
+
btcBinanceData: btcBinancePrevData,
|
|
582
|
+
btcCoinbaseData: btcCoinbasePrevData,
|
|
583
|
+
connector: testConnector
|
|
584
|
+
})
|
|
585
|
+
);
|
|
464
586
|
const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
|
|
465
|
-
const
|
|
466
|
-
|
|
587
|
+
const pendingAiRowBySignalId = /* @__PURE__ */ new Map();
|
|
588
|
+
const flushClosedResultsBatch = async () => {
|
|
589
|
+
if (!ml && !ai) return;
|
|
467
590
|
const batch = await testConnector.drainMlResultsBatch();
|
|
468
591
|
if (!batch.length) return;
|
|
469
592
|
for (const resultRecord of batch) {
|
|
470
593
|
const payload = pendingMlPayloadBySignalId.get(resultRecord.signalId);
|
|
471
|
-
if (
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
594
|
+
if (payload) {
|
|
595
|
+
pendingMlPayloadBySignalId.delete(resultRecord.signalId);
|
|
596
|
+
const fullRow = buildMlTrainingRow(payload, {
|
|
597
|
+
profit: resultRecord.profit
|
|
598
|
+
});
|
|
599
|
+
const row = trimMlTrainingRowWindows(fullRow, 5);
|
|
600
|
+
await appendMlDatasetRow({
|
|
601
|
+
strategyName,
|
|
602
|
+
chunkId,
|
|
603
|
+
row
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
const aiRowBase = pendingAiRowBySignalId.get(resultRecord.signalId);
|
|
607
|
+
if (aiRowBase) {
|
|
608
|
+
pendingAiRowBySignalId.delete(resultRecord.signalId);
|
|
609
|
+
await appendAiDatasetRow({
|
|
610
|
+
strategyName,
|
|
611
|
+
chunkId,
|
|
612
|
+
row: {
|
|
613
|
+
...aiRowBase,
|
|
614
|
+
profit: resultRecord.profit
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
482
618
|
}
|
|
483
619
|
};
|
|
484
620
|
for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
|
|
621
|
+
if (candleIndex % 25 === 0) {
|
|
622
|
+
throwIfTimedOut("candle loop");
|
|
623
|
+
}
|
|
485
624
|
const candle = testData[candleIndex];
|
|
486
625
|
const btcCandle = btcTestData[candleIndex];
|
|
487
|
-
await testConnector.checkSl(candle);
|
|
488
|
-
await testConnector.checkTp(candle);
|
|
489
|
-
const signal = await
|
|
626
|
+
await withTimeout("stop-loss check", testConnector.checkSl(candle));
|
|
627
|
+
await withTimeout("take-profit check", testConnector.checkTp(candle));
|
|
628
|
+
const signal = await withTimeout(
|
|
629
|
+
"strategy signal",
|
|
630
|
+
strategy(candle, btcCandle)
|
|
631
|
+
);
|
|
490
632
|
if (ml && signal && typeof signal !== "string" && signal.signalId) {
|
|
491
633
|
const payload = buildMlPayload({
|
|
492
634
|
signal,
|
|
@@ -503,9 +645,23 @@ var testing = async ({
|
|
|
503
645
|
});
|
|
504
646
|
pendingMlPayloadBySignalId.set(signal.signalId, payload);
|
|
505
647
|
}
|
|
648
|
+
if (ai && signal && typeof signal !== "string" && signal.signalId) {
|
|
649
|
+
pendingAiRowBySignalId.set(signal.signalId, {
|
|
650
|
+
signalId: signal.signalId,
|
|
651
|
+
strategyName: signal.strategy || strategyName,
|
|
652
|
+
symbol: signal.symbol || symbol,
|
|
653
|
+
direction: signal.direction,
|
|
654
|
+
timestamp: signal.timestamp,
|
|
655
|
+
payload: buildAiPayload(signal),
|
|
656
|
+
testId,
|
|
657
|
+
testSuiteId,
|
|
658
|
+
testName: name,
|
|
659
|
+
connectorName
|
|
660
|
+
});
|
|
661
|
+
}
|
|
506
662
|
}
|
|
507
|
-
await
|
|
508
|
-
return await testConnector.getResult();
|
|
663
|
+
await withTimeout("flush closed results", flushClosedResultsBatch());
|
|
664
|
+
return await withTimeout("collect result", testConnector.getResult());
|
|
509
665
|
};
|
|
510
666
|
export {
|
|
511
667
|
createTestConnector,
|