@backtest-kit/cli 0.0.3 → 0.0.5
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 +27 -25
- package/build/index.cjs +92 -18
- package/build/index.mjs +93 -19
- package/package.json +5 -2
- package/types.d.ts +2 -1
package/README.md
CHANGED
|
@@ -30,13 +30,13 @@ Point the CLI at your strategy file, choose a mode, and it handles exchange conn
|
|
|
30
30
|
|
|
31
31
|
`@backtest-kit/cli` wraps the `backtest-kit` engine and resolves all scaffolding automatically:
|
|
32
32
|
|
|
33
|
-
| Mode |
|
|
34
|
-
|
|
35
|
-
| **Backtest** | `--backtest`
|
|
36
|
-
| **Paper** | `--paper`
|
|
37
|
-
| **Live** | `--live`
|
|
38
|
-
| **UI Dashboard** | `--ui`
|
|
39
|
-
| **Telegram** | `--telegram`
|
|
33
|
+
| Mode | Command Line Args | Description |
|
|
34
|
+
|------------------|----------------------------|----------------------------------------------|
|
|
35
|
+
| **Backtest** | `--backtest` | Run strategy on historical candle data |
|
|
36
|
+
| **Paper** | `--paper` | Live prices, no real orders |
|
|
37
|
+
| **Live** | `--live` | Real trades via exchange API |
|
|
38
|
+
| **UI Dashboard** | `--ui` | Web dashboard at `http://localhost:60050` |
|
|
39
|
+
| **Telegram** | `--telegram` | Trade notifications with price charts |
|
|
40
40
|
|
|
41
41
|
## 🚀 Installation
|
|
42
42
|
|
|
@@ -129,19 +129,20 @@ npm start -- --symbol BTCUSDT --ui
|
|
|
129
129
|
|
|
130
130
|
## 🎛️ CLI Flags
|
|
131
131
|
|
|
132
|
-
|
|
|
133
|
-
|
|
134
|
-
| `--backtest`
|
|
135
|
-
| `--paper`
|
|
136
|
-
| `--live`
|
|
137
|
-
| `--ui`
|
|
138
|
-
| `--telegram`
|
|
139
|
-
| `--verbose`
|
|
140
|
-
| `--
|
|
141
|
-
| `--
|
|
142
|
-
| `--
|
|
143
|
-
| `--
|
|
144
|
-
| `--
|
|
132
|
+
| Command Line Args | Type | Description |
|
|
133
|
+
|---------------------------|---------|--------------------------------------------------------------------|
|
|
134
|
+
| `--backtest` | boolean | Run historical backtest (default: `false`) |
|
|
135
|
+
| `--paper` | boolean | Paper trading (live prices, no orders) (default: `false`) |
|
|
136
|
+
| `--live` | boolean | Run live trading (default: `false`) |
|
|
137
|
+
| `--ui` | boolean | Start web UI dashboard (default: `false`) |
|
|
138
|
+
| `--telegram` | boolean | Enable Telegram notifications (default: `false`) |
|
|
139
|
+
| `--verbose` | boolean | Log each candle fetch (default: `false`) |
|
|
140
|
+
| `--noCache` | boolean | Skip candle cache warming before backtest (default: `false`) |
|
|
141
|
+
| `--symbol` | string | Trading pair (default: `"BTCUSDT"`) |
|
|
142
|
+
| `--strategy` | string | Strategy name (default: first registered) |
|
|
143
|
+
| `--exchange` | string | Exchange name (default: first registered) |
|
|
144
|
+
| `--frame` | string | Backtest frame name (default: first registered) |
|
|
145
|
+
| `--cacheInterval` | string | Intervals to pre-cache before backtest (default: `"1m, 15m, 30m, 4h"`) |
|
|
145
146
|
|
|
146
147
|
**Positional argument (required):** path to your strategy entry point file (set once in `package.json` scripts).
|
|
147
148
|
|
|
@@ -162,7 +163,7 @@ Runs the strategy against historical candle data using a registered `FrameSchema
|
|
|
162
163
|
```json
|
|
163
164
|
{
|
|
164
165
|
"scripts": {
|
|
165
|
-
"backtest": "@backtest-kit/cli --backtest --symbol ETHUSDT --strategy my-strategy --exchange binance --frame feb-2024 --
|
|
166
|
+
"backtest": "@backtest-kit/cli --backtest --symbol ETHUSDT --strategy my-strategy --exchange binance --frame feb-2024 --cacheInterval \"1m, 15m, 1h, 4h\" ./src/index.mjs"
|
|
166
167
|
}
|
|
167
168
|
}
|
|
168
169
|
```
|
|
@@ -171,7 +172,7 @@ Runs the strategy against historical candle data using a registered `FrameSchema
|
|
|
171
172
|
npm run backtest
|
|
172
173
|
```
|
|
173
174
|
|
|
174
|
-
Before running, the CLI warms the candle cache for every interval in `--
|
|
175
|
+
Before running, the CLI warms the candle cache for every interval in `--cacheInterval`. On the next run, cached data is used directly — no API calls needed. Pass `--noCache` to skip this step entirely.
|
|
175
176
|
|
|
176
177
|
### Paper Trading
|
|
177
178
|
|
|
@@ -389,7 +390,7 @@ When your strategy module does not register an exchange, frame, or strategy name
|
|
|
389
390
|
| **Exchange** | CCXT Binance (`default_exchange`) | `Warning: The default exchange schema is set to CCXT Binance...` |
|
|
390
391
|
| **Frame** | February 2024 (`default_frame`) | `Warning: The default frame schema is set to February 2024...` |
|
|
391
392
|
| **Symbol** | `BTCUSDT` | — |
|
|
392
|
-
| **Cache intervals** | `1m, 15m, 30m, 4h` |
|
|
393
|
+
| **Cache intervals** | `1m, 15m, 30m, 4h` | Used if `--cacheInterval` not provided; skip entirely with `--noCache` |
|
|
393
394
|
|
|
394
395
|
> **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.
|
|
395
396
|
|
|
@@ -424,7 +425,8 @@ await run(mode, args);
|
|
|
424
425
|
| `strategy` | `string` | Strategy name (default: first registered) |
|
|
425
426
|
| `exchange` | `string` | Exchange name (default: first registered) |
|
|
426
427
|
| `frame` | `string` | Frame name (default: first registered) |
|
|
427
|
-
| `
|
|
428
|
+
| `cacheInterval` | `CandleInterval[]` | Intervals to pre-cache (default: `["1m","15m","30m","1h","4h"]`) |
|
|
429
|
+
| `noCache` | `boolean` | Skip candle cache warming (default: `false`) |
|
|
428
430
|
| `verbose` | `boolean` | Log each candle fetch (default: `false`) |
|
|
429
431
|
|
|
430
432
|
**Paper** and **Live** (`mode: "paper"` / `mode: "live"`):
|
|
@@ -448,7 +450,7 @@ await run('backtest', {
|
|
|
448
450
|
entryPoint: './src/index.mjs',
|
|
449
451
|
symbol: 'ETHUSDT',
|
|
450
452
|
frame: 'feb-2024',
|
|
451
|
-
|
|
453
|
+
cacheInterval: ['1m', '15m', '1h'],
|
|
452
454
|
verbose: true,
|
|
453
455
|
});
|
|
454
456
|
```
|
package/build/index.cjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
1
2
|
'use strict';
|
|
2
3
|
|
|
3
4
|
var backtestKit = require('backtest-kit');
|
|
@@ -5,6 +6,7 @@ var functoolsKit = require('functools-kit');
|
|
|
5
6
|
var fs = require('fs');
|
|
6
7
|
var stackTrace = require('stack-trace');
|
|
7
8
|
var url = require('url');
|
|
9
|
+
var module$1 = require('module');
|
|
8
10
|
var path = require('path');
|
|
9
11
|
var fs$1 = require('fs/promises');
|
|
10
12
|
var dotenv = require('dotenv');
|
|
@@ -21,7 +23,6 @@ var MarkdownIt = require('markdown-it');
|
|
|
21
23
|
var sanitizeHtml = require('sanitize-html');
|
|
22
24
|
var jsdom = require('jsdom');
|
|
23
25
|
var Mustache = require('mustache');
|
|
24
|
-
var module$1 = require('module');
|
|
25
26
|
|
|
26
27
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
27
28
|
function _interopNamespaceDefault(e) {
|
|
@@ -83,7 +84,7 @@ var stackTrace__namespace = /*#__PURE__*/_interopNamespaceDefault(stackTrace);
|
|
|
83
84
|
}
|
|
84
85
|
{
|
|
85
86
|
backtestKit.StorageLive.usePersist();
|
|
86
|
-
backtestKit.StorageBacktest.
|
|
87
|
+
backtestKit.StorageBacktest.useMemory();
|
|
87
88
|
}
|
|
88
89
|
{
|
|
89
90
|
backtestKit.NotificationLive.useDummy();
|
|
@@ -231,6 +232,24 @@ const entrySubject = new functoolsKit.BehaviorSubject();
|
|
|
231
232
|
|
|
232
233
|
const __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
233
234
|
const __dirname$1 = path.dirname(__filename$1);
|
|
235
|
+
const require$2 = module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
|
|
236
|
+
const REQUIRE_ENTRY_FACTORY = (filePath) => {
|
|
237
|
+
try {
|
|
238
|
+
require$2(filePath);
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
const IMPORT_ENTRY_FACTORY = async (filePath) => {
|
|
246
|
+
await import(url.pathToFileURL(filePath).href);
|
|
247
|
+
};
|
|
248
|
+
const LOAD_ENTRY_FN = async (filePath) => {
|
|
249
|
+
if (!REQUIRE_ENTRY_FACTORY(filePath)) {
|
|
250
|
+
await IMPORT_ENTRY_FACTORY(filePath);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
234
253
|
let _is_launched = false;
|
|
235
254
|
class ResolveService {
|
|
236
255
|
constructor() {
|
|
@@ -249,9 +268,9 @@ class ResolveService {
|
|
|
249
268
|
{
|
|
250
269
|
const cwd = process.cwd();
|
|
251
270
|
process.chdir(moduleRoot);
|
|
252
|
-
dotenv.config({ path: path.join(cwd, '.env') });
|
|
253
|
-
dotenv.config({ path: path.join(moduleRoot, '.env'), override: true });
|
|
254
|
-
await
|
|
271
|
+
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
272
|
+
dotenv.config({ path: path.join(moduleRoot, '.env'), override: true, quiet: true });
|
|
273
|
+
await LOAD_ENTRY_FN(absolutePath);
|
|
255
274
|
await entrySubject.next(absolutePath);
|
|
256
275
|
}
|
|
257
276
|
_is_launched = true;
|
|
@@ -394,7 +413,11 @@ const getArgs = functoolsKit.singleshot(() => {
|
|
|
394
413
|
type: "boolean",
|
|
395
414
|
default: false,
|
|
396
415
|
},
|
|
397
|
-
|
|
416
|
+
noCache: {
|
|
417
|
+
type: "boolean",
|
|
418
|
+
default: false,
|
|
419
|
+
},
|
|
420
|
+
cacheInterval: {
|
|
398
421
|
type: "string",
|
|
399
422
|
default: "1m, 15m, 30m, 4h",
|
|
400
423
|
},
|
|
@@ -458,17 +481,64 @@ const notifyFinish = functoolsKit.singleshot(() => {
|
|
|
458
481
|
});
|
|
459
482
|
|
|
460
483
|
const getEntry = (metaUrl) => {
|
|
461
|
-
|
|
484
|
+
const metaPath = url.fileURLToPath(metaUrl);
|
|
485
|
+
return path.resolve(process.argv[1]) === path.resolve(metaPath);
|
|
462
486
|
};
|
|
463
487
|
|
|
488
|
+
const notifyVerbose = functoolsKit.singleshot(() => {
|
|
489
|
+
console.log("Using verbose logging...");
|
|
490
|
+
backtestKit.listenSignal((event) => {
|
|
491
|
+
if (event.action === "scheduled") {
|
|
492
|
+
console.log(`[POSITION SCHEDULED] ${event.symbol}`);
|
|
493
|
+
console.log(` Strategy: ${event.strategyName}`);
|
|
494
|
+
console.log(` Current Price: ${event.currentPrice}`);
|
|
495
|
+
console.log(` Entry Price: ${event.signal.priceOpen}`);
|
|
496
|
+
console.log(` Signal ID: ${event.signal.id}`);
|
|
497
|
+
console.log(` Direction: ${event.signal.position}`);
|
|
498
|
+
console.log(` Stop Loss: ${event.signal.priceStopLoss}`);
|
|
499
|
+
console.log(` Take Profit: ${event.signal.priceTakeProfit}`);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (event.action === "opened") {
|
|
503
|
+
console.log(`[POSITION OPENED] ${event.symbol}`);
|
|
504
|
+
console.log(` Strategy: ${event.strategyName}`);
|
|
505
|
+
console.log(` Entry Price: ${event.currentPrice}`);
|
|
506
|
+
console.log(` Signal ID: ${event.signal.id}`);
|
|
507
|
+
console.log(` Direction: ${event.signal.position}`);
|
|
508
|
+
console.log(` Stop Loss: ${event.signal.priceStopLoss}`);
|
|
509
|
+
console.log(` Take Profit: ${event.signal.priceTakeProfit}`);
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (event.action === "closed") {
|
|
513
|
+
console.log(`[POSITION CLOSED] ${event.symbol}`);
|
|
514
|
+
console.log(` Strategy: ${event.strategyName}`);
|
|
515
|
+
console.log(` Entry Price (adj): ${event.pnl.priceOpen}`);
|
|
516
|
+
console.log(` Exit Price (adj): ${event.pnl.priceClose}`);
|
|
517
|
+
console.log(` Signal ID: ${event.signal.id}`);
|
|
518
|
+
console.log(` Close Reason: ${event.closeReason}`);
|
|
519
|
+
console.log(` PnL: ${event.pnl.pnlPercentage.toFixed(2)}%`);
|
|
520
|
+
console.log(` Win: ${event.pnl.pnlPercentage > 0 ? "YES" : "NO"}`);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (event.action === "cancelled") {
|
|
524
|
+
console.log(`[POSITION CANCELLED] ${event.symbol}`);
|
|
525
|
+
console.log(` Strategy: ${event.strategyName}`);
|
|
526
|
+
console.log(` Signal ID: ${event.signal.id}`);
|
|
527
|
+
console.log(` Current Price: ${event.currentPrice}`);
|
|
528
|
+
console.log(` Cancel Reason: ${event.reason}`);
|
|
529
|
+
console.log(` Cancelled At: ${new Date(event.closeTimestamp).toISOString()}`);
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
});
|
|
534
|
+
|
|
464
535
|
const DEFAULT_CACHE_LIST = ["1m", "15m", "30m", "1h", "4h"];
|
|
465
|
-
const
|
|
536
|
+
const GET_CACHE_INTERVAL_LIST_FN = () => {
|
|
466
537
|
const { values } = getArgs();
|
|
467
|
-
if (!values.
|
|
468
|
-
console.warn(`Warning: No cache timeframes provided. Using default timeframes: ${DEFAULT_CACHE_LIST.join(", ")}`);
|
|
538
|
+
if (!values.cacheInterval) {
|
|
469
539
|
return DEFAULT_CACHE_LIST;
|
|
470
540
|
}
|
|
471
|
-
return String(values.
|
|
541
|
+
return String(values.cacheInterval)
|
|
472
542
|
.split(",")
|
|
473
543
|
.map((timeframe) => timeframe.trim());
|
|
474
544
|
};
|
|
@@ -512,8 +582,8 @@ class BacktestMainService {
|
|
|
512
582
|
if (!frameName) {
|
|
513
583
|
throw new Error("Frame name is required");
|
|
514
584
|
}
|
|
515
|
-
{
|
|
516
|
-
await this.cacheLogicService.execute(payload.
|
|
585
|
+
if (!payload.noCache) {
|
|
586
|
+
await this.cacheLogicService.execute(payload.cacheInterval, {
|
|
517
587
|
exchangeName,
|
|
518
588
|
frameName,
|
|
519
589
|
symbol,
|
|
@@ -528,6 +598,7 @@ class BacktestMainService {
|
|
|
528
598
|
},
|
|
529
599
|
},
|
|
530
600
|
});
|
|
601
|
+
notifyVerbose();
|
|
531
602
|
}
|
|
532
603
|
backtestKit.Backtest.background(symbol, {
|
|
533
604
|
strategyName,
|
|
@@ -545,19 +616,20 @@ class BacktestMainService {
|
|
|
545
616
|
if (!values.backtest) {
|
|
546
617
|
return;
|
|
547
618
|
}
|
|
548
|
-
const [entryPoint = null] = positionals;
|
|
619
|
+
const [entryPoint = null] = positionals.slice(-1);
|
|
549
620
|
if (!entryPoint) {
|
|
550
621
|
throw new Error("Entry point is required");
|
|
551
622
|
}
|
|
552
|
-
const
|
|
623
|
+
const cacheInterval = GET_CACHE_INTERVAL_LIST_FN();
|
|
553
624
|
return await this.run({
|
|
554
625
|
symbol: values.symbol,
|
|
555
626
|
entryPoint,
|
|
556
|
-
|
|
627
|
+
cacheInterval,
|
|
557
628
|
exchange: values.exchange,
|
|
558
629
|
frame: values.frame,
|
|
559
630
|
strategy: values.strategy,
|
|
560
631
|
verbose: values.verbose,
|
|
632
|
+
noCache: values.noCache,
|
|
561
633
|
});
|
|
562
634
|
});
|
|
563
635
|
}
|
|
@@ -606,6 +678,7 @@ class LiveMainService {
|
|
|
606
678
|
},
|
|
607
679
|
},
|
|
608
680
|
});
|
|
681
|
+
notifyVerbose();
|
|
609
682
|
}
|
|
610
683
|
backtestKit.Live.background(symbol, {
|
|
611
684
|
strategyName,
|
|
@@ -622,7 +695,7 @@ class LiveMainService {
|
|
|
622
695
|
if (!values.live) {
|
|
623
696
|
return;
|
|
624
697
|
}
|
|
625
|
-
const [entryPoint = null] = positionals;
|
|
698
|
+
const [entryPoint = null] = positionals.slice(-1);
|
|
626
699
|
if (!entryPoint) {
|
|
627
700
|
throw new Error("Entry point is required");
|
|
628
701
|
}
|
|
@@ -676,6 +749,7 @@ class PaperMainService {
|
|
|
676
749
|
},
|
|
677
750
|
},
|
|
678
751
|
});
|
|
752
|
+
notifyVerbose();
|
|
679
753
|
}
|
|
680
754
|
backtestKit.Live.background(symbol, {
|
|
681
755
|
strategyName,
|
|
@@ -692,7 +766,7 @@ class PaperMainService {
|
|
|
692
766
|
if (!values.paper) {
|
|
693
767
|
return;
|
|
694
768
|
}
|
|
695
|
-
const [entryPoint = null] = positionals;
|
|
769
|
+
const [entryPoint = null] = positionals.slice(-1);
|
|
696
770
|
if (!entryPoint) {
|
|
697
771
|
throw new Error("Entry point is required");
|
|
698
772
|
}
|
package/build/index.mjs
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Storage, Notification, Markdown, Report, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit } from 'backtest-kit';
|
|
2
3
|
import { getErrorMessage, errorData, singleshot, str, BehaviorSubject, compose, execpool, queued, sleep, randomString, createAwaiter, TIMEOUT_SYMBOL, typo, retry, memoize, trycatch } from 'functools-kit';
|
|
3
4
|
import fs, { constants } from 'fs';
|
|
4
5
|
import * as stackTrace from 'stack-trace';
|
|
5
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
7
|
+
import { createRequire } from 'module';
|
|
6
8
|
import path from 'path';
|
|
7
9
|
import fs$1, { access } from 'fs/promises';
|
|
8
10
|
import dotenv from 'dotenv';
|
|
@@ -19,7 +21,6 @@ import MarkdownIt from 'markdown-it';
|
|
|
19
21
|
import sanitizeHtml from 'sanitize-html';
|
|
20
22
|
import { JSDOM } from 'jsdom';
|
|
21
23
|
import Mustache from 'mustache';
|
|
22
|
-
import { createRequire } from 'module';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Fix for `Attempted to assign to readonly property (at redactToken)`
|
|
@@ -61,7 +62,7 @@ import { createRequire } from 'module';
|
|
|
61
62
|
}
|
|
62
63
|
{
|
|
63
64
|
StorageLive.usePersist();
|
|
64
|
-
StorageBacktest.
|
|
65
|
+
StorageBacktest.useMemory();
|
|
65
66
|
}
|
|
66
67
|
{
|
|
67
68
|
NotificationLive.useDummy();
|
|
@@ -209,6 +210,24 @@ const entrySubject = new BehaviorSubject();
|
|
|
209
210
|
|
|
210
211
|
const __filename = fileURLToPath(import.meta.url);
|
|
211
212
|
const __dirname = path.dirname(__filename);
|
|
213
|
+
const require$1 = createRequire(import.meta.url);
|
|
214
|
+
const REQUIRE_ENTRY_FACTORY = (filePath) => {
|
|
215
|
+
try {
|
|
216
|
+
require$1(filePath);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const IMPORT_ENTRY_FACTORY = async (filePath) => {
|
|
224
|
+
await import(pathToFileURL(filePath).href);
|
|
225
|
+
};
|
|
226
|
+
const LOAD_ENTRY_FN = async (filePath) => {
|
|
227
|
+
if (!REQUIRE_ENTRY_FACTORY(filePath)) {
|
|
228
|
+
await IMPORT_ENTRY_FACTORY(filePath);
|
|
229
|
+
}
|
|
230
|
+
};
|
|
212
231
|
let _is_launched = false;
|
|
213
232
|
class ResolveService {
|
|
214
233
|
constructor() {
|
|
@@ -227,9 +246,9 @@ class ResolveService {
|
|
|
227
246
|
{
|
|
228
247
|
const cwd = process.cwd();
|
|
229
248
|
process.chdir(moduleRoot);
|
|
230
|
-
dotenv.config({ path: path.join(cwd, '.env') });
|
|
231
|
-
dotenv.config({ path: path.join(moduleRoot, '.env'), override: true });
|
|
232
|
-
await
|
|
249
|
+
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
250
|
+
dotenv.config({ path: path.join(moduleRoot, '.env'), override: true, quiet: true });
|
|
251
|
+
await LOAD_ENTRY_FN(absolutePath);
|
|
233
252
|
await entrySubject.next(absolutePath);
|
|
234
253
|
}
|
|
235
254
|
_is_launched = true;
|
|
@@ -372,7 +391,11 @@ const getArgs = singleshot(() => {
|
|
|
372
391
|
type: "boolean",
|
|
373
392
|
default: false,
|
|
374
393
|
},
|
|
375
|
-
|
|
394
|
+
noCache: {
|
|
395
|
+
type: "boolean",
|
|
396
|
+
default: false,
|
|
397
|
+
},
|
|
398
|
+
cacheInterval: {
|
|
376
399
|
type: "string",
|
|
377
400
|
default: "1m, 15m, 30m, 4h",
|
|
378
401
|
},
|
|
@@ -436,17 +459,64 @@ const notifyFinish = singleshot(() => {
|
|
|
436
459
|
});
|
|
437
460
|
|
|
438
461
|
const getEntry = (metaUrl) => {
|
|
439
|
-
|
|
462
|
+
const metaPath = fileURLToPath(metaUrl);
|
|
463
|
+
return path.resolve(process.argv[1]) === path.resolve(metaPath);
|
|
440
464
|
};
|
|
441
465
|
|
|
466
|
+
const notifyVerbose = singleshot(() => {
|
|
467
|
+
console.log("Using verbose logging...");
|
|
468
|
+
listenSignal((event) => {
|
|
469
|
+
if (event.action === "scheduled") {
|
|
470
|
+
console.log(`[POSITION SCHEDULED] ${event.symbol}`);
|
|
471
|
+
console.log(` Strategy: ${event.strategyName}`);
|
|
472
|
+
console.log(` Current Price: ${event.currentPrice}`);
|
|
473
|
+
console.log(` Entry Price: ${event.signal.priceOpen}`);
|
|
474
|
+
console.log(` Signal ID: ${event.signal.id}`);
|
|
475
|
+
console.log(` Direction: ${event.signal.position}`);
|
|
476
|
+
console.log(` Stop Loss: ${event.signal.priceStopLoss}`);
|
|
477
|
+
console.log(` Take Profit: ${event.signal.priceTakeProfit}`);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (event.action === "opened") {
|
|
481
|
+
console.log(`[POSITION OPENED] ${event.symbol}`);
|
|
482
|
+
console.log(` Strategy: ${event.strategyName}`);
|
|
483
|
+
console.log(` Entry Price: ${event.currentPrice}`);
|
|
484
|
+
console.log(` Signal ID: ${event.signal.id}`);
|
|
485
|
+
console.log(` Direction: ${event.signal.position}`);
|
|
486
|
+
console.log(` Stop Loss: ${event.signal.priceStopLoss}`);
|
|
487
|
+
console.log(` Take Profit: ${event.signal.priceTakeProfit}`);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
if (event.action === "closed") {
|
|
491
|
+
console.log(`[POSITION CLOSED] ${event.symbol}`);
|
|
492
|
+
console.log(` Strategy: ${event.strategyName}`);
|
|
493
|
+
console.log(` Entry Price (adj): ${event.pnl.priceOpen}`);
|
|
494
|
+
console.log(` Exit Price (adj): ${event.pnl.priceClose}`);
|
|
495
|
+
console.log(` Signal ID: ${event.signal.id}`);
|
|
496
|
+
console.log(` Close Reason: ${event.closeReason}`);
|
|
497
|
+
console.log(` PnL: ${event.pnl.pnlPercentage.toFixed(2)}%`);
|
|
498
|
+
console.log(` Win: ${event.pnl.pnlPercentage > 0 ? "YES" : "NO"}`);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (event.action === "cancelled") {
|
|
502
|
+
console.log(`[POSITION CANCELLED] ${event.symbol}`);
|
|
503
|
+
console.log(` Strategy: ${event.strategyName}`);
|
|
504
|
+
console.log(` Signal ID: ${event.signal.id}`);
|
|
505
|
+
console.log(` Current Price: ${event.currentPrice}`);
|
|
506
|
+
console.log(` Cancel Reason: ${event.reason}`);
|
|
507
|
+
console.log(` Cancelled At: ${new Date(event.closeTimestamp).toISOString()}`);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
|
|
442
513
|
const DEFAULT_CACHE_LIST = ["1m", "15m", "30m", "1h", "4h"];
|
|
443
|
-
const
|
|
514
|
+
const GET_CACHE_INTERVAL_LIST_FN = () => {
|
|
444
515
|
const { values } = getArgs();
|
|
445
|
-
if (!values.
|
|
446
|
-
console.warn(`Warning: No cache timeframes provided. Using default timeframes: ${DEFAULT_CACHE_LIST.join(", ")}`);
|
|
516
|
+
if (!values.cacheInterval) {
|
|
447
517
|
return DEFAULT_CACHE_LIST;
|
|
448
518
|
}
|
|
449
|
-
return String(values.
|
|
519
|
+
return String(values.cacheInterval)
|
|
450
520
|
.split(",")
|
|
451
521
|
.map((timeframe) => timeframe.trim());
|
|
452
522
|
};
|
|
@@ -490,8 +560,8 @@ class BacktestMainService {
|
|
|
490
560
|
if (!frameName) {
|
|
491
561
|
throw new Error("Frame name is required");
|
|
492
562
|
}
|
|
493
|
-
{
|
|
494
|
-
await this.cacheLogicService.execute(payload.
|
|
563
|
+
if (!payload.noCache) {
|
|
564
|
+
await this.cacheLogicService.execute(payload.cacheInterval, {
|
|
495
565
|
exchangeName,
|
|
496
566
|
frameName,
|
|
497
567
|
symbol,
|
|
@@ -506,6 +576,7 @@ class BacktestMainService {
|
|
|
506
576
|
},
|
|
507
577
|
},
|
|
508
578
|
});
|
|
579
|
+
notifyVerbose();
|
|
509
580
|
}
|
|
510
581
|
Backtest.background(symbol, {
|
|
511
582
|
strategyName,
|
|
@@ -523,19 +594,20 @@ class BacktestMainService {
|
|
|
523
594
|
if (!values.backtest) {
|
|
524
595
|
return;
|
|
525
596
|
}
|
|
526
|
-
const [entryPoint = null] = positionals;
|
|
597
|
+
const [entryPoint = null] = positionals.slice(-1);
|
|
527
598
|
if (!entryPoint) {
|
|
528
599
|
throw new Error("Entry point is required");
|
|
529
600
|
}
|
|
530
|
-
const
|
|
601
|
+
const cacheInterval = GET_CACHE_INTERVAL_LIST_FN();
|
|
531
602
|
return await this.run({
|
|
532
603
|
symbol: values.symbol,
|
|
533
604
|
entryPoint,
|
|
534
|
-
|
|
605
|
+
cacheInterval,
|
|
535
606
|
exchange: values.exchange,
|
|
536
607
|
frame: values.frame,
|
|
537
608
|
strategy: values.strategy,
|
|
538
609
|
verbose: values.verbose,
|
|
610
|
+
noCache: values.noCache,
|
|
539
611
|
});
|
|
540
612
|
});
|
|
541
613
|
}
|
|
@@ -584,6 +656,7 @@ class LiveMainService {
|
|
|
584
656
|
},
|
|
585
657
|
},
|
|
586
658
|
});
|
|
659
|
+
notifyVerbose();
|
|
587
660
|
}
|
|
588
661
|
Live.background(symbol, {
|
|
589
662
|
strategyName,
|
|
@@ -600,7 +673,7 @@ class LiveMainService {
|
|
|
600
673
|
if (!values.live) {
|
|
601
674
|
return;
|
|
602
675
|
}
|
|
603
|
-
const [entryPoint = null] = positionals;
|
|
676
|
+
const [entryPoint = null] = positionals.slice(-1);
|
|
604
677
|
if (!entryPoint) {
|
|
605
678
|
throw new Error("Entry point is required");
|
|
606
679
|
}
|
|
@@ -654,6 +727,7 @@ class PaperMainService {
|
|
|
654
727
|
},
|
|
655
728
|
},
|
|
656
729
|
});
|
|
730
|
+
notifyVerbose();
|
|
657
731
|
}
|
|
658
732
|
Live.background(symbol, {
|
|
659
733
|
strategyName,
|
|
@@ -670,7 +744,7 @@ class PaperMainService {
|
|
|
670
744
|
if (!values.paper) {
|
|
671
745
|
return;
|
|
672
746
|
}
|
|
673
|
-
const [entryPoint = null] = positionals;
|
|
747
|
+
const [entryPoint = null] = positionals.slice(-1);
|
|
674
748
|
if (!entryPoint) {
|
|
675
749
|
throw new Error("Entry point is required");
|
|
676
750
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backtest-kit/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
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",
|
|
@@ -59,6 +59,7 @@
|
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@backtest-kit/ui": "3.1.0",
|
|
62
|
+
"markdown-it": "14.1.1",
|
|
62
63
|
"@rollup/plugin-typescript": "11.1.6",
|
|
63
64
|
"@types/image-size": "0.7.0",
|
|
64
65
|
"@types/jsdom": "21.1.7",
|
|
@@ -77,7 +78,9 @@
|
|
|
77
78
|
"worker-testbed": "1.0.12"
|
|
78
79
|
},
|
|
79
80
|
"peerDependencies": {
|
|
80
|
-
"typescript": "^5.0.0"
|
|
81
|
+
"typescript": "^5.0.0",
|
|
82
|
+
"@backtest-kit/ui": "^3.1.0",
|
|
83
|
+
"markdown-it": "^14.1.1"
|
|
81
84
|
},
|
|
82
85
|
"dependencies": {
|
|
83
86
|
"ccxt": "4.5.39",
|
package/types.d.ts
CHANGED
|
@@ -68,8 +68,9 @@ declare class BacktestMainService {
|
|
|
68
68
|
strategy: string;
|
|
69
69
|
exchange: string;
|
|
70
70
|
frame: string;
|
|
71
|
-
|
|
71
|
+
cacheInterval: string[];
|
|
72
72
|
verbose: boolean;
|
|
73
|
+
noCache: boolean;
|
|
73
74
|
}) => Promise<void>) & functools_kit.ISingleshotClearable;
|
|
74
75
|
connect: (() => Promise<void>) & functools_kit.ISingleshotClearable;
|
|
75
76
|
}
|