@backtest-kit/cli 0.0.4 → 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 +8 -6
- package/build/index.cjs +91 -18
- package/build/index.mjs +92 -19
- package/package.json +5 -2
- package/types.d.ts +2 -1
package/README.md
CHANGED
|
@@ -137,11 +137,12 @@ npm start -- --symbol BTCUSDT --ui
|
|
|
137
137
|
| `--ui` | boolean | Start web UI dashboard (default: `false`) |
|
|
138
138
|
| `--telegram` | boolean | Enable Telegram notifications (default: `false`) |
|
|
139
139
|
| `--verbose` | boolean | Log each candle fetch (default: `false`) |
|
|
140
|
+
| `--noCache` | boolean | Skip candle cache warming before backtest (default: `false`) |
|
|
140
141
|
| `--symbol` | string | Trading pair (default: `"BTCUSDT"`) |
|
|
141
142
|
| `--strategy` | string | Strategy name (default: first registered) |
|
|
142
143
|
| `--exchange` | string | Exchange name (default: first registered) |
|
|
143
144
|
| `--frame` | string | Backtest frame name (default: first registered) |
|
|
144
|
-
| `--
|
|
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
|
@@ -6,6 +6,7 @@ var functoolsKit = require('functools-kit');
|
|
|
6
6
|
var fs = require('fs');
|
|
7
7
|
var stackTrace = require('stack-trace');
|
|
8
8
|
var url = require('url');
|
|
9
|
+
var module$1 = require('module');
|
|
9
10
|
var path = require('path');
|
|
10
11
|
var fs$1 = require('fs/promises');
|
|
11
12
|
var dotenv = require('dotenv');
|
|
@@ -22,7 +23,6 @@ var MarkdownIt = require('markdown-it');
|
|
|
22
23
|
var sanitizeHtml = require('sanitize-html');
|
|
23
24
|
var jsdom = require('jsdom');
|
|
24
25
|
var Mustache = require('mustache');
|
|
25
|
-
var module$1 = require('module');
|
|
26
26
|
|
|
27
27
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
28
28
|
function _interopNamespaceDefault(e) {
|
|
@@ -84,7 +84,7 @@ var stackTrace__namespace = /*#__PURE__*/_interopNamespaceDefault(stackTrace);
|
|
|
84
84
|
}
|
|
85
85
|
{
|
|
86
86
|
backtestKit.StorageLive.usePersist();
|
|
87
|
-
backtestKit.StorageBacktest.
|
|
87
|
+
backtestKit.StorageBacktest.useMemory();
|
|
88
88
|
}
|
|
89
89
|
{
|
|
90
90
|
backtestKit.NotificationLive.useDummy();
|
|
@@ -232,6 +232,24 @@ const entrySubject = new functoolsKit.BehaviorSubject();
|
|
|
232
232
|
|
|
233
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)));
|
|
234
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
|
+
};
|
|
235
253
|
let _is_launched = false;
|
|
236
254
|
class ResolveService {
|
|
237
255
|
constructor() {
|
|
@@ -250,9 +268,9 @@ class ResolveService {
|
|
|
250
268
|
{
|
|
251
269
|
const cwd = process.cwd();
|
|
252
270
|
process.chdir(moduleRoot);
|
|
253
|
-
dotenv.config({ path: path.join(cwd, '.env') });
|
|
254
|
-
dotenv.config({ path: path.join(moduleRoot, '.env'), override: true });
|
|
255
|
-
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);
|
|
256
274
|
await entrySubject.next(absolutePath);
|
|
257
275
|
}
|
|
258
276
|
_is_launched = true;
|
|
@@ -395,7 +413,11 @@ const getArgs = functoolsKit.singleshot(() => {
|
|
|
395
413
|
type: "boolean",
|
|
396
414
|
default: false,
|
|
397
415
|
},
|
|
398
|
-
|
|
416
|
+
noCache: {
|
|
417
|
+
type: "boolean",
|
|
418
|
+
default: false,
|
|
419
|
+
},
|
|
420
|
+
cacheInterval: {
|
|
399
421
|
type: "string",
|
|
400
422
|
default: "1m, 15m, 30m, 4h",
|
|
401
423
|
},
|
|
@@ -459,17 +481,64 @@ const notifyFinish = functoolsKit.singleshot(() => {
|
|
|
459
481
|
});
|
|
460
482
|
|
|
461
483
|
const getEntry = (metaUrl) => {
|
|
462
|
-
|
|
484
|
+
const metaPath = url.fileURLToPath(metaUrl);
|
|
485
|
+
return path.resolve(process.argv[1]) === path.resolve(metaPath);
|
|
463
486
|
};
|
|
464
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
|
+
|
|
465
535
|
const DEFAULT_CACHE_LIST = ["1m", "15m", "30m", "1h", "4h"];
|
|
466
|
-
const
|
|
536
|
+
const GET_CACHE_INTERVAL_LIST_FN = () => {
|
|
467
537
|
const { values } = getArgs();
|
|
468
|
-
if (!values.
|
|
469
|
-
console.warn(`Warning: No cache timeframes provided. Using default timeframes: ${DEFAULT_CACHE_LIST.join(", ")}`);
|
|
538
|
+
if (!values.cacheInterval) {
|
|
470
539
|
return DEFAULT_CACHE_LIST;
|
|
471
540
|
}
|
|
472
|
-
return String(values.
|
|
541
|
+
return String(values.cacheInterval)
|
|
473
542
|
.split(",")
|
|
474
543
|
.map((timeframe) => timeframe.trim());
|
|
475
544
|
};
|
|
@@ -513,8 +582,8 @@ class BacktestMainService {
|
|
|
513
582
|
if (!frameName) {
|
|
514
583
|
throw new Error("Frame name is required");
|
|
515
584
|
}
|
|
516
|
-
{
|
|
517
|
-
await this.cacheLogicService.execute(payload.
|
|
585
|
+
if (!payload.noCache) {
|
|
586
|
+
await this.cacheLogicService.execute(payload.cacheInterval, {
|
|
518
587
|
exchangeName,
|
|
519
588
|
frameName,
|
|
520
589
|
symbol,
|
|
@@ -529,6 +598,7 @@ class BacktestMainService {
|
|
|
529
598
|
},
|
|
530
599
|
},
|
|
531
600
|
});
|
|
601
|
+
notifyVerbose();
|
|
532
602
|
}
|
|
533
603
|
backtestKit.Backtest.background(symbol, {
|
|
534
604
|
strategyName,
|
|
@@ -546,19 +616,20 @@ class BacktestMainService {
|
|
|
546
616
|
if (!values.backtest) {
|
|
547
617
|
return;
|
|
548
618
|
}
|
|
549
|
-
const [entryPoint = null] = positionals;
|
|
619
|
+
const [entryPoint = null] = positionals.slice(-1);
|
|
550
620
|
if (!entryPoint) {
|
|
551
621
|
throw new Error("Entry point is required");
|
|
552
622
|
}
|
|
553
|
-
const
|
|
623
|
+
const cacheInterval = GET_CACHE_INTERVAL_LIST_FN();
|
|
554
624
|
return await this.run({
|
|
555
625
|
symbol: values.symbol,
|
|
556
626
|
entryPoint,
|
|
557
|
-
|
|
627
|
+
cacheInterval,
|
|
558
628
|
exchange: values.exchange,
|
|
559
629
|
frame: values.frame,
|
|
560
630
|
strategy: values.strategy,
|
|
561
631
|
verbose: values.verbose,
|
|
632
|
+
noCache: values.noCache,
|
|
562
633
|
});
|
|
563
634
|
});
|
|
564
635
|
}
|
|
@@ -607,6 +678,7 @@ class LiveMainService {
|
|
|
607
678
|
},
|
|
608
679
|
},
|
|
609
680
|
});
|
|
681
|
+
notifyVerbose();
|
|
610
682
|
}
|
|
611
683
|
backtestKit.Live.background(symbol, {
|
|
612
684
|
strategyName,
|
|
@@ -623,7 +695,7 @@ class LiveMainService {
|
|
|
623
695
|
if (!values.live) {
|
|
624
696
|
return;
|
|
625
697
|
}
|
|
626
|
-
const [entryPoint = null] = positionals;
|
|
698
|
+
const [entryPoint = null] = positionals.slice(-1);
|
|
627
699
|
if (!entryPoint) {
|
|
628
700
|
throw new Error("Entry point is required");
|
|
629
701
|
}
|
|
@@ -677,6 +749,7 @@ class PaperMainService {
|
|
|
677
749
|
},
|
|
678
750
|
},
|
|
679
751
|
});
|
|
752
|
+
notifyVerbose();
|
|
680
753
|
}
|
|
681
754
|
backtestKit.Live.background(symbol, {
|
|
682
755
|
strategyName,
|
|
@@ -693,7 +766,7 @@ class PaperMainService {
|
|
|
693
766
|
if (!values.paper) {
|
|
694
767
|
return;
|
|
695
768
|
}
|
|
696
|
-
const [entryPoint = null] = positionals;
|
|
769
|
+
const [entryPoint = null] = positionals.slice(-1);
|
|
697
770
|
if (!entryPoint) {
|
|
698
771
|
throw new Error("Entry point is required");
|
|
699
772
|
}
|
package/build/index.mjs
CHANGED
|
@@ -1,9 +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, listStrategySchema, overrideExchangeSchema, Backtest, Live, getCandles, checkCandles, warmCandles, listenRisk,
|
|
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';
|
|
3
3
|
import { getErrorMessage, errorData, singleshot, str, BehaviorSubject, compose, execpool, queued, sleep, randomString, createAwaiter, TIMEOUT_SYMBOL, typo, retry, memoize, trycatch } from 'functools-kit';
|
|
4
4
|
import fs, { constants } from 'fs';
|
|
5
5
|
import * as stackTrace from 'stack-trace';
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
7
|
+
import { createRequire } from 'module';
|
|
7
8
|
import path from 'path';
|
|
8
9
|
import fs$1, { access } from 'fs/promises';
|
|
9
10
|
import dotenv from 'dotenv';
|
|
@@ -20,7 +21,6 @@ import MarkdownIt from 'markdown-it';
|
|
|
20
21
|
import sanitizeHtml from 'sanitize-html';
|
|
21
22
|
import { JSDOM } from 'jsdom';
|
|
22
23
|
import Mustache from 'mustache';
|
|
23
|
-
import { createRequire } from 'module';
|
|
24
24
|
|
|
25
25
|
/**
|
|
26
26
|
* Fix for `Attempted to assign to readonly property (at redactToken)`
|
|
@@ -62,7 +62,7 @@ import { createRequire } from 'module';
|
|
|
62
62
|
}
|
|
63
63
|
{
|
|
64
64
|
StorageLive.usePersist();
|
|
65
|
-
StorageBacktest.
|
|
65
|
+
StorageBacktest.useMemory();
|
|
66
66
|
}
|
|
67
67
|
{
|
|
68
68
|
NotificationLive.useDummy();
|
|
@@ -210,6 +210,24 @@ const entrySubject = new BehaviorSubject();
|
|
|
210
210
|
|
|
211
211
|
const __filename = fileURLToPath(import.meta.url);
|
|
212
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
|
+
};
|
|
213
231
|
let _is_launched = false;
|
|
214
232
|
class ResolveService {
|
|
215
233
|
constructor() {
|
|
@@ -228,9 +246,9 @@ class ResolveService {
|
|
|
228
246
|
{
|
|
229
247
|
const cwd = process.cwd();
|
|
230
248
|
process.chdir(moduleRoot);
|
|
231
|
-
dotenv.config({ path: path.join(cwd, '.env') });
|
|
232
|
-
dotenv.config({ path: path.join(moduleRoot, '.env'), override: true });
|
|
233
|
-
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);
|
|
234
252
|
await entrySubject.next(absolutePath);
|
|
235
253
|
}
|
|
236
254
|
_is_launched = true;
|
|
@@ -373,7 +391,11 @@ const getArgs = singleshot(() => {
|
|
|
373
391
|
type: "boolean",
|
|
374
392
|
default: false,
|
|
375
393
|
},
|
|
376
|
-
|
|
394
|
+
noCache: {
|
|
395
|
+
type: "boolean",
|
|
396
|
+
default: false,
|
|
397
|
+
},
|
|
398
|
+
cacheInterval: {
|
|
377
399
|
type: "string",
|
|
378
400
|
default: "1m, 15m, 30m, 4h",
|
|
379
401
|
},
|
|
@@ -437,17 +459,64 @@ const notifyFinish = singleshot(() => {
|
|
|
437
459
|
});
|
|
438
460
|
|
|
439
461
|
const getEntry = (metaUrl) => {
|
|
440
|
-
|
|
462
|
+
const metaPath = fileURLToPath(metaUrl);
|
|
463
|
+
return path.resolve(process.argv[1]) === path.resolve(metaPath);
|
|
441
464
|
};
|
|
442
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
|
+
|
|
443
513
|
const DEFAULT_CACHE_LIST = ["1m", "15m", "30m", "1h", "4h"];
|
|
444
|
-
const
|
|
514
|
+
const GET_CACHE_INTERVAL_LIST_FN = () => {
|
|
445
515
|
const { values } = getArgs();
|
|
446
|
-
if (!values.
|
|
447
|
-
console.warn(`Warning: No cache timeframes provided. Using default timeframes: ${DEFAULT_CACHE_LIST.join(", ")}`);
|
|
516
|
+
if (!values.cacheInterval) {
|
|
448
517
|
return DEFAULT_CACHE_LIST;
|
|
449
518
|
}
|
|
450
|
-
return String(values.
|
|
519
|
+
return String(values.cacheInterval)
|
|
451
520
|
.split(",")
|
|
452
521
|
.map((timeframe) => timeframe.trim());
|
|
453
522
|
};
|
|
@@ -491,8 +560,8 @@ class BacktestMainService {
|
|
|
491
560
|
if (!frameName) {
|
|
492
561
|
throw new Error("Frame name is required");
|
|
493
562
|
}
|
|
494
|
-
{
|
|
495
|
-
await this.cacheLogicService.execute(payload.
|
|
563
|
+
if (!payload.noCache) {
|
|
564
|
+
await this.cacheLogicService.execute(payload.cacheInterval, {
|
|
496
565
|
exchangeName,
|
|
497
566
|
frameName,
|
|
498
567
|
symbol,
|
|
@@ -507,6 +576,7 @@ class BacktestMainService {
|
|
|
507
576
|
},
|
|
508
577
|
},
|
|
509
578
|
});
|
|
579
|
+
notifyVerbose();
|
|
510
580
|
}
|
|
511
581
|
Backtest.background(symbol, {
|
|
512
582
|
strategyName,
|
|
@@ -524,19 +594,20 @@ class BacktestMainService {
|
|
|
524
594
|
if (!values.backtest) {
|
|
525
595
|
return;
|
|
526
596
|
}
|
|
527
|
-
const [entryPoint = null] = positionals;
|
|
597
|
+
const [entryPoint = null] = positionals.slice(-1);
|
|
528
598
|
if (!entryPoint) {
|
|
529
599
|
throw new Error("Entry point is required");
|
|
530
600
|
}
|
|
531
|
-
const
|
|
601
|
+
const cacheInterval = GET_CACHE_INTERVAL_LIST_FN();
|
|
532
602
|
return await this.run({
|
|
533
603
|
symbol: values.symbol,
|
|
534
604
|
entryPoint,
|
|
535
|
-
|
|
605
|
+
cacheInterval,
|
|
536
606
|
exchange: values.exchange,
|
|
537
607
|
frame: values.frame,
|
|
538
608
|
strategy: values.strategy,
|
|
539
609
|
verbose: values.verbose,
|
|
610
|
+
noCache: values.noCache,
|
|
540
611
|
});
|
|
541
612
|
});
|
|
542
613
|
}
|
|
@@ -585,6 +656,7 @@ class LiveMainService {
|
|
|
585
656
|
},
|
|
586
657
|
},
|
|
587
658
|
});
|
|
659
|
+
notifyVerbose();
|
|
588
660
|
}
|
|
589
661
|
Live.background(symbol, {
|
|
590
662
|
strategyName,
|
|
@@ -601,7 +673,7 @@ class LiveMainService {
|
|
|
601
673
|
if (!values.live) {
|
|
602
674
|
return;
|
|
603
675
|
}
|
|
604
|
-
const [entryPoint = null] = positionals;
|
|
676
|
+
const [entryPoint = null] = positionals.slice(-1);
|
|
605
677
|
if (!entryPoint) {
|
|
606
678
|
throw new Error("Entry point is required");
|
|
607
679
|
}
|
|
@@ -655,6 +727,7 @@ class PaperMainService {
|
|
|
655
727
|
},
|
|
656
728
|
},
|
|
657
729
|
});
|
|
730
|
+
notifyVerbose();
|
|
658
731
|
}
|
|
659
732
|
Live.background(symbol, {
|
|
660
733
|
strategyName,
|
|
@@ -671,7 +744,7 @@ class PaperMainService {
|
|
|
671
744
|
if (!values.paper) {
|
|
672
745
|
return;
|
|
673
746
|
}
|
|
674
|
-
const [entryPoint = null] = positionals;
|
|
747
|
+
const [entryPoint = null] = positionals.slice(-1);
|
|
675
748
|
if (!entryPoint) {
|
|
676
749
|
throw new Error("Entry point is required");
|
|
677
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
|
}
|