@backtest-kit/cli 5.9.1 → 5.10.1

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,183 @@ 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
+ | `--output` | string | Output file base name without extension (default: `.pine` file name) |
454
+ | `--json` | boolean | Write plots as a JSON array to `<pine-dir>/dump/{output}.json` |
455
+ | `--jsonl` | boolean | Write plots as JSONL (one row per line) to `<pine-dir>/dump/{output}.jsonl` |
456
+ | `--markdown` | boolean | Write Markdown table to `<pine-dir>/dump/{output}.md` |
457
+
458
+ **Important:** `limit` must cover indicator warmup bars — rows before warmup completes will show `N/A`
459
+
460
+ **Positional argument:** path to the `.pine` file.
461
+
462
+ ### Exchange via `pine.module`
463
+
464
+ 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.
465
+
466
+ The CLI looks for `modules/pine.module` in two locations (first match wins):
467
+
468
+ 1. **Next to the `.pine` file** — `<pine-file-dir>/modules/pine.module.ts`
469
+ 2. **Project root** — `<cwd>/modules/pine.module.ts`
470
+
471
+ ```
472
+ my-project/
473
+ ├── math/
474
+ │ ├── impulse_trend_15m.pine ← indicator
475
+ │ └── modules/
476
+ │ └── pine.module.ts ← loaded first (next to .pine file)
477
+ ├── modules/
478
+ │ └── pine.module.ts ← fallback (project root)
479
+ └── package.json
480
+ ```
481
+
482
+ Inside `pine.module.ts` call `addExchangeSchema` from `backtest-kit` and give the exchange a name:
483
+
484
+ ```typescript
485
+ // modules/pine.module.ts
486
+ import { addExchangeSchema } from "backtest-kit";
487
+ import ccxt from "ccxt";
488
+
489
+ addExchangeSchema({
490
+ exchangeName: "my-exchange",
491
+ getCandles: async (symbol, interval, since, limit) => {
492
+ const exchange = new ccxt.bybit({ enableRateLimit: true });
493
+ const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
494
+ return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
495
+ timestamp, open, high, low, close, volume,
496
+ }));
497
+ },
498
+ formatPrice: (symbol, price) => price.toFixed(2),
499
+ formatQuantity: (symbol, quantity) => quantity.toFixed(8),
500
+ });
501
+ ```
502
+
503
+ ### Environment variables (`.env`)
504
+
505
+ 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):
506
+
507
+ ```
508
+ my-project/
509
+ ├── math/
510
+ │ ├── .env ← loaded second (overrides root)
511
+ │ └── impulse_trend_15m.pine
512
+ ├── .env ← loaded first
513
+ └── package.json
514
+ ```
515
+
516
+ Use this to store API keys without hardcoding them:
517
+
518
+ ```env
519
+ # .env
520
+ BYBIT_API_KEY=xxx
521
+ BYBIT_API_SECRET=yyy
522
+ ```
523
+
524
+ ```typescript
525
+ // modules/pine.module.ts
526
+ addExchangeSchema({
527
+ exchangeName: "my-exchange",
528
+ getCandles: async (symbol, interval, since, limit) => {
529
+ const exchange = new ccxt.bybit({
530
+ apiKey: process.env.BYBIT_API_KEY,
531
+ secret: process.env.BYBIT_API_SECRET,
532
+ enableRateLimit: true,
533
+ });
534
+ // ...
535
+ },
536
+ });
537
+ ```
538
+
539
+ Then run:
540
+
541
+ ```bash
542
+ npx @backtest-kit/cli --pine ./math/impulse_trend_15m.pine \
543
+ --exchange my-exchange \
544
+ --symbol BTCUSDT \
545
+ --timeframe 15m \
546
+ --limit 180 \
547
+ --when "2025-09-24T12:00:00.000Z"
548
+ ```
549
+
550
+ Or add it to `package.json`:
551
+
552
+ ```json
553
+ {
554
+ "scripts": {
555
+ "pine": "npx @backtest-kit/cli --pine ./math/impulse_trend_15m.pine --symbol BTCUSDT --timeframe 15m --limit 180"
556
+ }
557
+ }
558
+ ```
559
+
560
+ ```bash
561
+ npm run pine
562
+ ```
563
+
564
+ ### PineScript Requirements
565
+
566
+ 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:
567
+
568
+ ```pine
569
+ //@version=5
570
+ indicator("MyIndicator", overlay=true)
571
+
572
+ // ... computation ...
573
+
574
+ plot(close, "Close", display=display.data_window)
575
+ plot(position, "Position", display=display.data_window)
576
+ ```
577
+
578
+ The column names in the output Markdown table are taken directly from those plot names — no manual schema definition needed.
579
+
580
+ ### Output
581
+
582
+ The CLI prints a Markdown table to stdout:
583
+
584
+ ```
585
+ # PineScript Technical Analysis Dump
586
+
587
+ **Signal ID**: CLI execution 2025-09-24T12:00:00.000Z
588
+
589
+ | Close | Position | timestamp |
590
+ | --- | --- | --- |
591
+ | 112871.28 | -1.0000 | 2025-09-22T15:00:00.000Z |
592
+ | 112666.69 | -1.0000 | 2025-09-22T15:15:00.000Z |
593
+ | 112736.00 | 0.0000 | 2025-09-22T18:30:00.000Z |
594
+ | 112653.90 | 1.0000 | 2025-09-22T22:15:00.000Z |
595
+ ```
596
+
597
+ Save to `./math/dump/impulse_trend_15m.md` (uses `.pine` file name automatically, dump is created next to the `.pine` file):
598
+
599
+ ```bash
600
+ npx @backtest-kit/cli --pine ./math/impulse_trend_15m.pine --markdown
601
+ ```
602
+
603
+ Override the output name with `--output`:
604
+
605
+ ```bash
606
+ npx @backtest-kit/cli --pine ./math/impulse_trend_15m.pine --jsonl --output feb2026_bb
607
+ # → ./math/dump/feb2026_bb.jsonl
608
+ ```
609
+
610
+ Print to stdout (no flag):
611
+
612
+ ```bash
613
+ npx @backtest-kit/cli --pine ./math/impulse_trend_15m.pine
614
+ ```
615
+
439
616
  ## 🌍 Environment Variables
