@backtest-kit/cli 5.10.1 → 5.11.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 +92 -0
- package/build/index.cjs +93 -22
- package/build/index.mjs +95 -24
- package/package.json +13 -13
package/README.md
CHANGED
|
@@ -613,6 +613,98 @@ Print to stdout (no flag):
|
|
|
613
613
|
npx @backtest-kit/cli --pine ./math/impulse_trend_15m.pine
|
|
614
614
|
```
|
|
615
615
|
|
|
616
|
+
## 💾 Dumping Raw Candles
|
|
617
|
+
|
|
618
|
+
`@backtest-kit/cli` can fetch raw OHLCV candles from any registered exchange and save them to a file — no strategy file required.
|
|
619
|
+
|
|
620
|
+
### CLI Flags
|
|
621
|
+
|
|
622
|
+
| Flag | Type | Description |
|
|
623
|
+
|------|------|-------------|
|
|
624
|
+
| `--dump` | boolean | Enable candle dump mode |
|
|
625
|
+
| `--symbol` | string | Trading pair (default: `"BTCUSDT"`) |
|
|
626
|
+
| `--timeframe` | string | Candle interval (default: `"15m"`) |
|
|
627
|
+
| `--limit` | string | Number of candles to fetch (default: `250`) |
|
|
628
|
+
| `--when` | string | End date for candle window — ISO 8601 or Unix ms (default: now) |
|
|
629
|
+
| `--exchange` | string | Exchange name (default: first registered, falls back to CCXT Binance) |
|
|
630
|
+
| `--output` | string | Output file base name without extension (default: `{SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP}`) |
|
|
631
|
+
| `--json` | boolean | Write candles as a JSON array to `./dump/{output}.json` |
|
|
632
|
+
| `--jsonl` | boolean | Write candles as JSONL (one row per line) to `./dump/{output}.jsonl` |
|
|
633
|
+
|
|
634
|
+
The `dump/` directory is created in the current working directory (where the CLI is invoked from).
|
|
635
|
+
|
|
636
|
+
### Exchange via `dump.module`
|
|
637
|
+
|
|
638
|
+
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/dump.module.ts` file. The CLI loads it automatically before fetching candles.
|
|
639
|
+
|
|
640
|
+
The CLI looks for `modules/dump.module` in the current working directory
|
|
641
|
+
|
|
642
|
+
```
|
|
643
|
+
my-project/
|
|
644
|
+
├── modules/
|
|
645
|
+
│ └── dump.module.ts ← exchange registration
|
|
646
|
+
├── dump/ ← auto-created: candle output files
|
|
647
|
+
└── package.json
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Inside `dump.module.ts` call `addExchangeSchema` from `backtest-kit`:
|
|
651
|
+
|
|
652
|
+
```typescript
|
|
653
|
+
// modules/dump.module.ts
|
|
654
|
+
import { addExchangeSchema } from "backtest-kit";
|
|
655
|
+
import ccxt from "ccxt";
|
|
656
|
+
|
|
657
|
+
addExchangeSchema({
|
|
658
|
+
exchangeName: "my-exchange",
|
|
659
|
+
getCandles: async (symbol, interval, since, limit) => {
|
|
660
|
+
const exchange = new ccxt.bybit({ enableRateLimit: true });
|
|
661
|
+
const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
|
|
662
|
+
return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
|
|
663
|
+
timestamp, open, high, low, close, volume,
|
|
664
|
+
}));
|
|
665
|
+
},
|
|
666
|
+
formatPrice: (symbol, price) => price.toFixed(2),
|
|
667
|
+
formatQuantity: (symbol, quantity) => quantity.toFixed(8),
|
|
668
|
+
});
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
### Output
|
|
672
|
+
|
|
673
|
+
Each candle row contains OHLCV fields. Print to stdout:
|
|
674
|
+
|
|
675
|
+
```bash
|
|
676
|
+
npx @backtest-kit/cli --dump --symbol BTCUSDT --timeframe 15m --limit 100
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
Save to `./dump/BTCUSDT_100_15m_{timestamp}.jsonl`:
|
|
680
|
+
|
|
681
|
+
```bash
|
|
682
|
+
npx @backtest-kit/cli --dump --symbol BTCUSDT --timeframe 15m --limit 100 --jsonl
|
|
683
|
+
```
|
|
684
|
+
|
|
685
|
+
Fetch candles up to a specific date with `--when` and override the file name with `--output`:
|
|
686
|
+
|
|
687
|
+
```bash
|
|
688
|
+
npx @backtest-kit/cli --dump --symbol BTCUSDT --timeframe 15m --limit 500 \
|
|
689
|
+
--when "2026-02-28T00:00:00.000Z" \
|
|
690
|
+
--jsonl --output feb2026_btc
|
|
691
|
+
# → ./dump/feb2026_btc.jsonl
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
Or add it to `package.json`:
|
|
695
|
+
|
|
696
|
+
```json
|
|
697
|
+
{
|
|
698
|
+
"scripts": {
|
|
699
|
+
"dump": "npx @backtest-kit/cli --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl"
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
```bash
|
|
705
|
+
npx @backtest-kit/cli --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
|
|
706
|
+
```
|
|
707
|
+
|
|
616
708
|
## 🌍 Environment Variables
|
|
617
709
|
|
|
618
710
|
Create a `.env` file in your project root:
|
package/build/index.cjs
CHANGED
|
@@ -118,6 +118,7 @@ BacktestKit.setConfig({
|
|
|
118
118
|
BacktestKit.setConfig({
|
|
119
119
|
CC_ENABLE_DCA_EVERYWHERE: true,
|
|
120
120
|
CC_ENABLE_PPPL_EVERYWHERE: true,
|
|
121
|
+
CC_ENABLE_TRAILING_EVERYWHERE: true,
|
|
121
122
|
});
|
|
122
123
|
BacktestKit.setConfig({
|
|
123
124
|
CC_MAX_SIGNAL_GENERATION_SECONDS: 15 * 60,
|
|
@@ -445,6 +446,14 @@ var FrameName;
|
|
|
445
446
|
})(FrameName || (FrameName = {}));
|
|
446
447
|
var FrameName$1 = FrameName;
|
|
447
448
|
|
|
449
|
+
const ALLOWED_EXTENSIONS = [
|
|
450
|
+
`.cjs`,
|
|
451
|
+
`.mjs`,
|
|
452
|
+
`.ts`,
|
|
453
|
+
`.tsx`,
|
|
454
|
+
`.js`,
|
|
455
|
+
`.pine`,
|
|
456
|
+
];
|
|
448
457
|
const getArgs = functoolsKit.singleshot(() => {
|
|
449
458
|
const { values, positionals } = util.parseArgs({
|
|
450
459
|
args: process.argv,
|
|
@@ -507,6 +516,10 @@ const getArgs = functoolsKit.singleshot(() => {
|
|
|
507
516
|
type: "boolean",
|
|
508
517
|
default: false,
|
|
509
518
|
},
|
|
519
|
+
dump: {
|
|
520
|
+
type: "boolean",
|
|
521
|
+
default: false,
|
|
522
|
+
},
|
|
510
523
|
timeframe: {
|
|
511
524
|
type: "string",
|
|
512
525
|
default: "",
|
|
@@ -544,6 +557,11 @@ const getArgs = functoolsKit.singleshot(() => {
|
|
|
544
557
|
positionals,
|
|
545
558
|
};
|
|
546
559
|
});
|
|
560
|
+
const getPositional = functoolsKit.singleshot(() => {
|
|
561
|
+
const { positionals = [] } = getArgs();
|
|
562
|
+
const result = positionals.find((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
|
|
563
|
+
return result || null;
|
|
564
|
+
});
|
|
547
565
|
|
|
548
566
|
const ADD_FRAME_FN = (self) => {
|
|
549
567
|
self.loggerService.log("Adding February 2024 as a default frame schema");
|
|
@@ -729,11 +747,11 @@ class BacktestMainService {
|
|
|
729
747
|
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)))) {
|
|
730
748
|
return;
|
|
731
749
|
}
|
|
732
|
-
const { values
|
|
750
|
+
const { values } = getArgs();
|
|
733
751
|
if (!values.backtest) {
|
|
734
752
|
return;
|
|
735
753
|
}
|
|
736
|
-
const
|
|
754
|
+
const entryPoint = getPositional();
|
|
737
755
|
if (!entryPoint) {
|
|
738
756
|
throw new Error("Entry point is required");
|
|
739
757
|
}
|
|
@@ -808,11 +826,11 @@ class LiveMainService {
|
|
|
808
826
|
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)))) {
|
|
809
827
|
return;
|
|
810
828
|
}
|
|
811
|
-
const { values
|
|
829
|
+
const { values } = getArgs();
|
|
812
830
|
if (!values.live) {
|
|
813
831
|
return;
|
|
814
832
|
}
|
|
815
|
-
const
|
|
833
|
+
const entryPoint = getPositional();
|
|
816
834
|
if (!entryPoint) {
|
|
817
835
|
throw new Error("Entry point is required");
|
|
818
836
|
}
|
|
@@ -881,11 +899,11 @@ class PaperMainService {
|
|
|
881
899
|
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)))) {
|
|
882
900
|
return;
|
|
883
901
|
}
|
|
884
|
-
const { values
|
|
902
|
+
const { values } = getArgs();
|
|
885
903
|
if (!values.paper) {
|
|
886
904
|
return;
|
|
887
905
|
}
|
|
888
|
-
const
|
|
906
|
+
const entryPoint = getPositional();
|
|
889
907
|
if (!entryPoint) {
|
|
890
908
|
throw new Error("Entry point is required");
|
|
891
909
|
}
|
|
@@ -2202,7 +2220,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
|
2202
2220
|
const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
|
|
2203
2221
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
2204
2222
|
});
|
|
2205
|
-
const main$
|
|
2223
|
+
const main$6 = async () => {
|
|
2206
2224
|
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)))) {
|
|
2207
2225
|
return;
|
|
2208
2226
|
}
|
|
@@ -2213,7 +2231,7 @@ const main$5 = async () => {
|
|
|
2213
2231
|
await cli.backtestMainService.connect();
|
|
2214
2232
|
listenGracefulShutdown$4();
|
|
2215
2233
|
};
|
|
2216
|
-
main$
|
|
2234
|
+
main$6();
|
|
2217
2235
|
|
|
2218
2236
|
const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
2219
2237
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -2234,7 +2252,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
|
2234
2252
|
const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
|
|
2235
2253
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
2236
2254
|
});
|
|
2237
|
-
const main$
|
|
2255
|
+
const main$5 = async () => {
|
|
2238
2256
|
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)))) {
|
|
2239
2257
|
return;
|
|
2240
2258
|
}
|
|
@@ -2245,7 +2263,7 @@ const main$4 = async () => {
|
|
|
2245
2263
|
cli.paperMainService.connect();
|
|
2246
2264
|
listenGracefulShutdown$3();
|
|
2247
2265
|
};
|
|
2248
|
-
main$
|
|
2266
|
+
main$5();
|
|
2249
2267
|
|
|
2250
2268
|
const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
|
|
2251
2269
|
process.off("SIGINT", BEFORE_EXIT_FN$2);
|
|
@@ -2266,7 +2284,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
|
|
|
2266
2284
|
const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
|
|
2267
2285
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
2268
2286
|
});
|
|
2269
|
-
const main$
|
|
2287
|
+
const main$4 = async () => {
|
|
2270
2288
|
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)))) {
|
|
2271
2289
|
return;
|
|
2272
2290
|
}
|
|
@@ -2277,7 +2295,7 @@ const main$3 = async () => {
|
|
|
2277
2295
|
await cli.liveMainService.connect();
|
|
2278
2296
|
listenGracefulShutdown$2();
|
|
2279
2297
|
};
|
|
2280
|
-
main$
|
|
2298
|
+
main$4();
|
|
2281
2299
|
|
|
2282
2300
|
const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
2283
2301
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -2287,7 +2305,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
|
2287
2305
|
const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
|
|
2288
2306
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
2289
2307
|
});
|
|
2290
|
-
const main$
|
|
2308
|
+
const main$3 = async () => {
|
|
2291
2309
|
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)))) {
|
|
2292
2310
|
return;
|
|
2293
2311
|
}
|
|
@@ -2297,7 +2315,7 @@ const main$2 = async () => {
|
|
|
2297
2315
|
}
|
|
2298
2316
|
listenGracefulShutdown$1();
|
|
2299
2317
|
};
|
|
2300
|
-
main$
|
|
2318
|
+
main$3();
|
|
2301
2319
|
|
|
2302
2320
|
const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
2303
2321
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -2307,7 +2325,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
|
2307
2325
|
const listenGracefulShutdown = functoolsKit.singleshot(() => {
|
|
2308
2326
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
2309
2327
|
});
|
|
2310
|
-
const main$
|
|
2328
|
+
const main$2 = async () => {
|
|
2311
2329
|
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)))) {
|
|
2312
2330
|
return;
|
|
2313
2331
|
}
|
|
@@ -2317,7 +2335,7 @@ const main$1 = async () => {
|
|
|
2317
2335
|
}
|
|
2318
2336
|
listenGracefulShutdown();
|
|
2319
2337
|
};
|
|
2320
|
-
main$
|
|
2338
|
+
main$2();
|
|
2321
2339
|
|
|
2322
2340
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
2323
2341
|
const keys = Object.keys(schema);
|
|
@@ -2339,15 +2357,15 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
2339
2357
|
}
|
|
2340
2358
|
return rows;
|
|
2341
2359
|
};
|
|
2342
|
-
const main = async () => {
|
|
2360
|
+
const main$1 = async () => {
|
|
2343
2361
|
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
2362
|
return;
|
|
2345
2363
|
}
|
|
2346
|
-
const { values
|
|
2364
|
+
const { values } = getArgs();
|
|
2347
2365
|
if (!values.pine) {
|
|
2348
2366
|
return;
|
|
2349
2367
|
}
|
|
2350
|
-
const
|
|
2368
|
+
const entryPoint = getPositional();
|
|
2351
2369
|
if (!entryPoint) {
|
|
2352
2370
|
return;
|
|
2353
2371
|
}
|
|
@@ -2388,23 +2406,76 @@ const main = async () => {
|
|
|
2388
2406
|
const dumpDir = path.join(process.cwd(), "dump");
|
|
2389
2407
|
if (values.json) {
|
|
2390
2408
|
const rows = EXTRACT_ROWS_FN(plots, signalSchema);
|
|
2409
|
+
const filePath = path.resolve(dumpDir, `${dumpName}.json`);
|
|
2391
2410
|
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
2392
|
-
await fs$1.writeFile(
|
|
2411
|
+
await fs$1.writeFile(filePath, JSON.stringify(rows, null, 2), "utf-8");
|
|
2412
|
+
console.log(`Saved: ${filePath}`);
|
|
2393
2413
|
return;
|
|
2394
2414
|
}
|
|
2395
2415
|
if (values.jsonl) {
|
|
2396
2416
|
const rows = EXTRACT_ROWS_FN(plots, signalSchema);
|
|
2417
|
+
const filePath = path.resolve(dumpDir, `${dumpName}.jsonl`);
|
|
2397
2418
|
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
2398
|
-
await fs$1.writeFile(
|
|
2419
|
+
await fs$1.writeFile(filePath, rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
|
|
2420
|
+
console.log(`Saved: ${filePath}`);
|
|
2399
2421
|
return;
|
|
2400
2422
|
}
|
|
2401
2423
|
if (values.markdown) {
|
|
2424
|
+
const filePath = path.resolve(dumpDir, `${dumpName}.md`);
|
|
2402
2425
|
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
2403
|
-
await fs$1.writeFile(
|
|
2426
|
+
await fs$1.writeFile(filePath, await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema), "utf-8");
|
|
2427
|
+
console.log(`Saved: ${filePath}`);
|
|
2404
2428
|
return;
|
|
2405
2429
|
}
|
|
2406
2430
|
console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
|
|
2407
2431
|
};
|
|
2432
|
+
main$1();
|
|
2433
|
+
|
|
2434
|
+
const main = async () => {
|
|
2435
|
+
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)))) {
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
const { values } = getArgs();
|
|
2439
|
+
if (!values.dump) {
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
await cli.moduleConnectionService.loadModule("./dump.module");
|
|
2443
|
+
{
|
|
2444
|
+
await cli.exchangeSchemaService.addSchema();
|
|
2445
|
+
await cli.symbolSchemaService.addSchema();
|
|
2446
|
+
}
|
|
2447
|
+
const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
|
|
2448
|
+
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
2449
|
+
const symbol = values.symbol || "BTCUSDT";
|
|
2450
|
+
const timeframe = values.timeframe || "15m";
|
|
2451
|
+
const limitStr = values.limit || "250";
|
|
2452
|
+
const limitNum = parseInt(limitStr);
|
|
2453
|
+
const limit = isNaN(limitNum) ? 250 : limitNum;
|
|
2454
|
+
const whenStr = values.when || Date.now().toString();
|
|
2455
|
+
const whenStamp = Date.parse(whenStr);
|
|
2456
|
+
const when = isNaN(whenStamp) ? new Date() : new Date(whenStamp);
|
|
2457
|
+
const timestamp = BacktestKit.alignToInterval(when, timeframe).getTime();
|
|
2458
|
+
const candles = await BacktestKit.Exchange.getRawCandles(symbol, timeframe, {
|
|
2459
|
+
exchangeName,
|
|
2460
|
+
}, limit, undefined, timestamp);
|
|
2461
|
+
const dumpName = values.output || `${symbol}_${limit}_${timeframe}_${timestamp}`;
|
|
2462
|
+
const dumpDir = path.join(process.cwd(), "dump");
|
|
2463
|
+
if (values.json) {
|
|
2464
|
+
const filePath = path.resolve(dumpDir, `${dumpName}.json`);
|
|
2465
|
+
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
2466
|
+
await fs$1.writeFile(filePath, JSON.stringify(candles, null, 2), "utf-8");
|
|
2467
|
+
console.log(`Saved: ${filePath}`);
|
|
2468
|
+
return;
|
|
2469
|
+
}
|
|
2470
|
+
if (values.jsonl) {
|
|
2471
|
+
const filePath = path.resolve(dumpDir, `${dumpName}.jsonl`);
|
|
2472
|
+
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
2473
|
+
await fs$1.writeFile(filePath, candles.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
|
|
2474
|
+
console.log(`Saved: ${filePath}`);
|
|
2475
|
+
return;
|
|
2476
|
+
}
|
|
2477
|
+
console.log(JSON.stringify(candles, null, 2));
|
|
2478
|
+
};
|
|
2408
2479
|
main();
|
|
2409
2480
|
|
|
2410
2481
|
function setLogger(logger) {
|
package/build/index.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as BacktestKit from 'backtest-kit';
|
|
3
|
-
import { Storage, Notification, Markdown, Report, Dump, Memory, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, setConfig, Log, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenSync } from 'backtest-kit';
|
|
3
|
+
import { Storage, Notification, Markdown, Report, Dump, Memory, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, setConfig, Log, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenSync, alignToInterval, Exchange } from 'backtest-kit';
|
|
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, { basename, extname, join } from 'path';
|
|
7
|
+
import path, { basename, extname, join, resolve } from 'path';
|
|
8
8
|
import fs$1, { access, readFile, mkdir, writeFile } from 'fs/promises';
|
|
9
9
|
import dotenv from 'dotenv';
|
|
10
10
|
import { createActivator } from 'di-kit';
|
|
@@ -93,6 +93,7 @@ setConfig({
|
|
|
93
93
|
setConfig({
|
|
94
94
|
CC_ENABLE_DCA_EVERYWHERE: true,
|
|
95
95
|
CC_ENABLE_PPPL_EVERYWHERE: true,
|
|
96
|
+
CC_ENABLE_TRAILING_EVERYWHERE: true,
|
|
96
97
|
});
|
|
97
98
|
setConfig({
|
|
98
99
|
CC_MAX_SIGNAL_GENERATION_SECONDS: 15 * 60,
|
|
@@ -420,6 +421,14 @@ var FrameName;
|
|
|
420
421
|
})(FrameName || (FrameName = {}));
|
|
421
422
|
var FrameName$1 = FrameName;
|
|
422
423
|
|
|
424
|
+
const ALLOWED_EXTENSIONS = [
|
|
425
|
+
`.cjs`,
|
|
426
|
+
`.mjs`,
|
|
427
|
+
`.ts`,
|
|
428
|
+
`.tsx`,
|
|
429
|
+
`.js`,
|
|
430
|
+
`.pine`,
|
|
431
|
+
];
|
|
423
432
|
const getArgs = singleshot(() => {
|
|
424
433
|
const { values, positionals } = parseArgs({
|
|
425
434
|
args: process.argv,
|
|
@@ -482,6 +491,10 @@ const getArgs = singleshot(() => {
|
|
|
482
491
|
type: "boolean",
|
|
483
492
|
default: false,
|
|
484
493
|
},
|
|
494
|
+
dump: {
|
|
495
|
+
type: "boolean",
|
|
496
|
+
default: false,
|
|
497
|
+
},
|
|
485
498
|
timeframe: {
|
|
486
499
|
type: "string",
|
|
487
500
|
default: "",
|
|
@@ -519,6 +532,11 @@ const getArgs = singleshot(() => {
|
|
|
519
532
|
positionals,
|
|
520
533
|
};
|
|
521
534
|
});
|
|
535
|
+
const getPositional = singleshot(() => {
|
|
536
|
+
const { positionals = [] } = getArgs();
|
|
537
|
+
const result = positionals.find((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
|
|
538
|
+
return result || null;
|
|
539
|
+
});
|
|
522
540
|
|
|
523
541
|
const ADD_FRAME_FN = (self) => {
|
|
524
542
|
self.loggerService.log("Adding February 2024 as a default frame schema");
|
|
@@ -704,11 +722,11 @@ class BacktestMainService {
|
|
|
704
722
|
if (!getEntry(import.meta.url)) {
|
|
705
723
|
return;
|
|
706
724
|
}
|
|
707
|
-
const { values
|
|
725
|
+
const { values } = getArgs();
|
|
708
726
|
if (!values.backtest) {
|
|
709
727
|
return;
|
|
710
728
|
}
|
|
711
|
-
const
|
|
729
|
+
const entryPoint = getPositional();
|
|
712
730
|
if (!entryPoint) {
|
|
713
731
|
throw new Error("Entry point is required");
|
|
714
732
|
}
|
|
@@ -783,11 +801,11 @@ class LiveMainService {
|
|
|
783
801
|
if (!getEntry(import.meta.url)) {
|
|
784
802
|
return;
|
|
785
803
|
}
|
|
786
|
-
const { values
|
|
804
|
+
const { values } = getArgs();
|
|
787
805
|
if (!values.live) {
|
|
788
806
|
return;
|
|
789
807
|
}
|
|
790
|
-
const
|
|
808
|
+
const entryPoint = getPositional();
|
|
791
809
|
if (!entryPoint) {
|
|
792
810
|
throw new Error("Entry point is required");
|
|
793
811
|
}
|
|
@@ -856,11 +874,11 @@ class PaperMainService {
|
|
|
856
874
|
if (!getEntry(import.meta.url)) {
|
|
857
875
|
return;
|
|
858
876
|
}
|
|
859
|
-
const { values
|
|
877
|
+
const { values } = getArgs();
|
|
860
878
|
if (!values.paper) {
|
|
861
879
|
return;
|
|
862
880
|
}
|
|
863
|
-
const
|
|
881
|
+
const entryPoint = getPositional();
|
|
864
882
|
if (!entryPoint) {
|
|
865
883
|
throw new Error("Entry point is required");
|
|
866
884
|
}
|
|
@@ -2173,7 +2191,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
|
|
|
2173
2191
|
const listenGracefulShutdown$4 = singleshot(() => {
|
|
2174
2192
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
2175
2193
|
});
|
|
2176
|
-
const main$
|
|
2194
|
+
const main$6 = async () => {
|
|
2177
2195
|
if (!getEntry(import.meta.url)) {
|
|
2178
2196
|
return;
|
|
2179
2197
|
}
|
|
@@ -2184,7 +2202,7 @@ const main$5 = async () => {
|
|
|
2184
2202
|
await cli.backtestMainService.connect();
|
|
2185
2203
|
listenGracefulShutdown$4();
|
|
2186
2204
|
};
|
|
2187
|
-
main$
|
|
2205
|
+
main$6();
|
|
2188
2206
|
|
|
2189
2207
|
const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
2190
2208
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -2205,7 +2223,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
|
2205
2223
|
const listenGracefulShutdown$3 = singleshot(() => {
|
|
2206
2224
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
2207
2225
|
});
|
|
2208
|
-
const main$
|
|
2226
|
+
const main$5 = async () => {
|
|
2209
2227
|
if (!getEntry(import.meta.url)) {
|
|
2210
2228
|
return;
|
|
2211
2229
|
}
|
|
@@ -2216,7 +2234,7 @@ const main$4 = async () => {
|
|
|
2216
2234
|
cli.paperMainService.connect();
|
|
2217
2235
|
listenGracefulShutdown$3();
|
|
2218
2236
|
};
|
|
2219
|
-
main$
|
|
2237
|
+
main$5();
|
|
2220
2238
|
|
|
2221
2239
|
const BEFORE_EXIT_FN$2 = singleshot(async () => {
|
|
2222
2240
|
process.off("SIGINT", BEFORE_EXIT_FN$2);
|
|
@@ -2237,7 +2255,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
|
|
|
2237
2255
|
const listenGracefulShutdown$2 = singleshot(() => {
|
|
2238
2256
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
2239
2257
|
});
|
|
2240
|
-
const main$
|
|
2258
|
+
const main$4 = async () => {
|
|
2241
2259
|
if (!getEntry(import.meta.url)) {
|
|
2242
2260
|
return;
|
|
2243
2261
|
}
|
|
@@ -2248,7 +2266,7 @@ const main$3 = async () => {
|
|
|
2248
2266
|
await cli.liveMainService.connect();
|
|
2249
2267
|
listenGracefulShutdown$2();
|
|
2250
2268
|
};
|
|
2251
|
-
main$
|
|
2269
|
+
main$4();
|
|
2252
2270
|
|
|
2253
2271
|
const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
2254
2272
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -2258,7 +2276,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
|
2258
2276
|
const listenGracefulShutdown$1 = singleshot(() => {
|
|
2259
2277
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
2260
2278
|
});
|
|
2261
|
-
const main$
|
|
2279
|
+
const main$3 = async () => {
|
|
2262
2280
|
if (!getEntry(import.meta.url)) {
|
|
2263
2281
|
return;
|
|
2264
2282
|
}
|
|
@@ -2268,7 +2286,7 @@ const main$2 = async () => {
|
|
|
2268
2286
|
}
|
|
2269
2287
|
listenGracefulShutdown$1();
|
|
2270
2288
|
};
|
|
2271
|
-
main$
|
|
2289
|
+
main$3();
|
|
2272
2290
|
|
|
2273
2291
|
const BEFORE_EXIT_FN = singleshot(async () => {
|
|
2274
2292
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -2278,7 +2296,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
|
|
|
2278
2296
|
const listenGracefulShutdown = singleshot(() => {
|
|
2279
2297
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
2280
2298
|
});
|
|
2281
|
-
const main$
|
|
2299
|
+
const main$2 = async () => {
|
|
2282
2300
|
if (!getEntry(import.meta.url)) {
|
|
2283
2301
|
return;
|
|
2284
2302
|
}
|
|
@@ -2288,7 +2306,7 @@ const main$1 = async () => {
|
|
|
2288
2306
|
}
|
|
2289
2307
|
listenGracefulShutdown();
|
|
2290
2308
|
};
|
|
2291
|
-
main$
|
|
2309
|
+
main$2();
|
|
2292
2310
|
|
|
2293
2311
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
2294
2312
|
const keys = Object.keys(schema);
|
|
@@ -2310,15 +2328,15 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
2310
2328
|
}
|
|
2311
2329
|
return rows;
|
|
2312
2330
|
};
|
|
2313
|
-
const main = async () => {
|
|
2331
|
+
const main$1 = async () => {
|
|
2314
2332
|
if (!getEntry(import.meta.url)) {
|
|
2315
2333
|
return;
|
|
2316
2334
|
}
|
|
2317
|
-
const { values
|
|
2335
|
+
const { values } = getArgs();
|
|
2318
2336
|
if (!values.pine) {
|
|
2319
2337
|
return;
|
|
2320
2338
|
}
|
|
2321
|
-
const
|
|
2339
|
+
const entryPoint = getPositional();
|
|
2322
2340
|
if (!entryPoint) {
|
|
2323
2341
|
return;
|
|
2324
2342
|
}
|
|
@@ -2359,23 +2377,76 @@ const main = async () => {
|
|
|
2359
2377
|
const dumpDir = join(process.cwd(), "dump");
|
|
2360
2378
|
if (values.json) {
|
|
2361
2379
|
const rows = EXTRACT_ROWS_FN(plots, signalSchema);
|
|
2380
|
+
const filePath = resolve(dumpDir, `${dumpName}.json`);
|
|
2362
2381
|
await mkdir(dumpDir, { recursive: true });
|
|
2363
|
-
await writeFile(
|
|
2382
|
+
await writeFile(filePath, JSON.stringify(rows, null, 2), "utf-8");
|
|
2383
|
+
console.log(`Saved: ${filePath}`);
|
|
2364
2384
|
return;
|
|
2365
2385
|
}
|
|
2366
2386
|
if (values.jsonl) {
|
|
2367
2387
|
const rows = EXTRACT_ROWS_FN(plots, signalSchema);
|
|
2388
|
+
const filePath = resolve(dumpDir, `${dumpName}.jsonl`);
|
|
2368
2389
|
await mkdir(dumpDir, { recursive: true });
|
|
2369
|
-
await writeFile(
|
|
2390
|
+
await writeFile(filePath, rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
|
|
2391
|
+
console.log(`Saved: ${filePath}`);
|
|
2370
2392
|
return;
|
|
2371
2393
|
}
|
|
2372
2394
|
if (values.markdown) {
|
|
2395
|
+
const filePath = resolve(dumpDir, `${dumpName}.md`);
|
|
2373
2396
|
await mkdir(dumpDir, { recursive: true });
|
|
2374
|
-
await writeFile(
|
|
2397
|
+
await writeFile(filePath, await toMarkdown(signalId, plots, signalSchema), "utf-8");
|
|
2398
|
+
console.log(`Saved: ${filePath}`);
|
|
2375
2399
|
return;
|
|
2376
2400
|
}
|
|
2377
2401
|
console.log(await toMarkdown(signalId, plots, signalSchema));
|
|
2378
2402
|
};
|
|
2403
|
+
main$1();
|
|
2404
|
+
|
|
2405
|
+
const main = async () => {
|
|
2406
|
+
if (!getEntry(import.meta.url)) {
|
|
2407
|
+
return;
|
|
2408
|
+
}
|
|
2409
|
+
const { values } = getArgs();
|
|
2410
|
+
if (!values.dump) {
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
await cli.moduleConnectionService.loadModule("./dump.module");
|
|
2414
|
+
{
|
|
2415
|
+
await cli.exchangeSchemaService.addSchema();
|
|
2416
|
+
await cli.symbolSchemaService.addSchema();
|
|
2417
|
+
}
|
|
2418
|
+
const [defaultExchangeName = null] = await listExchangeSchema();
|
|
2419
|
+
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
2420
|
+
const symbol = values.symbol || "BTCUSDT";
|
|
2421
|
+
const timeframe = values.timeframe || "15m";
|
|
2422
|
+
const limitStr = values.limit || "250";
|
|
2423
|
+
const limitNum = parseInt(limitStr);
|
|
2424
|
+
const limit = isNaN(limitNum) ? 250 : limitNum;
|
|
2425
|
+
const whenStr = values.when || Date.now().toString();
|
|
2426
|
+
const whenStamp = Date.parse(whenStr);
|
|
2427
|
+
const when = isNaN(whenStamp) ? new Date() : new Date(whenStamp);
|
|
2428
|
+
const timestamp = alignToInterval(when, timeframe).getTime();
|
|
2429
|
+
const candles = await Exchange.getRawCandles(symbol, timeframe, {
|
|
2430
|
+
exchangeName,
|
|
2431
|
+
}, limit, undefined, timestamp);
|
|
2432
|
+
const dumpName = values.output || `${symbol}_${limit}_${timeframe}_${timestamp}`;
|
|
2433
|
+
const dumpDir = join(process.cwd(), "dump");
|
|
2434
|
+
if (values.json) {
|
|
2435
|
+
const filePath = resolve(dumpDir, `${dumpName}.json`);
|
|
2436
|
+
await mkdir(dumpDir, { recursive: true });
|
|
2437
|
+
await writeFile(filePath, JSON.stringify(candles, null, 2), "utf-8");
|
|
2438
|
+
console.log(`Saved: ${filePath}`);
|
|
2439
|
+
return;
|
|
2440
|
+
}
|
|
2441
|
+
if (values.jsonl) {
|
|
2442
|
+
const filePath = resolve(dumpDir, `${dumpName}.jsonl`);
|
|
2443
|
+
await mkdir(dumpDir, { recursive: true });
|
|
2444
|
+
await writeFile(filePath, candles.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
|
|
2445
|
+
console.log(`Saved: ${filePath}`);
|
|
2446
|
+
return;
|
|
2447
|
+
}
|
|
2448
|
+
console.log(JSON.stringify(candles, null, 2));
|
|
2449
|
+
};
|
|
2379
2450
|
main();
|
|
2380
2451
|
|
|
2381
2452
|
function setLogger(logger) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backtest-kit/cli",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.11.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",
|
|
@@ -61,11 +61,11 @@
|
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@babel/plugin-transform-modules-umd": "7.27.1",
|
|
63
63
|
"@babel/standalone": "7.29.1",
|
|
64
|
-
"@backtest-kit/ui": "5.
|
|
65
|
-
"@backtest-kit/graph": "5.
|
|
66
|
-
"@backtest-kit/ollama": "5.
|
|
67
|
-
"@backtest-kit/pinets": "5.
|
|
68
|
-
"@backtest-kit/signals": "5.
|
|
64
|
+
"@backtest-kit/ui": "5.11.0",
|
|
65
|
+
"@backtest-kit/graph": "5.11.0",
|
|
66
|
+
"@backtest-kit/ollama": "5.11.0",
|
|
67
|
+
"@backtest-kit/pinets": "5.11.0",
|
|
68
|
+
"@backtest-kit/signals": "5.11.0",
|
|
69
69
|
"@rollup/plugin-replace": "6.0.3",
|
|
70
70
|
"@rollup/plugin-typescript": "11.1.6",
|
|
71
71
|
"@types/image-size": "0.7.0",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"@types/mustache": "4.2.6",
|
|
74
74
|
"@types/node": "22.9.0",
|
|
75
75
|
"@types/stack-trace": "0.0.33",
|
|
76
|
-
"backtest-kit": "5.
|
|
76
|
+
"backtest-kit": "5.11.0",
|
|
77
77
|
"glob": "11.0.1",
|
|
78
78
|
"markdown-it": "14.1.1",
|
|
79
79
|
"rimraf": "6.0.1",
|
|
@@ -88,12 +88,12 @@
|
|
|
88
88
|
"peerDependencies": {
|
|
89
89
|
"@babel/plugin-transform-modules-umd": "^7.27.1",
|
|
90
90
|
"@babel/standalone": "^7.29.1",
|
|
91
|
-
"@backtest-kit/ui": "^5.
|
|
92
|
-
"@backtest-kit/graph": "^5.
|
|
93
|
-
"@backtest-kit/ollama": "^5.
|
|
94
|
-
"@backtest-kit/pinets": "^5.
|
|
95
|
-
"@backtest-kit/signals": "^5.
|
|
96
|
-
"backtest-kit": "^5.
|
|
91
|
+
"@backtest-kit/ui": "^5.11.0",
|
|
92
|
+
"@backtest-kit/graph": "^5.11.0",
|
|
93
|
+
"@backtest-kit/ollama": "^5.11.0",
|
|
94
|
+
"@backtest-kit/pinets": "^5.11.0",
|
|
95
|
+
"@backtest-kit/signals": "^5.11.0",
|
|
96
|
+
"backtest-kit": "^5.11.0",
|
|
97
97
|
"markdown-it": "^14.1.1",
|
|
98
98
|
"typescript": "^5.0.0"
|
|
99
99
|
},
|