@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.
@@ -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
+ };