440
617
 
441
618
  Create a `.env` file in your project root:
@@ -474,7 +651,6 @@ When your strategy module does not register an exchange, frame, or strategy name
474
651
 
475
652
  > **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
653
 
477
-
478
654
  ## 🔧 Programmatic API
479
655
 
480
656
  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,39 @@ 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
+ output: {
523
+ type: "string",
524
+ default: "",
525
+ },
526
+ json: {
527
+ type: "boolean",
528
+ default: false,
529
+ },
530
+ jsonl: {
531
+ type: "boolean",
532
+ default: false,
533
+ },
534
+ markdown: {
535
+ type: "boolean",
536
+ default: false,
537
+ },
483
538
  },
484
539
  strict: false,
485
540
  allowPositionals: true,
@@ -621,7 +676,7 @@ class BacktestMainService {
621
676
  this.frontendProviderService.connect();
622
677
  this.telegramProviderService.connect();
623
678
  }
624
- await this.resolveService.attachEntryPoint(payload.entryPoint);
679
+ await this.resolveService.attachJavascript(payload.entryPoint);
625
680
  {
626
681
  this.exchangeSchemaService.addSchema();
627
682
  this.symbolSchemaService.addSchema();
@@ -714,7 +769,7 @@ class LiveMainService {
714
769
  this.frontendProviderService.connect();
715
770
  this.telegramProviderService.connect();
716
771
  }
717
- await this.resolveService.attachEntryPoint(payload.entryPoint);
772
+ await this.resolveService.attachJavascript(payload.entryPoint);
718
773
  {
719
774
  this.exchangeSchemaService.addSchema();
720
775
  this.symbolSchemaService.addSchema();
@@ -787,7 +842,7 @@ class PaperMainService {
787
842
  this.frontendProviderService.connect();
788
843
  this.telegramProviderService.connect();
789
844
  }
790
- await this.resolveService.attachEntryPoint(payload.entryPoint);
845
+ await this.resolveService.attachJavascript(payload.entryPoint);
791
846
  {
792
847
  this.exchangeSchemaService.addSchema();
793
848
  this.symbolSchemaService.addSchema();
@@ -2147,7 +2202,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
2147
2202
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
2148
2203
  process.on("SIGINT", BEFORE_EXIT_FN$4);
2149
2204
  });
2150
- const main$4 = async () => {
2205
+ const main$5 = async () => {
2151
2206
  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
2207
  return;
2153
2208
  }
@@ -2158,7 +2213,7 @@ const main$4 = async () => {
2158
2213
  await cli.backtestMainService.connect();
2159
2214
  listenGracefulShutdown$4();
2160
2215
  };
2161
- main$4();
2216
+ main$5();
2162
2217
 
2163
2218
  const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
2164
2219
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -2179,7 +2234,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
2179
2234
  const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
2180
2235
  process.on("SIGINT", BEFORE_EXIT_FN$3);
2181
2236
  });
