@backtest-kit/cli 5.9.0 → 5.10.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 CHANGED
@@ -436,6 +436,169 @@ For projects that compile to or use CommonJS. Loaded via `require()`:
436
436
  }
437
437
  ```
438
438
 
439
+ ## 🌲 Running Local PineScript Indicators
440
+
441
+ `@backtest-kit/cli` can execute any local `.pine` file against a real exchange and print the results as a Markdown table — no TradingView account required.
442
+
443
+ ### CLI Flags
444
+
445
+ | Flag | Type | Description |
446
+ |------|------|-------------|
447
+ | `--pine` | boolean | Enable PineScript execution mode |
448
+ | `--symbol` | string | Trading pair (default: `"BTCUSDT"`) |
449
+ | `--timeframe` | string | Candle interval (default: `"15m"`) |
450
+ | `--limit` | string | Number of candles to fetch (default: `250`) |
451
+ | `--when` | string | End date for candle window — ISO 8601 or Unix ms (default: now) |
452
+ | `--exchange` | string | Exchange name (default: first registered, falls back to CCXT Binance) |
453
+ | `--json` | string | Write plots as a JSON array to a file (e.g. `--json=./output.json`) |
454
+ | `--jsonl` | string | Write plots as JSONL (one row per line) to a file (e.g. `--jsonl=./output.jsonl`) |
455
+ | `--markdown` | string | Write Markdown table to a file (e.g. `--markdown=./output.md`) |
456
+
457
+ **Important:** `limit` must cover indicator warmup bars — rows before warmup completes will show `N/A`
458
+
459
+ **Positional argument:** path to the `.pine` file.
460
+
461
+ ### Exchange via `pine.module`
462
+
463
+ By default the CLI registers CCXT Binance automatically. To use a different exchange — or to configure API keys, custom rate limits, or a non-spot market — create a `modules/pine.module.ts` file. The CLI loads it automatically before running the script.
464
+
465
+ The CLI looks for `modules/pine.module` in two locations (first match wins):
466
+
467
+ 1. **Next to the `.pine` file** — `<pine-file-dir>/modules/pine.module.ts`
468
+ 2. **Project root** — `<cwd>/modules/pine.module.ts`
469
+
470
+ ```
471
+ my-project/
472
+ ├── math/
473
+ │ ├── master_trend_15m.pine ← indicator
474
+ │ └── modules/
475
+ │ └── pine.module.ts ← loaded first (next to .pine file)
476
+ ├── modules/
477
+ │ └── pine.module.ts ← fallback (project root)
478
+ └── package.json
479
+ ```
480
+
481
+ Inside `pine.module.ts` call `addExchangeSchema` from `backtest-kit` and give the exchange a name:
482
+
483
+ ```typescript
484
+ // modules/pine.module.ts
485
+ import { addExchangeSchema } from "backtest-kit";
486
+ import ccxt from "ccxt";
487
+
488
+ addExchangeSchema({
489
+ exchangeName: "my-exchange",
490
+ getCandles: async (symbol, interval, since, limit) => {
491
+ const exchange = new ccxt.bybit({ enableRateLimit: true });
492
+ const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
493
+ return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
494
+ timestamp, open, high, low, close, volume,
495
+ }));
496
+ },
497
+ formatPrice: (symbol, price) => price.toFixed(2),
498
+ formatQuantity: (symbol, quantity) => quantity.toFixed(8),
499
+ });
500
+ ```
501
+
502
+ ### Environment variables (`.env`)
503
+
504
+ Before loading `pine.module`, the CLI loads `.env` files in the same order as for strategy modules — project root first, then the `.pine` file directory (overrides root):
505
+
506
+ ```
507
+ my-project/
508
+ ├── math/
509
+ │ ├── .env ← loaded second (overrides root)
510
+ │ └── master_trend_15m.pine
511
+ ├── .env ← loaded first
512
+ └── package.json
513
+ ```
514
+
515
+ Use this to store API keys without hardcoding them:
516
+
517
+ ```env
518
+ # .env
519
+ BYBIT_API_KEY=xxx
520
+ BYBIT_API_SECRET=yyy
521
+ ```
522
+
523
+ ```typescript
524
+ // modules/pine.module.ts
525
+ addExchangeSchema({
526
+ exchangeName: "my-exchange",
527
+ getCandles: async (symbol, interval, since, limit) => {
528
+ const exchange = new ccxt.bybit({
529
+ apiKey: process.env.BYBIT_API_KEY,
530
+ secret: process.env.BYBIT_API_SECRET,
531
+ enableRateLimit: true,
532
+ });
533
+ // ...
534
+ },
535
+ });
536
+ ```
537
+
538
+ Then run:
539
+
540
+ ```bash
541
+ npx @backtest-kit/cli --pine ./math/master_trend_15m.pine \
542
+ --exchange my-exchange \
543
+ --symbol BTCUSDT \
544
+ --timeframe 15m \
545
+ --limit 180 \
546
+ --when "2025-09-24T12:00:00.000Z"
547
+ ```
548
+
549
+ Or add it to `package.json`:
550
+
551
+ ```json
552
+ {
553
+ "scripts": {
554
+ "pine": "npx @backtest-kit/cli --pine ./math/master_trend_15m.pine --symbol BTCUSDT --timeframe 15m --limit 180"
555
+ }
556
+ }
557
+ ```
558
+
559
+ ```bash
560
+ npm run pine
561
+ ```
562
+
563
+ ### PineScript Requirements
564
+
565
+ The CLI reads all `plot()` calls that use `display=display.data_window` as output columns. Every other `plot()` is ignored. Name each output plot explicitly:
566
+
567
+ ```pine
568
+ //@version=5
569
+ indicator("MyIndicator", overlay=true)
570
+
571
+ // ... computation ...
572
+
573
+ plot(close, "Close", display=display.data_window)
574
+ plot(position, "Position", display=display.data_window)
575
+ ```
576
+
577
+ The column names in the output Markdown table are taken directly from those plot names — no manual schema definition needed.
578
+
579
+ ### Output
580
+
581
+ The CLI prints a Markdown table to stdout:
582
+
583
+ ```
584
+ # PineScript Technical Analysis Dump
585
+
586
+ **Signal ID**: CLI execution 2025-09-24T12:00:00.000Z
587
+
588
+ | Close | Position | timestamp |
589
+ | --- | --- | --- |
590
+ | 112871.28 | -1.0000 | 2025-09-22T15:00:00.000Z |
591
+ | 112666.69 | -1.0000 | 2025-09-22T15:15:00.000Z |
592
+ | 112736.00 | 0.0000 | 2025-09-22T18:30:00.000Z |
593
+ | 112653.90 | 1.0000 | 2025-09-22T22:15:00.000Z |
594
+ ```
595
+
596
+ Redirect to a file to save the report:
597
+
598
+ ```bash
599
+ npm run pine > report.md
600
+ ```
601
+
439
602
  ## 🌍 Environment Variables
440
603
 
441
604
  Create a `.env` file in your project root:
@@ -474,7 +637,6 @@ When your strategy module does not register an exchange, frame, or strategy name
474
637
 
475
638
  > **Note:** The default exchange schema **does not support order book fetching in backtest mode**. If your strategy calls `getOrderBook()` during backtest, you must register a custom exchange schema with your own snapshot storage.
476
639
 
477
-
478
640
  ## 🔧 Programmatic API
479
641
 
480
642
  In addition to the CLI, `@backtest-kit/cli` can be used as a library — call `run()` directly from your own script without spawning a child process or parsing CLI flags.
package/build/index.cjs CHANGED
@@ -93,6 +93,8 @@ var BacktestKitSignals__namespace = /*#__PURE__*/_interopNamespaceDefault(Backte
93
93
  {
94
94
  BacktestKit.Markdown.enable();
95
95
  BacktestKit.Report.enable();
96
+ BacktestKit.Dump.enable();
97
+ BacktestKit.Memory.enable();
96
98
  }
97
99
  {
98
100
  BacktestKit.Dump.useMarkdown();
@@ -296,12 +298,31 @@ class ResolveService {
296
298
  this.loggerService.log("resolveService getIsLaunched");
297
299
  return _is_launched;
298
300
  };
299
- this.attachEntryPoint = async (entryPoint) => {
300
- this.loggerService.log("resolveService attachEntryPoint");
301
+ this.attachPine = async (pinePath) => {
302
+ this.loggerService.log("resolveService attachPine");
301
303
  if (_is_launched) {
302
304
  throw new Error("Entry point is already attached. Multiple entry points are not allowed.");
303
305
  }
304
- const absolutePath = path.resolve(entryPoint);
306
+ const absolutePath = path.resolve(pinePath);
307
+ await fs$1.access(absolutePath, fs.constants.F_OK | fs.constants.R_OK);
308
+ const pineDir = path.dirname(absolutePath);
309
+ {
310
+ const cwd = process.cwd();
311
+ process.chdir(pineDir);
312
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
313
+ dotenv.config({ path: path.join(pineDir, '.env'), override: true, quiet: true });
314
+ }
315
+ _is_launched = true;
316
+ return await fs$1.readFile(absolutePath, "utf-8");
317
+ };
318
+ this.attachJavascript = async (jsPath) => {
319
+ this.loggerService.log("resolveService attachJavascript", {
320
+ jsPath
321
+ });
322
+ if (_is_launched) {
323
+ throw new Error("Entry point is already attached. Multiple entry points are not allowed.");
324
+ }
325
+ const absolutePath = path.resolve(jsPath);
305
326
  await fs$1.access(absolutePath, fs.constants.F_OK | fs.constants.R_OK);
306
327
  const moduleRoot = path.dirname(absolutePath);
307
328
  {
@@ -428,6 +449,7 @@ const getArgs = functoolsKit.singleshot(() => {
428
449
  const { values, positionals } = util.parseArgs({
429
450
  args: process.argv,
430
451
  options: {
452
+ // backtest entry
431
453
  symbol: {
432
454
  type: "string",
433
455
  default: "",
@@ -480,6 +502,35 @@ const getArgs = functoolsKit.singleshot(() => {
480
502
  type: "string",
481
503
  default: "1m, 15m, 30m, 4h",
482
504
  },
505
+ // pinescript entry
506
+ pine: {
507
+ type: "boolean",
508
+ default: false,
509
+ },
510
+ timeframe: {
511
+ type: "string",
512
+ default: "",
513
+ },
514
+ limit: {
515
+ type: "string",
516
+ default: "",
517
+ },
518
+ when: {
519
+ type: "string",
520
+ default: "",
521
+ },
522
+ json: {
523
+ type: "string",
524
+ default: "",
525
+ },
526
+ jsonl: {
527
+ type: "string",
528
+ default: "",
529
+ },
530
+ markdown: {
531
+ type: "string",
532
+ default: "",
533
+ },
483
534
  },
484
535
  strict: false,
485
536
  allowPositionals: true,
@@ -621,7 +672,7 @@ class BacktestMainService {
621
672
  this.frontendProviderService.connect();
622
673
  this.telegramProviderService.connect();
623
674
  }
624
- await this.resolveService.attachEntryPoint(payload.entryPoint);
675
+ await this.resolveService.attachJavascript(payload.entryPoint);
625
676
  {
626
677
  this.exchangeSchemaService.addSchema();
627
678
  this.symbolSchemaService.addSchema();
@@ -714,7 +765,7 @@ class LiveMainService {
714
765
  this.frontendProviderService.connect();
715
766
  this.telegramProviderService.connect();
716
767
  }
717
- await this.resolveService.attachEntryPoint(payload.entryPoint);
768
+ await this.resolveService.attachJavascript(payload.entryPoint);
718
769
  {
719
770
  this.exchangeSchemaService.addSchema();
720
771
  this.symbolSchemaService.addSchema();
@@ -787,7 +838,7 @@ class PaperMainService {
787
838
  this.frontendProviderService.connect();
788
839
  this.telegramProviderService.connect();
789
840
  }
790
- await this.resolveService.attachEntryPoint(payload.entryPoint);
841
+ await this.resolveService.attachJavascript(payload.entryPoint);
791
842
  {
792
843
  this.exchangeSchemaService.addSchema();
793
844
  this.symbolSchemaService.addSchema();
@@ -2147,7 +2198,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
2147
2198
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
2148
2199
  process.on("SIGINT", BEFORE_EXIT_FN$4);
2149
2200
  });
2150
- const main$4 = async () => {
2201
+ const main$5 = async () => {
2151
2202
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
2152
2203
  return;
2153
2204
  }
@@ -2158,7 +2209,7 @@ const main$4 = async () => {
2158
2209
  await cli.backtestMainService.connect();
2159
2210
  listenGracefulShutdown$4();
2160
2211
  };
2161
- main$4();
2212
+ main$5();
2162
2213
 
2163
2214
  const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
2164
2215
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -2179,7 +2230,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
2179
2230
  const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
2180
2231
  process.on("SIGINT", BEFORE_EXIT_FN$3);
2181
2232
  });
2182
- const main$3 = async () => {
2233
+ const main$4 = async () => {
2183
2234
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
2184
2235
  return;
2185
2236
  }
@@ -2190,7 +2241,7 @@ const main$3 = async () => {
2190
2241
  cli.paperMainService.connect();
2191
2242
  listenGracefulShutdown$3();
2192
2243
  };
2193
- main$3();
2244
+ main$4();
2194
2245
 
2195
2246
  const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
2196
2247
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -2211,7 +2262,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
2211
2262
  const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
2212
2263
  process.on("SIGINT", BEFORE_EXIT_FN$2);
2213
2264
  });
2214
- const main$2 = async () => {
2265
+ const main$3 = async () => {
2215
2266
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
2216
2267
  return;
2217
2268
  }
@@ -2222,7 +2273,7 @@ const main$2 = async () => {
2222
2273
  await cli.liveMainService.connect();
2223
2274
  listenGracefulShutdown$2();
2224
2275
  };
2225
- main$2();
2276
+ main$3();
2226
2277
 
2227
2278
  const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
2228
2279
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -2232,7 +2283,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
2232
2283
  const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
2233
2284
  process.on("SIGINT", BEFORE_EXIT_FN$1);
2234
2285
  });
2235
- const main$1 = async () => {
2286
+ const main$2 = async () => {
2236
2287
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
2237
2288
  return;
2238
2289
  }
@@ -2242,7 +2293,7 @@ const main$1 = async () => {
2242
2293
  }
2243
2294
  listenGracefulShutdown$1();
2244
2295
  };
2245
- main$1();
2296
+ main$2();
2246
2297
 
2247
2298
  const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
2248
2299
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -2252,7 +2303,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
2252
2303
  const listenGracefulShutdown = functoolsKit.singleshot(() => {
2253
2304
  process.on("SIGINT", BEFORE_EXIT_FN);
2254
2305
  });
2255
- const main = async () => {
2306
+ const main$1 = async () => {
2256
2307
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
2257
2308
  return;
2258
2309
  }
@@ -2262,6 +2313,92 @@ const main = async () => {
2262
2313
  }
2263
2314
  listenGracefulShutdown();
2264
2315
  };
2316
+ main$1();
2317
+
2318
+ const EXTRACT_ROWS_FN = (plots, schema) => {
2319
+ const keys = Object.keys(schema);
2320
+ const dataLength = keys
2321
+ .map((k) => plots[k]?.data?.length ?? 0)
2322
+ .reduce((acm, cur) => Math.max(acm, cur), 0);
2323
+ const rows = [];
2324
+ for (let i = 0; i < dataLength; i++) {
2325
+ const row = {};
2326
+ for (const key of keys) {
2327
+ const point = plots[key]?.data?.[i];
2328
+ row[key] = point?.value ?? null;
2329
+ }
2330
+ const point = plots[keys[0]]?.data?.[i];
2331
+ if (point?.time) {
2332
+ row.timestamp = new Date(point.time).toISOString();
2333
+ }
2334
+ rows.push(row);
2335
+ }
2336
+ return rows;
2337
+ };
2338
+ const main = async () => {
2339
+ if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
2340
+ return;
2341
+ }
2342
+ const { values, positionals } = getArgs();
2343
+ if (!values.pine) {
2344
+ return;
2345
+ }
2346
+ const [entryPoint = null] = positionals.slice(-1);
2347
+ if (!entryPoint) {
2348
+ return;
2349
+ }
2350
+ const source = await cli.resolveService.attachPine(entryPoint);
2351
+ await cli.moduleConnectionService.loadModule("./pine.module");
2352
+ {
2353
+ await cli.exchangeSchemaService.addSchema();
2354
+ await cli.symbolSchemaService.addSchema();
2355
+ }
2356
+ const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
2357
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
2358
+ const symbol = values.symbol || "BTCUSDT";
2359
+ const timeframe = values.timeframe || "15m";
2360
+ const limitStr = values.limit || "250";
2361
+ const limitNum = parseInt(limitStr);
2362
+ const limit = isNaN(limitNum) ? 250 : limitNum;
2363
+ const whenStr = values.when || Date.now().toString();
2364
+ const whenStamp = Date.parse(whenStr);
2365
+ const when = isNaN(whenStamp) ? new Date() : new Date(whenStamp);
2366
+ const plots = await BacktestKitPinets.run(BacktestKitPinets.Code.fromString(source), {
2367
+ symbol,
2368
+ timeframe: timeframe,
2369
+ limit,
2370
+ }, exchangeName, when);
2371
+ const signalId = `CLI execution ${new Date().toISOString()}`;
2372
+ const signalSchema = Object.fromEntries(Object.keys(plots)
2373
+ .filter((key) => plots[key].data.some((v) => {
2374
+ if (typeof v?.value !== "number") {
2375
+ return false;
2376
+ }
2377
+ if (!isFinite(v.value)) {
2378
+ return false;
2379
+ }
2380
+ return true;
2381
+ }))
2382
+ .map((key) => [key, key]));
2383
+ const jsonPath = values.json;
2384
+ if (jsonPath) {
2385
+ const rows = EXTRACT_ROWS_FN(plots, signalSchema);
2386
+ await fs$1.writeFile(jsonPath, JSON.stringify(rows, null, 2), "utf-8");
2387
+ return;
2388
+ }
2389
+ const jsonlPath = values.jsonl;
2390
+ if (jsonlPath) {
2391
+ const rows = EXTRACT_ROWS_FN(plots, signalSchema);
2392
+ await fs$1.writeFile(jsonlPath, rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
2393
+ return;
2394
+ }
2395
+ const markdownPath = values.markdown;
2396
+ if (markdownPath) {
2397
+ await fs$1.writeFile(markdownPath, await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema), "utf-8");
2398
+ return;
2399
+ }
2400
+ console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
2401
+ };
2265
2402
  main();
2266
2403
 
2267
2404
  function setLogger(logger) {
package/build/index.mjs CHANGED
@@ -5,7 +5,7 @@ import { getErrorMessage, errorData, singleshot, str, BehaviorSubject, compose,
5
5
  import fs, { constants } from 'fs';
6
6
  import * as stackTrace from 'stack-trace';
7
7
  import path from 'path';
8
- import fs$1, { access } from 'fs/promises';
8
+ import fs$1, { access, readFile, writeFile } from 'fs/promises';
9
9
  import dotenv from 'dotenv';
10
10
  import { createActivator } from 'di-kit';
11
11
  import { fileURLToPath } from 'url';
@@ -28,6 +28,7 @@ import { createRequire } from 'module';
28
28
  import * as BacktestKitGraph from '@backtest-kit/graph';
29
29
  import * as BacktestKitOllama from '@backtest-kit/ollama';
30
30
  import * as BacktestKitPinets from '@backtest-kit/pinets';
31
+ import { run as run$1, Code, toMarkdown } from '@backtest-kit/pinets';
31
32
  import * as BacktestKitSignals from '@backtest-kit/signals';
32
33
 
33
34
  /**
@@ -67,6 +68,8 @@ import * as BacktestKitSignals from '@backtest-kit/signals';
67
68
  {
68
69
  Markdown.enable();
69
70
  Report.enable();
71
+ Dump.enable();
72
+ Memory.enable();
70
73
  }
71
74
  {
72
75
  Dump.useMarkdown();
@@ -270,12 +273,31 @@ class ResolveService {
270
273
  this.loggerService.log("resolveService getIsLaunched");
271
274
  return _is_launched;
272
275
  };
273
- this.attachEntryPoint = async (entryPoint) => {
274
- this.loggerService.log("resolveService attachEntryPoint");
276
+ this.attachPine = async (pinePath) => {
277
+ this.loggerService.log("resolveService attachPine");
275
278
  if (_is_launched) {
276
279
  throw new Error("Entry point is already attached. Multiple entry points are not allowed.");
277
280
  }
278
- const absolutePath = path.resolve(entryPoint);
281
+ const absolutePath = path.resolve(pinePath);
282
+ await access(absolutePath, constants.F_OK | constants.R_OK);
283
+ const pineDir = path.dirname(absolutePath);
284
+ {
285
+ const cwd = process.cwd();
286
+ process.chdir(pineDir);
287
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
288
+ dotenv.config({ path: path.join(pineDir, '.env'), override: true, quiet: true });
289
+ }
290
+ _is_launched = true;
291
+ return await readFile(absolutePath, "utf-8");
292
+ };
293
+ this.attachJavascript = async (jsPath) => {
294
+ this.loggerService.log("resolveService attachJavascript", {
295
+ jsPath
296
+ });
297
+ if (_is_launched) {
298
+ throw new Error("Entry point is already attached. Multiple entry points are not allowed.");
299
+ }
300
+ const absolutePath = path.resolve(jsPath);
279
301
  await access(absolutePath, constants.F_OK | constants.R_OK);
280
302
  const moduleRoot = path.dirname(absolutePath);
281
303
  {
@@ -402,6 +424,7 @@ const getArgs = singleshot(() => {
402
424
  const { values, positionals } = parseArgs({
403
425
  args: process.argv,
404
426
  options: {
427
+ // backtest entry
405
428
  symbol: {
406
429
  type: "string",
407
430
  default: "",
@@ -454,6 +477,35 @@ const getArgs = singleshot(() => {
454
477
  type: "string",
455
478
  default: "1m, 15m, 30m, 4h",
456
479
  },
480
+ // pinescript entry
481
+ pine: {
482
+ type: "boolean",
483
+ default: false,
484
+ },
485
+ timeframe: {
486
+ type: "string",
487
+ default: "",
488
+ },
489
+ limit: {
490
+ type: "string",
491
+ default: "",
492
+ },
493
+ when: {
494
+ type: "string",
495
+ default: "",
496
+ },
497
+ json: {
498
+ type: "string",
499
+ default: "",
500
+ },
501
+ jsonl: {
502
+ type: "string",
503
+ default: "",
504
+ },
505
+ markdown: {
506
+ type: "string",
507
+ default: "",
508
+ },
457
509
  },
458
510
  strict: false,
459
511
  allowPositionals: true,
@@ -595,7 +647,7 @@ class BacktestMainService {
595
647
  this.frontendProviderService.connect();
596
648
  this.telegramProviderService.connect();
597
649
  }
598
- await this.resolveService.attachEntryPoint(payload.entryPoint);
650
+ await this.resolveService.attachJavascript(payload.entryPoint);
599
651
  {
600
652
  this.exchangeSchemaService.addSchema();
601
653
  this.symbolSchemaService.addSchema();
@@ -688,7 +740,7 @@ class LiveMainService {
688
740
  this.frontendProviderService.connect();
689
741
  this.telegramProviderService.connect();
690
742
  }
691
- await this.resolveService.attachEntryPoint(payload.entryPoint);
743
+ await this.resolveService.attachJavascript(payload.entryPoint);
692
744
  {
693
745
  this.exchangeSchemaService.addSchema();
694
746
  this.symbolSchemaService.addSchema();
@@ -761,7 +813,7 @@ class PaperMainService {
761
813
  this.frontendProviderService.connect();
762
814
  this.telegramProviderService.connect();
763
815
  }
764
- await this.resolveService.attachEntryPoint(payload.entryPoint);
816
+ await this.resolveService.attachJavascript(payload.entryPoint);
765
817
  {
766
818
  this.exchangeSchemaService.addSchema();
767
819
  this.symbolSchemaService.addSchema();
@@ -2117,7 +2169,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
2117
2169
  const listenGracefulShutdown$4 = singleshot(() => {
2118
2170
  process.on("SIGINT", BEFORE_EXIT_FN$4);
2119
2171
  });
2120
- const main$4 = async () => {
2172
+ const main$5 = async () => {
2121
2173
  if (!getEntry(import.meta.url)) {
2122
2174
  return;
2123
2175
  }
@@ -2128,7 +2180,7 @@ const main$4 = async () => {
2128
2180
  await cli.backtestMainService.connect();
2129
2181
  listenGracefulShutdown$4();
2130
2182
  };
2131
- main$4();
2183
+ main$5();
2132
2184
 
2133
2185
  const BEFORE_EXIT_FN$3 = singleshot(async () => {
2134
2186
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -2149,7 +2201,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
2149
2201
  const listenGracefulShutdown$3 = singleshot(() => {
2150
2202
  process.on("SIGINT", BEFORE_EXIT_FN$3);
2151
2203
  });
2152
- const main$3 = async () => {
2204
+ const main$4 = async () => {
2153
2205
  if (!getEntry(import.meta.url)) {
2154
2206
  return;
2155
2207
  }
@@ -2160,7 +2212,7 @@ const main$3 = async () => {
2160
2212
  cli.paperMainService.connect();
2161
2213
  listenGracefulShutdown$3();
2162
2214
  };
2163
- main$3();
2215
+ main$4();
2164
2216
 
2165
2217
  const BEFORE_EXIT_FN$2 = singleshot(async () => {
2166
2218
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -2181,7 +2233,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
2181
2233
  const listenGracefulShutdown$2 = singleshot(() => {
2182
2234
  process.on("SIGINT", BEFORE_EXIT_FN$2);
2183
2235
  });
2184
- const main$2 = async () => {
2236
+ const main$3 = async () => {
2185
2237
  if (!getEntry(import.meta.url)) {
2186
2238
  return;
2187
2239
  }
@@ -2192,7 +2244,7 @@ const main$2 = async () => {
2192
2244
  await cli.liveMainService.connect();
2193
2245
  listenGracefulShutdown$2();
2194
2246
  };
2195
- main$2();
2247
+ main$3();
2196
2248
 
2197
2249
  const BEFORE_EXIT_FN$1 = singleshot(async () => {
2198
2250
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -2202,7 +2254,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
2202
2254
  const listenGracefulShutdown$1 = singleshot(() => {
2203
2255
  process.on("SIGINT", BEFORE_EXIT_FN$1);
2204
2256
  });
2205
- const main$1 = async () => {
2257
+ const main$2 = async () => {
2206
2258
  if (!getEntry(import.meta.url)) {
2207
2259
  return;
2208
2260
  }
@@ -2212,7 +2264,7 @@ const main$1 = async () => {
2212
2264
  }
2213
2265
  listenGracefulShutdown$1();
2214
2266
  };
2215
- main$1();
2267
+ main$2();
2216
2268
 
2217
2269
  const BEFORE_EXIT_FN = singleshot(async () => {
2218
2270
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -2222,7 +2274,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
2222
2274
  const listenGracefulShutdown = singleshot(() => {
2223
2275
  process.on("SIGINT", BEFORE_EXIT_FN);
2224
2276
  });
2225
- const main = async () => {
2277
+ const main$1 = async () => {
2226
2278
  if (!getEntry(import.meta.url)) {
2227
2279
  return;
2228
2280
  }
@@ -2232,6 +2284,92 @@ const main = async () => {
2232
2284
  }
2233
2285
  listenGracefulShutdown();
2234
2286
  };
2287
+ main$1();
2288
+
2289
+ const EXTRACT_ROWS_FN = (plots, schema) => {
2290
+ const keys = Object.keys(schema);
2291
+ const dataLength = keys
2292
+ .map((k) => plots[k]?.data?.length ?? 0)
2293
+ .reduce((acm, cur) => Math.max(acm, cur), 0);
2294
+ const rows = [];
2295
+ for (let i = 0; i < dataLength; i++) {
2296
+ const row = {};
2297
+ for (const key of keys) {
2298
+ const point = plots[key]?.data?.[i];
2299
+ row[key] = point?.value ?? null;
2300
+ }
2301
+ const point = plots[keys[0]]?.data?.[i];
2302
+ if (point?.time) {
2303
+ row.timestamp = new Date(point.time).toISOString();
2304
+ }
2305
+ rows.push(row);
2306
+ }
2307
+ return rows;
2308
+ };
2309
+ const main = async () => {
2310
+ if (!getEntry(import.meta.url)) {
2311
+ return;
2312
+ }
2313
+ const { values, positionals } = getArgs();
2314
+ if (!values.pine) {
2315
+ return;
2316
+ }
2317
+ const [entryPoint = null] = positionals.slice(-1);
2318
+ if (!entryPoint) {
2319
+ return;
2320
+ }
2321
+ const source = await cli.resolveService.attachPine(entryPoint);
2322
+ await cli.moduleConnectionService.loadModule("./pine.module");
2323
+ {
2324
+ await cli.exchangeSchemaService.addSchema();
2325
+ await cli.symbolSchemaService.addSchema();
2326
+ }
2327
+ const [defaultExchangeName = null] = await listExchangeSchema();
2328
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
2329
+ const symbol = values.symbol || "BTCUSDT";
2330
+ const timeframe = values.timeframe || "15m";
2331
+ const limitStr = values.limit || "250";
2332
+ const limitNum = parseInt(limitStr);
2333
+ const limit = isNaN(limitNum) ? 250 : limitNum;
2334
+ const whenStr = values.when || Date.now().toString();
2335
+ const whenStamp = Date.parse(whenStr);
2336
+ const when = isNaN(whenStamp) ? new Date() : new Date(whenStamp);
2337
+ const plots = await run$1(Code.fromString(source), {
2338
+ symbol,
2339
+ timeframe: timeframe,
2340
+ limit,
2341
+ }, exchangeName, when);
2342
+ const signalId = `CLI execution ${new Date().toISOString()}`;
2343
+ const signalSchema = Object.fromEntries(Object.keys(plots)
2344
+ .filter((key) => plots[key].data.some((v) => {
2345
+ if (typeof v?.value !== "number") {
2346
+ return false;
2347
+ }
2348
+ if (!isFinite(v.value)) {
2349
+ return false;
2350
+ }
2351
+ return true;
2352
+ }))
2353
+ .map((key) => [key, key]));
2354
+ const jsonPath = values.json;
2355
+ if (jsonPath) {
2356
+ const rows = EXTRACT_ROWS_FN(plots, signalSchema);
2357
+ await writeFile(jsonPath, JSON.stringify(rows, null, 2), "utf-8");
2358
+ return;
2359
+ }
2360
+ const jsonlPath = values.jsonl;
2361
+ if (jsonlPath) {
2362
+ const rows = EXTRACT_ROWS_FN(plots, signalSchema);
2363
+ await writeFile(jsonlPath, rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
2364
+ return;
2365
+ }
2366
+ const markdownPath = values.markdown;
2367
+ if (markdownPath) {
2368
+ await writeFile(markdownPath, await toMarkdown(signalId, plots, signalSchema), "utf-8");
2369
+ return;
2370
+ }
2371
+ console.log(await toMarkdown(signalId, plots, signalSchema));
2372
+ };
2235
2373
  main();
2236
2374
 
2237
2375
  function setLogger(logger) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtest-kit/cli",
3
- "version": "5.9.0",
3
+ "version": "5.10.0",
4
4
  "description": "Zero-boilerplate CLI runner for backtest-kit strategies. Run backtests, paper trading, and live bots with candle cache warming, web dashboard, and Telegram notifications — no setup code required.",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
@@ -45,7 +45,8 @@
45
45
  "url": "https://github.com/tripolskypetr/backtest-kit/issues"
46
46
  },
47
47
  "scripts": {
48
- "build": "rollup -c"
48
+ "build": "rollup -c",
49
+ "start": "npm run build && node build/index.mjs math/master_trend_15m.pine --pine --symbol BTCUSDT --timeframe 15m --limit 180"
49
50
  },
50
51
  "main": "build/index.cjs",
51
52
  "module": "build/index.mjs",
@@ -60,11 +61,11 @@
60
61
  "devDependencies": {
61
62
  "@babel/plugin-transform-modules-umd": "7.27.1",
62
63
  "@babel/standalone": "7.29.1",
63
- "@backtest-kit/ui": "5.9.0",
64
- "@backtest-kit/graph": "5.9.0",
65
- "@backtest-kit/ollama": "5.9.0",
66
- "@backtest-kit/pinets": "5.9.0",
67
- "@backtest-kit/signals": "5.9.0",
64
+ "@backtest-kit/ui": "5.10.0",
65
+ "@backtest-kit/graph": "5.10.0",
66
+ "@backtest-kit/ollama": "5.10.0",
67
+ "@backtest-kit/pinets": "5.10.0",
68
+ "@backtest-kit/signals": "5.10.0",
68
69
  "@rollup/plugin-replace": "6.0.3",
69
70
  "@rollup/plugin-typescript": "11.1.6",
70
71
  "@types/image-size": "0.7.0",
@@ -72,7 +73,7 @@
72
73
  "@types/mustache": "4.2.6",
73
74
  "@types/node": "22.9.0",
74
75
  "@types/stack-trace": "0.0.33",
75
- "backtest-kit": "5.9.0",
76
+ "backtest-kit": "5.10.0",
76
77
  "glob": "11.0.1",
77
78
  "markdown-it": "14.1.1",
78
79
  "rimraf": "6.0.1",
@@ -87,12 +88,12 @@
87
88
  "peerDependencies": {
88
89
  "@babel/plugin-transform-modules-umd": "^7.27.1",
89
90
  "@babel/standalone": "^7.29.1",
90
- "@backtest-kit/ui": "^5.9.0",
91
- "@backtest-kit/graph": "^5.9.0",
92
- "@backtest-kit/ollama": "^5.9.0",
93
- "@backtest-kit/pinets": "^5.9.0",
94
- "@backtest-kit/signals": "^5.9.0",
95
- "backtest-kit": "^5.9.0",
91
+ "@backtest-kit/ui": "^5.10.0",
92
+ "@backtest-kit/graph": "^5.10.0",
93
+ "@backtest-kit/ollama": "^5.10.0",
94
+ "@backtest-kit/pinets": "^5.10.0",
95
+ "@backtest-kit/signals": "^5.10.0",
96
+ "backtest-kit": "^5.10.0",
96
97
  "markdown-it": "^14.1.1",
97
98
  "typescript": "^5.0.0"
98
99
  },
package/types.d.ts CHANGED
@@ -103,7 +103,8 @@ declare class ResolveService {
103
103
  readonly OVERRIDE_TEMPLATE_DIR: string;
104
104
  readonly OVERRIDE_MODULES_DIR: string;
105
105
  getIsLaunched: () => boolean;
106
- attachEntryPoint: (entryPoint: string) => Promise<void>;
106
+ attachPine: (pinePath: string) => Promise<string>;
107
+ attachJavascript: (jsPath: string) => Promise<void>;
107
108
  }
108
109
 
109
110
  declare class ErrorService {