@tradejs/node 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -0
- package/dist/ai-NNJ3RLLL.mjs +19 -0
- package/dist/backtest.d.mts +9 -0
- package/dist/backtest.d.ts +9 -0
- package/dist/backtest.js +6561 -0
- package/dist/backtest.mjs +487 -0
- package/dist/chunk-4ZPGZWO7.mjs +13 -0
- package/dist/chunk-5YNMSWL3.mjs +0 -0
- package/dist/chunk-6DZX6EAA.mjs +37 -0
- package/dist/chunk-CVTV6S2V.mjs +94 -0
- package/dist/chunk-DE7ADBIR.mjs +204 -0
- package/dist/chunk-E2QNOA5M.mjs +227 -0
- package/dist/chunk-GKDBAF3A.mjs +5500 -0
- package/dist/chunk-MHCXPD2B.mjs +201 -0
- package/dist/chunk-PXJJPAQT.mjs +49 -0
- package/dist/chunk-ZIMX3JX2.mjs +294 -0
- package/dist/cli.d.mts +12 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +6385 -0
- package/dist/cli.mjs +548 -0
- package/dist/connectors.d.mts +20 -0
- package/dist/connectors.d.ts +20 -0
- package/dist/connectors.js +468 -0
- package/dist/connectors.mjs +28 -0
- package/dist/constants.d.mts +6 -0
- package/dist/constants.d.ts +6 -0
- package/dist/constants.js +40 -0
- package/dist/constants.mjs +13 -0
- package/dist/pine.d.mts +8 -0
- package/dist/pine.d.ts +8 -0
- package/dist/pine.js +128 -0
- package/dist/pine.mjs +11 -0
- package/dist/registry.d.mts +15 -0
- package/dist/registry.d.ts +15 -0
- package/dist/registry.js +439 -0
- package/dist/registry.mjs +29 -0
- package/dist/strategies.d.mts +71 -0
- package/dist/strategies.d.ts +71 -0
- package/dist/strategies.js +7210 -0
- package/dist/strategies.mjs +847 -0
- package/package.json +69 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BUILTIN_CONNECTOR_NAMES,
|
|
3
|
+
getConnectorCreatorByName
|
|
4
|
+
} from "./chunk-E2QNOA5M.mjs";
|
|
5
|
+
import {
|
|
6
|
+
buildMlPayload
|
|
7
|
+
} from "./chunk-PXJJPAQT.mjs";
|
|
8
|
+
import {
|
|
9
|
+
require_lodash
|
|
10
|
+
} from "./chunk-GKDBAF3A.mjs";
|
|
11
|
+
import {
|
|
12
|
+
getStrategyCreator
|
|
13
|
+
} from "./chunk-MHCXPD2B.mjs";
|
|
14
|
+
import "./chunk-DE7ADBIR.mjs";
|
|
15
|
+
import {
|
|
16
|
+
__toESM
|
|
17
|
+
} from "./chunk-6DZX6EAA.mjs";
|
|
18
|
+
|
|
19
|
+
// src/backtest.ts
|
|
20
|
+
export * from "@tradejs/core/backtest";
|
|
21
|
+
|
|
22
|
+
// src/testing.ts
|
|
23
|
+
import { alignSortedCandlesByTimestamp } from "@tradejs/core/indicators";
|
|
24
|
+
import { PRELOAD_DAYS } from "@tradejs/core/constants";
|
|
25
|
+
import { getTimestamp } from "@tradejs/core/time";
|
|
26
|
+
import {
|
|
27
|
+
appendMlDatasetRow,
|
|
28
|
+
buildMlTrainingRow,
|
|
29
|
+
trimMlTrainingRowWindows
|
|
30
|
+
} from "@tradejs/infra/ml";
|
|
31
|
+
import { logger } from "@tradejs/infra/logger";
|
|
32
|
+
|
|
33
|
+
// src/testConnector.ts
|
|
34
|
+
var import_lodash = __toESM(require_lodash());
|
|
35
|
+
import { randomUUID } from "crypto";
|
|
36
|
+
import { TTL_1D } from "@tradejs/core/constants";
|
|
37
|
+
import { round } from "@tradejs/core/math";
|
|
38
|
+
import { redisKeys, setData } from "@tradejs/infra/redis";
|
|
39
|
+
var FEE = 5e-3;
|
|
40
|
+
var INITIAL_AMOUNT = 100;
|
|
41
|
+
var createTestConnector = (connector, context) => {
|
|
42
|
+
const userName = context?.userName;
|
|
43
|
+
let state = {};
|
|
44
|
+
const orderLog = [];
|
|
45
|
+
const positionLog = [];
|
|
46
|
+
let currentPosition = null;
|
|
47
|
+
let amount = INITIAL_AMOUNT;
|
|
48
|
+
let originalQty = 0;
|
|
49
|
+
let currentPositionProfit = 0;
|
|
50
|
+
let takeProfits = [];
|
|
51
|
+
let stopLossPrice = null;
|
|
52
|
+
const closedMlResults = [];
|
|
53
|
+
const logOrder = (data) => {
|
|
54
|
+
const nextEntry = {
|
|
55
|
+
...currentPosition || {},
|
|
56
|
+
...data,
|
|
57
|
+
amount: round(amount),
|
|
58
|
+
profit: round(data.profit || 0),
|
|
59
|
+
index: orderLog.length
|
|
60
|
+
};
|
|
61
|
+
if (nextEntry.signal) {
|
|
62
|
+
nextEntry.signal = import_lodash.default.omit(nextEntry.signal, "indicators");
|
|
63
|
+
}
|
|
64
|
+
orderLog.push(nextEntry);
|
|
65
|
+
};
|
|
66
|
+
const clearPosition = (timestamp) => {
|
|
67
|
+
takeProfits = [];
|
|
68
|
+
stopLossPrice = null;
|
|
69
|
+
originalQty = 0;
|
|
70
|
+
if (!currentPosition) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (context?.mlEnabled) {
|
|
74
|
+
const signalId = currentPosition.signal?.signalId;
|
|
75
|
+
if (signalId) {
|
|
76
|
+
closedMlResults.push({
|
|
77
|
+
signalId,
|
|
78
|
+
profit: currentPositionProfit
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
positionLog.push({
|
|
83
|
+
direction: currentPosition.direction,
|
|
84
|
+
open: {
|
|
85
|
+
timestamp: currentPosition.timestamp,
|
|
86
|
+
amount: round(currentPosition.amount)
|
|
87
|
+
},
|
|
88
|
+
close: {
|
|
89
|
+
timestamp,
|
|
90
|
+
amount: round(amount)
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
currentPosition = null;
|
|
94
|
+
currentPositionProfit = 0;
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
getState: async () => state,
|
|
98
|
+
setState: async (newState) => {
|
|
99
|
+
state = {
|
|
100
|
+
...state,
|
|
101
|
+
...newState
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
kline: async (options) => connector.kline(options),
|
|
105
|
+
getResult: async () => {
|
|
106
|
+
const orderLogId = randomUUID().slice(-12);
|
|
107
|
+
const cacheUserName = userName || "root";
|
|
108
|
+
await setData(
|
|
109
|
+
redisKeys.cacheOrders(cacheUserName, orderLogId),
|
|
110
|
+
orderLog,
|
|
111
|
+
{
|
|
112
|
+
expire: TTL_1D
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
await setData(
|
|
116
|
+
redisKeys.cachePositions(cacheUserName, orderLogId),
|
|
117
|
+
positionLog,
|
|
118
|
+
{
|
|
119
|
+
expire: TTL_1D
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
return {
|
|
123
|
+
stat: {
|
|
124
|
+
amount,
|
|
125
|
+
profit: amount - INITIAL_AMOUNT,
|
|
126
|
+
orders: positionLog.length
|
|
127
|
+
},
|
|
128
|
+
orderLogId
|
|
129
|
+
};
|
|
130
|
+
},
|
|
131
|
+
getPosition: async () => currentPosition || null,
|
|
132
|
+
checkTp: async (candle) => {
|
|
133
|
+
if (import_lodash.default.isEmpty(candle) || !currentPosition || !currentPosition.qty) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const isLong = currentPosition.direction === "LONG";
|
|
137
|
+
const entryPrice = currentPosition.price;
|
|
138
|
+
const high = candle.high;
|
|
139
|
+
const low = candle.low;
|
|
140
|
+
for (const tp of takeProfits) {
|
|
141
|
+
if (!currentPosition || currentPosition.qty <= 0) break;
|
|
142
|
+
const targetPrice = tp.price;
|
|
143
|
+
const reached = isLong ? high >= targetPrice : low <= targetPrice;
|
|
144
|
+
if (reached) {
|
|
145
|
+
const qty = originalQty * tp.rate;
|
|
146
|
+
const profit = isLong ? (targetPrice - entryPrice) * qty : (entryPrice - targetPrice) * qty;
|
|
147
|
+
amount += profit;
|
|
148
|
+
currentPositionProfit += profit;
|
|
149
|
+
currentPosition.qty = parseFloat(
|
|
150
|
+
(currentPosition.qty - qty).toFixed(8)
|
|
151
|
+
);
|
|
152
|
+
logOrder({
|
|
153
|
+
timestamp: candle.timestamp,
|
|
154
|
+
qty,
|
|
155
|
+
price: targetPrice,
|
|
156
|
+
profit,
|
|
157
|
+
type: isLong ? "TAKE_PROFIT_LONG" : "TAKE_PROFIT_SHORT"
|
|
158
|
+
});
|
|
159
|
+
tp.done = true;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
takeProfits = takeProfits.filter(({ done }) => !done);
|
|
163
|
+
if (currentPosition && currentPosition.qty <= 0) {
|
|
164
|
+
clearPosition(candle.timestamp);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
checkSl: async (candle) => {
|
|
168
|
+
if (!stopLossPrice || !currentPosition || import_lodash.default.isEmpty(candle)) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const isLong = currentPosition.direction === "LONG";
|
|
172
|
+
const hitStop = isLong ? candle.low <= stopLossPrice : candle.high >= stopLossPrice;
|
|
173
|
+
if (hitStop) {
|
|
174
|
+
const qty = currentPosition.qty;
|
|
175
|
+
const profit = isLong ? (stopLossPrice - currentPosition.price) * qty : (currentPosition.price - stopLossPrice) * qty;
|
|
176
|
+
amount += profit;
|
|
177
|
+
currentPositionProfit += profit;
|
|
178
|
+
logOrder({
|
|
179
|
+
timestamp: candle.timestamp,
|
|
180
|
+
qty,
|
|
181
|
+
profit,
|
|
182
|
+
price: stopLossPrice,
|
|
183
|
+
type: isLong ? "STOP_LOSS_LONG" : "STOP_LOSS_SHORT"
|
|
184
|
+
});
|
|
185
|
+
clearPosition(candle.timestamp);
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
placeOrder: async (order, tp = [], slPrice) => {
|
|
189
|
+
if (currentPosition) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
const isLong = order.direction === "LONG";
|
|
193
|
+
takeProfits = import_lodash.default.cloneDeep(tp);
|
|
194
|
+
stopLossPrice = slPrice || null;
|
|
195
|
+
currentPosition = { ...order, amount };
|
|
196
|
+
originalQty = order.qty;
|
|
197
|
+
const fee = order.price * order.qty * FEE;
|
|
198
|
+
const profit = fee * -1;
|
|
199
|
+
amount += profit;
|
|
200
|
+
currentPositionProfit = profit;
|
|
201
|
+
logOrder({
|
|
202
|
+
...order,
|
|
203
|
+
profit,
|
|
204
|
+
fee,
|
|
205
|
+
type: isLong ? "OPEN_LONG" : "OPEN_SHORT"
|
|
206
|
+
});
|
|
207
|
+
return true;
|
|
208
|
+
},
|
|
209
|
+
closePosition: async (order) => {
|
|
210
|
+
if (!currentPosition) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
const isLong = currentPosition.direction === "LONG";
|
|
214
|
+
const profit = isLong ? (order.price - currentPosition.price) * currentPosition.qty : (currentPosition.price - order.price) * currentPosition.qty;
|
|
215
|
+
amount += profit;
|
|
216
|
+
currentPositionProfit += profit;
|
|
217
|
+
logOrder({
|
|
218
|
+
...order,
|
|
219
|
+
qty: currentPosition.qty,
|
|
220
|
+
profit,
|
|
221
|
+
type: isLong ? "CLOSE_LONG" : "CLOSE_SHORT"
|
|
222
|
+
});
|
|
223
|
+
clearPosition(order.timestamp);
|
|
224
|
+
return true;
|
|
225
|
+
},
|
|
226
|
+
getTickers: connector.getTickers,
|
|
227
|
+
getPositions: connector.getPositions,
|
|
228
|
+
drainMlResultsBatch: async () => closedMlResults.splice(0)
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
// src/testing.ts
|
|
233
|
+
var preloadStart = getTimestamp(PRELOAD_DAYS);
|
|
234
|
+
var coinKlineCache = /* @__PURE__ */ new Map();
|
|
235
|
+
var btcKlineCache = /* @__PURE__ */ new Map();
|
|
236
|
+
var btcBinanceKlineCache = /* @__PURE__ */ new Map();
|
|
237
|
+
var btcCoinbaseKlineCache = /* @__PURE__ */ new Map();
|
|
238
|
+
var getKlineCacheKey = (params) => {
|
|
239
|
+
const { userName, connectorName, symbol, end, interval, cacheOnly } = params;
|
|
240
|
+
return [
|
|
241
|
+
userName,
|
|
242
|
+
connectorName,
|
|
243
|
+
symbol,
|
|
244
|
+
preloadStart,
|
|
245
|
+
end,
|
|
246
|
+
interval,
|
|
247
|
+
cacheOnly ? 1 : 0
|
|
248
|
+
].join(":");
|
|
249
|
+
};
|
|
250
|
+
var resetTestingKlineCache = () => {
|
|
251
|
+
coinKlineCache.clear();
|
|
252
|
+
btcKlineCache.clear();
|
|
253
|
+
btcBinanceKlineCache.clear();
|
|
254
|
+
btcCoinbaseKlineCache.clear();
|
|
255
|
+
};
|
|
256
|
+
var testing = async ({
|
|
257
|
+
userName,
|
|
258
|
+
symbol,
|
|
259
|
+
options: { start, end },
|
|
260
|
+
name,
|
|
261
|
+
testId,
|
|
262
|
+
testSuiteId,
|
|
263
|
+
strategyName,
|
|
264
|
+
strategyConfig,
|
|
265
|
+
connectorName,
|
|
266
|
+
ml = false,
|
|
267
|
+
chunkId = "single"
|
|
268
|
+
}) => {
|
|
269
|
+
if (!start) {
|
|
270
|
+
throw new Error("no start");
|
|
271
|
+
}
|
|
272
|
+
const connectorCreator = await getConnectorCreatorByName(connectorName);
|
|
273
|
+
if (!connectorCreator) {
|
|
274
|
+
throw new Error(`Unknown connector: ${connectorName}`);
|
|
275
|
+
}
|
|
276
|
+
const connector = await connectorCreator({
|
|
277
|
+
userName
|
|
278
|
+
});
|
|
279
|
+
const strategyCreator = await getStrategyCreator(strategyName);
|
|
280
|
+
if (!strategyCreator) {
|
|
281
|
+
throw new Error(`Unknown strategy: ${strategyName}`);
|
|
282
|
+
}
|
|
283
|
+
const binanceCreator = await getConnectorCreatorByName(
|
|
284
|
+
BUILTIN_CONNECTOR_NAMES.Binance
|
|
285
|
+
);
|
|
286
|
+
const coinbaseCreator = await getConnectorCreatorByName(
|
|
287
|
+
BUILTIN_CONNECTOR_NAMES.Coinbase
|
|
288
|
+
);
|
|
289
|
+
if (!binanceCreator || !coinbaseCreator) {
|
|
290
|
+
logger.warn(
|
|
291
|
+
"Binance/Coinbase connectors are unavailable. Reusing %s for BTC references.",
|
|
292
|
+
connectorName
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const interval = "15";
|
|
296
|
+
const cacheOnly = true;
|
|
297
|
+
const coinCacheKey = getKlineCacheKey({
|
|
298
|
+
userName,
|
|
299
|
+
connectorName,
|
|
300
|
+
symbol,
|
|
301
|
+
end,
|
|
302
|
+
interval,
|
|
303
|
+
cacheOnly
|
|
304
|
+
});
|
|
305
|
+
const btcCacheKey = getKlineCacheKey({
|
|
306
|
+
userName,
|
|
307
|
+
connectorName,
|
|
308
|
+
symbol: "BTCUSDT",
|
|
309
|
+
end,
|
|
310
|
+
interval,
|
|
311
|
+
cacheOnly
|
|
312
|
+
});
|
|
313
|
+
const cachedCoinData = coinKlineCache.get(coinCacheKey);
|
|
314
|
+
const cachedBtcData = btcKlineCache.get(btcCacheKey);
|
|
315
|
+
const btcBinanceCacheKey = getKlineCacheKey({
|
|
316
|
+
userName,
|
|
317
|
+
connectorName: binanceCreator ? BUILTIN_CONNECTOR_NAMES.Binance : connectorName,
|
|
318
|
+
symbol: "BTCUSDT",
|
|
319
|
+
end,
|
|
320
|
+
interval,
|
|
321
|
+
cacheOnly
|
|
322
|
+
});
|
|
323
|
+
const btcCoinbaseCacheKey = getKlineCacheKey({
|
|
324
|
+
userName,
|
|
325
|
+
connectorName: coinbaseCreator ? BUILTIN_CONNECTOR_NAMES.Coinbase : connectorName,
|
|
326
|
+
symbol: "BTCUSDT",
|
|
327
|
+
end,
|
|
328
|
+
interval,
|
|
329
|
+
cacheOnly
|
|
330
|
+
});
|
|
331
|
+
const cachedBtcBinanceData = btcBinanceKlineCache.get(btcBinanceCacheKey);
|
|
332
|
+
const cachedBtcCoinbaseData = btcCoinbaseKlineCache.get(btcCoinbaseCacheKey);
|
|
333
|
+
const [data, btcData, btcBinanceData, btcCoinbaseData] = await Promise.all([
|
|
334
|
+
cachedCoinData ? Promise.resolve(cachedCoinData) : connector.kline({
|
|
335
|
+
symbol,
|
|
336
|
+
start: preloadStart,
|
|
337
|
+
end,
|
|
338
|
+
interval,
|
|
339
|
+
silent: true,
|
|
340
|
+
cacheOnly
|
|
341
|
+
}),
|
|
342
|
+
cachedBtcData ? Promise.resolve(cachedBtcData) : connector.kline({
|
|
343
|
+
symbol: "BTCUSDT",
|
|
344
|
+
start: preloadStart,
|
|
345
|
+
end,
|
|
346
|
+
interval,
|
|
347
|
+
silent: true,
|
|
348
|
+
cacheOnly
|
|
349
|
+
}),
|
|
350
|
+
cachedBtcBinanceData ? Promise.resolve(cachedBtcBinanceData) : binanceCreator ? binanceCreator({ userName }).then(
|
|
351
|
+
(binanceConnector) => binanceConnector.kline({
|
|
352
|
+
symbol: "BTCUSDT",
|
|
353
|
+
start: preloadStart,
|
|
354
|
+
end,
|
|
355
|
+
interval,
|
|
356
|
+
silent: true,
|
|
357
|
+
cacheOnly
|
|
358
|
+
})
|
|
359
|
+
) : connector.kline({
|
|
360
|
+
symbol: "BTCUSDT",
|
|
361
|
+
start: preloadStart,
|
|
362
|
+
end,
|
|
363
|
+
interval,
|
|
364
|
+
silent: true,
|
|
365
|
+
cacheOnly
|
|
366
|
+
}),
|
|
367
|
+
cachedBtcCoinbaseData ? Promise.resolve(cachedBtcCoinbaseData) : coinbaseCreator ? coinbaseCreator({ userName }).then(
|
|
368
|
+
(coinbaseConnector) => coinbaseConnector.kline({
|
|
369
|
+
symbol: "BTCUSDT",
|
|
370
|
+
start: preloadStart,
|
|
371
|
+
end,
|
|
372
|
+
interval,
|
|
373
|
+
silent: true,
|
|
374
|
+
cacheOnly
|
|
375
|
+
})
|
|
376
|
+
) : connector.kline({
|
|
377
|
+
symbol: "BTCUSDT",
|
|
378
|
+
start: preloadStart,
|
|
379
|
+
end,
|
|
380
|
+
interval,
|
|
381
|
+
silent: true,
|
|
382
|
+
cacheOnly
|
|
383
|
+
})
|
|
384
|
+
]);
|
|
385
|
+
if (!cachedCoinData) {
|
|
386
|
+
coinKlineCache.set(coinCacheKey, data);
|
|
387
|
+
}
|
|
388
|
+
if (!cachedBtcData) {
|
|
389
|
+
btcKlineCache.set(btcCacheKey, btcData);
|
|
390
|
+
}
|
|
391
|
+
if (!cachedBtcBinanceData) {
|
|
392
|
+
btcBinanceKlineCache.set(btcBinanceCacheKey, btcBinanceData);
|
|
393
|
+
}
|
|
394
|
+
if (!cachedBtcCoinbaseData) {
|
|
395
|
+
btcCoinbaseKlineCache.set(btcCoinbaseCacheKey, btcCoinbaseData);
|
|
396
|
+
}
|
|
397
|
+
const prevDataRaw = data.filter(
|
|
398
|
+
(candle) => candle.timestamp >= preloadStart && candle.timestamp < start
|
|
399
|
+
);
|
|
400
|
+
const btcPrevDataRaw = btcData.filter(
|
|
401
|
+
(candle) => candle.timestamp >= preloadStart && candle.timestamp < start
|
|
402
|
+
);
|
|
403
|
+
const testDataRaw = data.filter(
|
|
404
|
+
(candle) => candle.timestamp >= start
|
|
405
|
+
);
|
|
406
|
+
const btcTestDataRaw = btcData.filter(
|
|
407
|
+
(candle) => candle.timestamp >= start
|
|
408
|
+
);
|
|
409
|
+
const { alignedCoinCandles: prevData, alignedBtcCandles: btcPrevData } = alignSortedCandlesByTimestamp(prevDataRaw, btcPrevDataRaw);
|
|
410
|
+
const { alignedCoinCandles: testData, alignedBtcCandles: btcTestData } = alignSortedCandlesByTimestamp(testDataRaw, btcTestDataRaw);
|
|
411
|
+
const { alignedBtcCandles: btcBinancePrevData } = alignSortedCandlesByTimestamp(
|
|
412
|
+
prevDataRaw,
|
|
413
|
+
btcBinanceData.filter(
|
|
414
|
+
(candle) => candle.timestamp >= preloadStart && candle.timestamp < start
|
|
415
|
+
)
|
|
416
|
+
);
|
|
417
|
+
const { alignedBtcCandles: btcCoinbasePrevData } = alignSortedCandlesByTimestamp(
|
|
418
|
+
prevDataRaw,
|
|
419
|
+
btcCoinbaseData.filter(
|
|
420
|
+
(candle) => candle.timestamp >= preloadStart && candle.timestamp < start
|
|
421
|
+
)
|
|
422
|
+
);
|
|
423
|
+
const testConnector = createTestConnector(connector, {
|
|
424
|
+
userName,
|
|
425
|
+
mlEnabled: ml
|
|
426
|
+
});
|
|
427
|
+
const strategy = await strategyCreator({
|
|
428
|
+
userName,
|
|
429
|
+
config: strategyConfig,
|
|
430
|
+
symbol,
|
|
431
|
+
data: prevData,
|
|
432
|
+
btcData: btcPrevData,
|
|
433
|
+
btcBinanceData: btcBinancePrevData,
|
|
434
|
+
btcCoinbaseData: btcCoinbasePrevData,
|
|
435
|
+
connector: testConnector
|
|
436
|
+
});
|
|
437
|
+
const pendingMlPayloadBySignalId = /* @__PURE__ */ new Map();
|
|
438
|
+
const flushMlResultsBatch = async () => {
|
|
439
|
+
if (!ml) return;
|
|
440
|
+
const batch = await testConnector.drainMlResultsBatch();
|
|
441
|
+
if (!batch.length) return;
|
|
442
|
+
for (const resultRecord of batch) {
|
|
443
|
+
const payload = pendingMlPayloadBySignalId.get(resultRecord.signalId);
|
|
444
|
+
if (!payload) continue;
|
|
445
|
+
pendingMlPayloadBySignalId.delete(resultRecord.signalId);
|
|
446
|
+
const fullRow = buildMlTrainingRow(payload, {
|
|
447
|
+
profit: resultRecord.profit
|
|
448
|
+
});
|
|
449
|
+
const row = trimMlTrainingRowWindows(fullRow, 5);
|
|
450
|
+
await appendMlDatasetRow({
|
|
451
|
+
strategyName,
|
|
452
|
+
chunkId,
|
|
453
|
+
row
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
for (let candleIndex = 0; candleIndex < testData.length; candleIndex++) {
|
|
458
|
+
const candle = testData[candleIndex];
|
|
459
|
+
const btcCandle = btcTestData[candleIndex];
|
|
460
|
+
await testConnector.checkSl(candle);
|
|
461
|
+
await testConnector.checkTp(candle);
|
|
462
|
+
const signal = await strategy(candle, btcCandle);
|
|
463
|
+
if (ml && signal && typeof signal !== "string" && signal.signalId) {
|
|
464
|
+
const payload = buildMlPayload({
|
|
465
|
+
signal,
|
|
466
|
+
context: {
|
|
467
|
+
userName,
|
|
468
|
+
testId,
|
|
469
|
+
testSuiteId,
|
|
470
|
+
testName: name,
|
|
471
|
+
symbol,
|
|
472
|
+
strategyName,
|
|
473
|
+
strategyConfig,
|
|
474
|
+
connectorName
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
pendingMlPayloadBySignalId.set(signal.signalId, payload);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
await flushMlResultsBatch();
|
|
481
|
+
return await testConnector.getResult();
|
|
482
|
+
};
|
|
483
|
+
export {
|
|
484
|
+
createTestConnector,
|
|
485
|
+
resetTestingKlineCache,
|
|
486
|
+
testing
|
|
487
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/constants.ts
|
|
2
|
+
var { NODE_ENV } = process.env;
|
|
3
|
+
var KLINE_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 5 : 10;
|
|
4
|
+
var TG_CONCURRENCY_LIMIT = 3;
|
|
5
|
+
var AI_CONCURRENCY_LIMIT = 3;
|
|
6
|
+
var SCREENSHOT_CONCURRENCY_LIMIT = NODE_ENV === "production" ? 1 : 2;
|
|
7
|
+
|
|
8
|
+
export {
|
|
9
|
+
KLINE_CONCURRENCY_LIMIT,
|
|
10
|
+
TG_CONCURRENCY_LIMIT,
|
|
11
|
+
AI_CONCURRENCY_LIMIT,
|
|
12
|
+
SCREENSHOT_CONCURRENCY_LIMIT
|
|
13
|
+
};
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
8
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
9
|
+
}) : x)(function(x) {
|
|
10
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
11
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
12
|
+
});
|
|
13
|
+
var __commonJS = (cb, mod) => function __require2() {
|
|
14
|
+
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
15
|
+
};
|
|
16
|
+
var __copyProps = (to, from, except, desc) => {
|
|
17
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
18
|
+
for (let key of __getOwnPropNames(from))
|
|
19
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
20
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
25
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
26
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
27
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
28
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
29
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
30
|
+
mod
|
|
31
|
+
));
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
__require,
|
|
35
|
+
__commonJS,
|
|
36
|
+
__toESM
|
|
37
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__require
|
|
3
|
+
} from "./chunk-6DZX6EAA.mjs";
|
|
4
|
+
|
|
5
|
+
// src/pine.ts
|
|
6
|
+
import fs from "fs";
|
|
7
|
+
import path from "path";
|
|
8
|
+
export * from "@tradejs/core/pine";
|
|
9
|
+
var loadPinets = () => {
|
|
10
|
+
const resolvedPath = __require.resolve("pinets");
|
|
11
|
+
const cjsPath = resolvedPath.includes("pinets.min.browser") ? resolvedPath.replace(/pinets\.min\.browser(\.es)?\.js$/, "pinets.min.cjs") : resolvedPath;
|
|
12
|
+
return __require(cjsPath);
|
|
13
|
+
};
|
|
14
|
+
var loadPineScript = (filePath, fallback = "") => {
|
|
15
|
+
const resolvedPath = String(filePath || "").trim();
|
|
16
|
+
if (!resolvedPath) {
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
return fs.readFileSync(resolvedPath, "utf8").trim();
|
|
21
|
+
} catch {
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var createLoadPineScript = (baseDir) => {
|
|
26
|
+
const resolvedBaseDir = path.resolve(baseDir);
|
|
27
|
+
return (fileNameOrPath, fallback = "") => {
|
|
28
|
+
const rawPath = String(fileNameOrPath || "").trim();
|
|
29
|
+
if (!rawPath) {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
const resolvedPath = path.isAbsolute(rawPath) ? rawPath : path.resolve(resolvedBaseDir, rawPath);
|
|
33
|
+
return loadPineScript(resolvedPath, fallback);
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
var MINUTE_MS = 6e4;
|
|
37
|
+
var normalizeTimestampMs = (timestamp) => timestamp < 1e12 ? timestamp * 1e3 : timestamp;
|
|
38
|
+
var resolveCandleDuration = (candles) => {
|
|
39
|
+
if (candles.length < 2) {
|
|
40
|
+
return MINUTE_MS;
|
|
41
|
+
}
|
|
42
|
+
const first = normalizeTimestampMs(candles[0].timestamp);
|
|
43
|
+
const second = normalizeTimestampMs(candles[1].timestamp);
|
|
44
|
+
const duration = Math.max(second - first, MINUTE_MS);
|
|
45
|
+
return Number.isFinite(duration) && duration > 0 ? duration : MINUTE_MS;
|
|
46
|
+
};
|
|
47
|
+
var toPineRuntimeCandles = (candles) => {
|
|
48
|
+
const candleDuration = resolveCandleDuration(candles);
|
|
49
|
+
return candles.map((candle) => {
|
|
50
|
+
const openTime = normalizeTimestampMs(candle.timestamp);
|
|
51
|
+
return {
|
|
52
|
+
open: Number(candle.open),
|
|
53
|
+
high: Number(candle.high),
|
|
54
|
+
low: Number(candle.low),
|
|
55
|
+
close: Number(candle.close),
|
|
56
|
+
volume: Number(candle.volume ?? 0),
|
|
57
|
+
openTime,
|
|
58
|
+
closeTime: openTime + candleDuration
|
|
59
|
+
};
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
var runPineScript = async ({
|
|
63
|
+
candles,
|
|
64
|
+
script,
|
|
65
|
+
symbol = "SYMBOL",
|
|
66
|
+
timeframe = "15",
|
|
67
|
+
inputs = {},
|
|
68
|
+
limit
|
|
69
|
+
}) => {
|
|
70
|
+
const { PineTS, Indicator } = loadPinets();
|
|
71
|
+
const trimmedScript = String(script || "").trim();
|
|
72
|
+
if (!trimmedScript) {
|
|
73
|
+
throw new Error("Pine script is empty");
|
|
74
|
+
}
|
|
75
|
+
if (!Array.isArray(candles) || candles.length === 0) {
|
|
76
|
+
throw new Error("No candles provided for Pine script execution");
|
|
77
|
+
}
|
|
78
|
+
const pineCandles = toPineRuntimeCandles(candles);
|
|
79
|
+
const pine = new PineTS(
|
|
80
|
+
pineCandles,
|
|
81
|
+
symbol,
|
|
82
|
+
timeframe,
|
|
83
|
+
Math.max(1, limit ?? pineCandles.length)
|
|
84
|
+
);
|
|
85
|
+
const indicator = new Indicator(trimmedScript, inputs);
|
|
86
|
+
const context = await pine.run(indicator);
|
|
87
|
+
return context;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
loadPineScript,
|
|
92
|
+
createLoadPineScript,
|
|
93
|
+
runPineScript
|
|
94
|
+
};
|