2182
- const main$3 = async () => {
2237
+ const main$4 = async () => {
2183
2238
  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
2239
  return;
2185
2240
  }
@@ -2190,7 +2245,7 @@ const main$3 = async () => {
2190
2245
  cli.paperMainService.connect();
2191
2246
  listenGracefulShutdown$3();
2192
2247
  };
2193
- main$3();
2248
+ main$4();
2194
2249
 
2195
2250
  const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
2196
2251
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -2211,7 +2266,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
2211
2266
  const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
2212
2267
  process.on("SIGINT", BEFORE_EXIT_FN$2);
2213
2268
  });
2214
- const main$2 = async () => {
2269
+ const main$3 = async () => {
2215
2270
  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
2271
  return;
2217
2272
  }
@@ -2222,7 +2277,7 @@ const main$2 = async () => {
2222
2277
  await cli.liveMainService.connect();
2223
2278
  listenGracefulShutdown$2();
2224
2279
  };
2225
- main$2();
2280
+ main$3();
2226
2281
 
2227
2282
  const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
2228
2283
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -2232,7 +2287,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
2232
2287
  const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
2233
2288
  process.on("SIGINT", BEFORE_EXIT_FN$1);
2234
2289
  });
2235
- const main$1 = async () => {
2290
+ const main$2 = async () => {
2236
2291
  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
2292
  return;
2238
2293
  }
@@ -2242,7 +2297,7 @@ const main$1 = async () => {
2242
2297
  }
2243
2298
  listenGracefulShutdown$1();
2244
2299
  };
2245
- main$1();
2300
+ main$2();
2246
2301
 
2247
2302
  const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
2248
2303
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -2252,7 +2307,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
2252
2307
  const listenGracefulShutdown = functoolsKit.singleshot(() => {
2253
2308
  process.on("SIGINT", BEFORE_EXIT_FN);
2254
2309
  });
2255
- const main = async () => {
2310
+ const main$1 = async () => {
2256
2311
  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
2312
  return;
2258
2313
  }
@@ -2262,6 +2317,94 @@ const main = async () => {
2262
2317
  }
2263
2318
  listenGracefulShutdown();
2264
2319
  };
2320
+ main$1();
2321
+
2322
+ const EXTRACT_ROWS_FN = (plots, schema) => {
2323
+ const keys = Object.keys(schema);
2324
+ const dataLength = keys
2325
+ .map((k) => plots[k]?.data?.length ?? 0)
2326
+ .reduce((acm, cur) => Math.max(acm, cur), 0);
2327
+ const rows = [];
2328
+ for (let i = 0; i < dataLength; i++) {
2329
+ const row = {};
2330
+ for (const key of keys) {
2331
+ const point = plots[key]?.data?.[i];
2332
+ row[key] = point?.value ?? null;
2333
+ }
2334
+ const point = plots[keys[0]]?.data?.[i];
2335
+ if (point?.time) {
2336
+ row.timestamp = new Date(point.time).toISOString();
2337
+ }
2338
+ rows.push(row);
2339
+ }
2340
+ return rows;
2341
+ };
2342
+ const main = async () => {
2343
+ 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)))) {
2344
+ return;
2345
+ }
2346
+ const { values, positionals } = getArgs();
2347
+ if (!values.pine) {
2348
+ return;
2349
+ }
2350
+ const [entryPoint = null] = positionals.slice(-1);
2351
+ if (!entryPoint) {
2352
+ return;
2353
+ }
2354
+ const source = await cli.resolveService.attachPine(entryPoint);
2355
+ await cli.moduleConnectionService.loadModule("./pine.module");
2356
+ {
2357
+ await cli.exchangeSchemaService.addSchema();
2358
+ await cli.symbolSchemaService.addSchema();
2359
+ }
2360
+ const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
2361
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
2362
+ const symbol = values.symbol || "BTCUSDT";
2363
+ const timeframe = values.timeframe || "15m";
2364
+ const limitStr = values.limit || "250";
2365
+ const limitNum = parseInt(limitStr);
2366
+ const limit = isNaN(limitNum) ? 250 : limitNum;
2367
+ const whenStr = values.when || Date.now().toString();
2368
+ const whenStamp = Date.parse(whenStr);
2369
+ const when = isNaN(whenStamp) ? new Date() : new Date(whenStamp);
2370
+ const plots = await BacktestKitPinets.run(BacktestKitPinets.Code.fromString(source), {
2371
+ symbol,
2372
+ timeframe: timeframe,
2373
+ limit,
2374
+ }, exchangeName, when);
2375
+ const signalId = `CLI execution ${new Date().toISOString()}`;
2376
+ const signalSchema = Object.fromEntries(Object.keys(plots)
2377
+ .filter((key) => plots[key].data.some((v) => {
2378
+ if (typeof v?.value !== "number") {
2379
+ return false;
2380
+ }
2381
+ if (!isFinite(v.value)) {
2382
+ return false;
2383
+ }
2384
+ return true;
2385
+ }))
2386
+ .map((key) => [key, key]));
2387
+ const dumpName = values.output || path.basename(entryPoint, path.extname(entryPoint));
2388
+ const dumpDir = path.join(process.cwd(), "dump");
2389
+ if (values.json) {
2390
+ const rows = EXTRACT_ROWS_FN(plots, signalSchema);
2391
+ await fs$1.mkdir(dumpDir, { recursive: true });
2392
+ await fs$1.writeFile(path.join(dumpDir, `${dumpName}.json`), JSON.stringify(rows, null, 2), "utf-8");
2393
+ return;
2394
+ }
2395
+ if (values.jsonl) {
2396
+ const rows = EXTRACT_ROWS_FN(plots, signalSchema);
2397
+ await fs$1.mkdir(dumpDir, { recursive: true });
2398
+ await fs$1.writeFile(path.join(dumpDir, `${dumpName}.jsonl`), rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
2399
+ return;
2400
+ }
2401
+ if (values.markdown) {
2402
+ await fs$1.mkdir(dumpDir, { recursive: true });
2403
+ await fs$1.writeFile(path.join(dumpDir, `${dumpName}.md`), await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema), "utf-8");
2404
+ return;
2405
+ }
2406
+ console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
2407
+ };
2265
2408
  main();
2266
2409
 
2267
2410
  function setLogger(logger) {
package/build/index.mjs CHANGED
@@ -4,8 +4,8 @@ import { Storage, Notification, Markdown, Report, Dump, Memory, StorageLive, Sto
4
4
  import { getErrorMessage, errorData, singleshot, str, BehaviorSubject, compose, execpool, queued, sleep, randomString, createAwaiter, TIMEOUT_SYMBOL, typo, retry, trycatch, memoize } from 'functools-kit';
5
5
  import fs, { constants } from 'fs';
6
6
  import * as stackTrace from 'stack-trace';
7
- import path from 'path';
8
- import fs$1, { access } from 'fs/promises';
7
+ import path, { basename, extname, join } from 'path';
8
+ import fs$1, { access, readFile, mkdir, 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,39 @@ 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
+ output: {
498
+ type: "string",
499
+ default: "",
500
+ },
501
+ json: {
502
+ type: "boolean",
503
+ default: false,
504
+ },
505
+ jsonl: {
506
+ type: "boolean",
507
+ default: false,
508
+ },
509
+ markdown: {
510
+ type: "boolean",
511
+ default: false,
512
+ },
457
513
  },
458
514
  strict: false,
459
515
  allowPositionals: true,
@@ -595,7 +651,7 @@ class BacktestMainService {
595
651
  this.frontendProviderService.connect();
596
652
  this.telegramProviderService.connect();
597
653
  }
598
- await this.resolveService.attachEntryPoint(payload.entryPoint);
654
+ await this.resolveService.attachJavascript(payload.entryPoint);
599
655
  {
600
656
  this.exchangeSchemaService.addSchema();
601
657
  this.symbolSchemaService.addSchema();
@@ -688,7 +744,7 @@ class LiveMainService {
688
744
  this.frontendProviderService.connect();
689
745
  this.telegramProviderService.connect();
690
746
  }
691
- await this.resolveService.attachEntryPoint(payload.entryPoint);
747
+ await this.resolveService.attachJavascript(payload.entryPoint);
692
748
  {
693
749
  this.exchangeSchemaService.addSchema();
694
750
  this.symbolSchemaService.addSchema();
@@ -761,7 +817,7 @@ class PaperMainService {
761
817
  this.frontendProviderService.connect();
762
818
  this.telegramProviderService.connect();
763
819
  }
764
- await this.resolveService.attachEntryPoint(payload.entryPoint);
820
+ await this.resolveService.attachJavascript(payload.entryPoint);
765
821
  {
766
822
  this.exchangeSchemaService.addSchema();
767
823
  this.symbolSchemaService.addSchema();
@@ -2117,7 +2173,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
2117
2173
  const listenGracefulShutdown$4 = singleshot(() => {
2118
2174
  process.on("SIGINT", BEFORE_EXIT_FN$4);
2119
2175
  });
2120
- const main$4 = async () => {
2176
+ const main$5 = async () => {
2121
2177
  if (!getEntry(import.meta.url)) {
2122
2178
  return;
2123
2179
  }
@@ -2128,7 +2184,7 @@ const main$4 = async () => {
2128
2184
  await cli.backtestMainService.connect();
2129
2185
  listenGracefulShutdown$4();
2130
2186
  };
2131
- main$4();
2187
+ main$5();
2132
2188
 
2133
2189
  const BEFORE_EXIT_FN$3 = singleshot(async () => {
2134
2190
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -2149,7 +2205,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
2149
2205
  const listenGracefulShutdown$3 = singleshot(() => {
2150
2206
  process.on("SIGINT", BEFORE_EXIT_FN$3);
2151
2207
  });
2152
- const main$3 = async () => {
2208
+ const main$4 = async () => {
2153
2209
  if (!getEntry(import.meta.url)) {
2154
2210
  return;
2155
2211
  }
@@ -2160,7 +2216,7 @@ const main$3 = async () => {
2160
2216
  cli.paperMainService.connect();
2161
2217
  listenGracefulShutdown$3();
2162
2218
  };
2163
- main$3();
2219
+ main$4();
2164
2220
 
2165
2221
  const BEFORE_EXIT_FN$2 = singleshot(async () => {
2166
2222
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -2181,7 +2237,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
2181
2237
  const listenGracefulShutdown$2 = singleshot(() => {
2182
2238
  process.on("SIGINT", BEFORE_EXIT_FN$2);
2183
2239
  });
2184
- const main$2 = async () => {
2240
+ const main$3 = async () => {
2185
2241
  if (!getEntry(import.meta.url)) {
2186
2242
  return;
2187
2243
  }
@@ -2192,7 +2248,7 @@ const main$2 = async () => {
2192
2248
  await cli.liveMainService.connect();
2193
2249
  listenGracefulShutdown$2();
2194
2250
  };
2195
- main$2();
2251
+ main$3();
2196
2252
 
2197
2253
  const BEFORE_EXIT_FN$1 = singleshot(async () => {
2198
2254
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -2202,7 +2258,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
2202
2258
  const listenGracefulShutdown$1 = singleshot(() => {
2203
2259
  process.on("SIGINT", BEFORE_EXIT_FN$1);
2204
2260
  });
2205
- const main$1 = async () => {
2261
+ const main$2 = async () => {
2206
2262
  if (!getEntry(import.meta.url)) {
2207
2263
  return;
2208
2264
  }
@@ -2212,7 +2268,7 @@ const main$1 = async () => {
2212
2268
  }
2213
2269
  listenGracefulShutdown$1();
2214
2270
  };
2215
- main$1();
2271
+ main$2();
2216
2272
 
2217
2273
  const BEFORE_EXIT_FN = singleshot(async () => {
2218
2274
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -2222,7 +2278,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
2222
2278
  const listenGracefulShutdown = singleshot(() => {
2223
2279
  process.on("SIGINT", BEFORE_EXIT_FN);
2224
2280
  });
2225
- const main = async () => {
2281
+ const main$1 = async () => {
2226
2282
  if (!getEntry(import.meta.url)) {
2227
2283
  return;
2228
2284
  }
@@ -2232,6 +2288,94 @@ const main = async () => {
2232
2288
  }
2233
2289
  listenGracefulShutdown();
2234
2290
  };
2291
+ main$1();
2292
+
2293
+ const EXTRACT_ROWS_FN = (plots, schema) => {
2294
+ const keys = Object.keys(schema);
2295
+ const dataLength = keys
2296
+ .map((k) => plots[k]?.data?.length ?? 0)
2297
+ .reduce((acm, cur) => Math.max(acm, cur), 0);
2298
+ const rows = [];
2299
+ for (let i = 0; i < dataLength; i++) {
2300
+ const row = {};
2301
+ for (const key of keys) {
2302
+ const point = plots[key]?.data?.[i];
2303
+ row[key] = point?.value ?? null;
2304
+ }
2305
+ const point = plots[keys[0]]?.data?.[i];
2306
+ if (point?.time) {
2307
+ row.timestamp = new Date(point.time).toISOString();
2308
+ }
2309
+ rows.push(row);
2310
+ }
2311
+ return rows;
2312
+ };
2313
+ const main = async () => {
2314
+ if (!getEntry(import.meta.url)) {
2315
+ return;
2316
+ }
2317
+ const { values, positionals } = getArgs();
2318
+ if (!values.pine) {
2319
+ return;
2320
+ }
2321
+ const [entryPoint = null] = positionals.slice(-1);
2322
+ if (!entryPoint) {
2323
+ return;
2324
+ }
2325
+ const source = await cli.resolveService.attachPine(entryPoint);
2326
+ await cli.moduleConnectionService.loadModule("./pine.module");
2327
+ {
2328
+ await cli.exchangeSchemaService.addSchema();
2329
+ await cli.symbolSchemaService.addSchema();
2330
+ }
2331
+ const [defaultExchangeName = null] = await listExchangeSchema();
2332
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
2333
+ const symbol = values.symbol || "BTCUSDT";
2334
+ const timeframe = values.timeframe || "15m";
2335
+ const limitStr = values.limit || "250";
2336
+ const limitNum = parseInt(limitStr);
2337
+ const limit = isNaN(limitNum) ? 250 : limitNum;
2338
+ const whenStr = values.when || Date.now().toString();
2339
+ const whenStamp = Date.parse(whenStr);
2340
+ const when = isNaN(whenStamp) ? new Date() : new Date(whenStamp);
2341
+ const plots = await run$1(Code.fromString(source), {
2342
+ symbol,
2343
+ timeframe: timeframe,
2344
+ limit,
2345
+ }, exchangeName, when);
2346
+ const signalId = `CLI execution ${new Date().toISOString()}`;
2347
+ const signalSchema = Object.fromEntries(Object.keys(plots)
2348
+ .filter((key) => plots[key].data.some((v) => {
2349
+ if (typeof v?.value !== "number") {
2350
+ return false;
2351
+ }
2352
+ if (!isFinite(v.value)) {
2353
+ return false;
2354
+ }
2355
+ return true;
2356
+ }))
2357
+ .map((key) => [key, key]));
2358
+ const dumpName = values.output || basename(entryPoint, extname(entryPoint));
2359
+ const dumpDir = join(process.cwd(), "dump");
2360
+ if (values.json) {
2361
+ const rows = EXTRACT_ROWS_FN(plots, signalSchema);
2362
+ await mkdir(dumpDir, { recursive: true });
2363
+ await writeFile(join(dumpDir, `${dumpName}.json`), JSON.stringify(rows, null, 2), "utf-8");
2364
+ return;
2365
+ }
2366
+ if (values.jsonl) {
2367
+ const rows = EXTRACT_ROWS_FN(plots, signalSchema);
2368
+ await mkdir(dumpDir, { recursive: true });
2369
+ await writeFile(join(dumpDir, `${dumpName}.jsonl`), rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
2370
+ return;
2371
+ }
2372
+ if (values.markdown) {
2373
+ await mkdir(dumpDir, { recursive: true });
2374
+ await writeFile(join(dumpDir, `${dumpName}.md`), await toMarkdown(signalId, plots, signalSchema), "utf-8");
2375
+ return;
2376
+ }
2377
+ console.log(await toMarkdown(signalId, plots, signalSchema));
2378
+ };
2235
2379
  main();
2236
2380
 
2237
2381
  function setLogger(logger) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtest-kit/cli",
3
- "version": "5.9.1",
3
+ "version": "5.10.1",
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 {