@backtest-kit/cli 16.3.0 → 16.4.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 +31 -2
- package/build/index.cjs +283 -33
- package/build/index.mjs +287 -37
- package/docker/package.json +6 -6
- package/package.json +13 -13
- package/template/project/package.mustache +5 -5
package/README.md
CHANGED
|
@@ -104,6 +104,7 @@ Every invocation is **one mode** (a primary flag) + a positional strategy/entry
|
|
|
104
104
|
| **Candle Dump** | `--dump` | Fetch & save raw OHLCV candles to a file |
|
|
105
105
|
| **PnL Debug** | `--pnldebug` | Simulate per-minute PnL for a given entry price & direction |
|
|
106
106
|
| **Broker Debug** | `--brokerdebug` | Fire a single broker commit against the live adapter |
|
|
107
|
+
| **Simulator** | `--simulator` | Sweep exit/entry parameters over a crowd-ideas feed, train an author whitelist |
|
|
107
108
|
| **Flush** | `--flush` | Delete report/log/markdown/agent folders from a strategy dump dir |
|
|
108
109
|
| **Init** | `--init` | Scaffold a new project |
|
|
109
110
|
| **Docker** | `--docker` | Scaffold a self-contained Docker workspace |
|
|
@@ -266,12 +267,12 @@ The same shape works for `--live --entry` / `--paper --entry` (call `Live.backgr
|
|
|
266
267
|
|
|
267
268
|
## 🛠️ Tooling modes
|
|
268
269
|
|
|
269
|
-
|
|
270
|
+
Six utilities that don't run a strategy. They share one convention, explained once here and referenced below.
|
|
270
271
|
|
|
271
272
|
> **The `<mode>.module` convention.** By default the CLI auto-registers CCXT Binance. To use a different exchange (custom API keys, rate limits, a non-spot market), drop a `modules/<mode>.module.ts` that calls `addExchangeSchema` from `backtest-kit`. The CLI loads it automatically before running, trying `.ts`/`.mjs`/`.cjs`; it's searched **next to the target file first, then in the project root**. `.env` is loaded root-first then the target-file dir (override), so API keys stay out of code.
|
|
272
273
|
|
|
273
274
|
<details>
|
|
274
|
-
<summary>The shared <code><mode>.module.ts</code> shape (pine / editor / dump / pnldebug / brokerdebug)</summary>
|
|
275
|
+
<summary>The shared <code><mode>.module.ts</code> shape (pine / editor / dump / pnldebug / brokerdebug / simulator)</summary>
|
|
275
276
|
|
|
276
277
|
```typescript
|
|
277
278
|
// modules/pine.module.ts (same shape for editor/dump/pnldebug.module; brokerdebug registers a Broker instead)
|
|
@@ -449,6 +450,34 @@ The CLI loads `./modules/brokerdebug.module`, fetches the last candle for `--sym
|
|
|
449
450
|
|
|
450
451
|
</details>
|
|
451
452
|
|
|
453
|
+
### 🎛️ Simulator (`--simulator`)
|
|
454
|
+
|
|
455
|
+
Sweep exit/entry parameters over a feed of crowd trading ideas using the `Simulator` entity — one candle pass per idea, a full parameter grid evaluated arithmetically, an author whitelist trained with default-ban semantics. Prints a Markdown report with three ranking winners (time-based Sharpe, Sortino, PnL) and the production whitelist.
|
|
456
|
+
|
|
457
|
+
```bash
|
|
458
|
+
npx @backtest-kit/cli --simulator --symbol BTCUSDT ./assets/ts-ideas.normalized.jsonl
|
|
459
|
+
# → summary to stdout; add --json / --markdown to save into ./dump/
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
<details>
|
|
463
|
+
<summary>Simulator flags, input format & behavior</summary>
|
|
464
|
+
|
|
465
|
+
| Flag | Type | Description |
|
|
466
|
+
|------|------|-------------|
|
|
467
|
+
| `--simulator` | boolean | Enable simulator mode |
|
|
468
|
+
| `--symbol` | string | Trading pair to simulate (default `"BTCUSDT"`) |
|
|
469
|
+
| `--exchange` | string | Exchange (default: first registered, falls back to CCXT Binance) |
|
|
470
|
+
| `--output` | string | Output base name (default `simulator_{SYMBOL}_{TIMESTAMP}`) |
|
|
471
|
+
| `--json` | boolean | Save the full `ISimulatorResult` to `./dump/<output>.json` |
|
|
472
|
+
| `--markdown` | boolean | Save the summary report to `./dump/<output>.md` |
|
|
473
|
+
| `--verbose` | boolean | Log every simulator lifecycle callback to the console |
|
|
474
|
+
|
|
475
|
+
**Positional (required):** path to an ideas `.jsonl` file — one idea per line, exact shape `{ "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`. The file is validated **before any work starts** — the first line that does not match the structure aborts the run with an error naming the line and the field. Ideas of other symbols are filtered out by the engine, so one shared feed serves any `--symbol`.
|
|
476
|
+
|
|
477
|
+
Under the hood: 5-day candle horizon per idea (lazy chunked fetch through the exchange, persist cache first), flood dedupe (one idea per author per direction per 8h), author ban thresholds swept as grid axes (unproven author = banned by default), production slot semantics, time-based Sharpe/Sortino over daily equity buckets. With `--verbose` every lifecycle callback (`onIdeas`, `onProfiles`, `onAuthorsTrained`, `onGridPoint`, `onRanking`, `onDone`) is logged to the console as it fires, so long runs show progress. Exchange via `simulator.module` (see convention above).
|
|
478
|
+
|
|
479
|
+
</details>
|
|
480
|
+
|
|
452
481
|
### 🗑️ Flush (`--flush`)
|
|
453
482
|
|
|
454
483
|
Delete generated output folders from one or more strategy dump dirs **without** touching cached candle data.
|
package/build/index.cjs
CHANGED
|
@@ -233,14 +233,14 @@ const getReadySubject = functoolsKit.singleshot(() => {
|
|
|
233
233
|
return new functoolsKit.Subject();
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
-
const main$
|
|
236
|
+
const main$j = () => {
|
|
237
237
|
const entrySubject = getEntrySubject();
|
|
238
238
|
entrySubject.subscribe(async (path) => {
|
|
239
239
|
console.log("Running", path);
|
|
240
240
|
await getReadySubject().next();
|
|
241
241
|
});
|
|
242
242
|
};
|
|
243
|
-
main$
|
|
243
|
+
main$j();
|
|
244
244
|
|
|
245
245
|
const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
|
|
246
246
|
function dumpStackTrace(error) {
|
|
@@ -891,6 +891,10 @@ const getArgs = functoolsKit.singleshot(() => {
|
|
|
891
891
|
type: "boolean",
|
|
892
892
|
default: false,
|
|
893
893
|
},
|
|
894
|
+
simulator: {
|
|
895
|
+
type: "boolean",
|
|
896
|
+
default: false,
|
|
897
|
+
},
|
|
894
898
|
commit: {
|
|
895
899
|
type: "string",
|
|
896
900
|
default: "",
|
|
@@ -3534,14 +3538,14 @@ const cli = {
|
|
|
3534
3538
|
};
|
|
3535
3539
|
init();
|
|
3536
3540
|
|
|
3537
|
-
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "flush", "init", "docker", "help", "version"];
|
|
3541
|
+
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "flush", "init", "docker", "help", "version"];
|
|
3538
3542
|
const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
|
|
3539
3543
|
const HELP_TEXT$1 = `
|
|
3540
3544
|
Example:
|
|
3541
3545
|
|
|
3542
3546
|
node ${ENTRY_PATH$1} --help
|
|
3543
3547
|
`.trimStart();
|
|
3544
|
-
const main$
|
|
3548
|
+
const main$i = async () => {
|
|
3545
3549
|
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)))) {
|
|
3546
3550
|
return;
|
|
3547
3551
|
}
|
|
@@ -3549,14 +3553,14 @@ const main$h = async () => {
|
|
|
3549
3553
|
if (MODES.some((mode) => values[mode])) {
|
|
3550
3554
|
return;
|
|
3551
3555
|
}
|
|
3552
|
-
process.stdout.write(`@backtest-kit/cli ${"16.
|
|
3556
|
+
process.stdout.write(`@backtest-kit/cli ${"16.4.0"}\n`);
|
|
3553
3557
|
process.stdout.write("\n");
|
|
3554
3558
|
process.stdout.write(`Run with --help to see available commands.\n`);
|
|
3555
3559
|
process.stdout.write("\n");
|
|
3556
3560
|
process.stdout.write(HELP_TEXT$1);
|
|
3557
3561
|
process.exit(0);
|
|
3558
3562
|
};
|
|
3559
|
-
main$
|
|
3563
|
+
main$i();
|
|
3560
3564
|
|
|
3561
3565
|
const notifyShutdown = functoolsKit.singleshot(async () => {
|
|
3562
3566
|
console.log("Graceful shutdown initiated.");
|
|
@@ -3572,7 +3576,7 @@ const flush = async (entryPoint) => {
|
|
|
3572
3576
|
console.log(`Removed: ${target}`);
|
|
3573
3577
|
}
|
|
3574
3578
|
};
|
|
3575
|
-
const main$
|
|
3579
|
+
const main$h = async () => {
|
|
3576
3580
|
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)))) {
|
|
3577
3581
|
return;
|
|
3578
3582
|
}
|
|
@@ -3589,7 +3593,7 @@ const main$g = async () => {
|
|
|
3589
3593
|
}
|
|
3590
3594
|
process.exit(0);
|
|
3591
3595
|
};
|
|
3592
|
-
main$
|
|
3596
|
+
main$h();
|
|
3593
3597
|
|
|
3594
3598
|
const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
|
|
3595
3599
|
process.off("SIGINT", BEFORE_EXIT_FN$6);
|
|
@@ -3612,7 +3616,7 @@ const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
|
|
|
3612
3616
|
const listenGracefulShutdown$6 = functoolsKit.singleshot(() => {
|
|
3613
3617
|
process.on("SIGINT", BEFORE_EXIT_FN$6);
|
|
3614
3618
|
});
|
|
3615
|
-
const main$
|
|
3619
|
+
const main$g = async () => {
|
|
3616
3620
|
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)))) {
|
|
3617
3621
|
return;
|
|
3618
3622
|
}
|
|
@@ -3630,7 +3634,7 @@ const main$f = async () => {
|
|
|
3630
3634
|
await cli.backtestMainService.connect();
|
|
3631
3635
|
listenGracefulShutdown$6();
|
|
3632
3636
|
};
|
|
3633
|
-
main$
|
|
3637
|
+
main$g();
|
|
3634
3638
|
|
|
3635
3639
|
const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
|
|
3636
3640
|
process.off("SIGINT", BEFORE_EXIT_FN$5);
|
|
@@ -3649,7 +3653,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
|
|
|
3649
3653
|
const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
|
|
3650
3654
|
process.on("SIGINT", BEFORE_EXIT_FN$5);
|
|
3651
3655
|
});
|
|
3652
|
-
const main$
|
|
3656
|
+
const main$f = async () => {
|
|
3653
3657
|
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)))) {
|
|
3654
3658
|
return;
|
|
3655
3659
|
}
|
|
@@ -3668,7 +3672,7 @@ const main$e = async () => {
|
|
|
3668
3672
|
listenGracefulShutdown$5();
|
|
3669
3673
|
await cli.walkerMainService.connect();
|
|
3670
3674
|
};
|
|
3671
|
-
main$
|
|
3675
|
+
main$f();
|
|
3672
3676
|
|
|
3673
3677
|
const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
3674
3678
|
process.off("SIGINT", BEFORE_EXIT_FN$4);
|
|
@@ -3690,7 +3694,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
|
3690
3694
|
const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
|
|
3691
3695
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
3692
3696
|
});
|
|
3693
|
-
const main$
|
|
3697
|
+
const main$e = async () => {
|
|
3694
3698
|
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)))) {
|
|
3695
3699
|
return;
|
|
3696
3700
|
}
|
|
@@ -3704,7 +3708,7 @@ const main$d = async () => {
|
|
|
3704
3708
|
await cli.paperMainService.connect();
|
|
3705
3709
|
listenGracefulShutdown$4();
|
|
3706
3710
|
};
|
|
3707
|
-
main$
|
|
3711
|
+
main$e();
|
|
3708
3712
|
|
|
3709
3713
|
const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
3710
3714
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -3726,7 +3730,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
|
3726
3730
|
const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
|
|
3727
3731
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
3728
3732
|
});
|
|
3729
|
-
const main$
|
|
3733
|
+
const main$d = async () => {
|
|
3730
3734
|
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)))) {
|
|
3731
3735
|
return;
|
|
3732
3736
|
}
|
|
@@ -3740,7 +3744,7 @@ const main$c = async () => {
|
|
|
3740
3744
|
await cli.liveMainService.connect();
|
|
3741
3745
|
listenGracefulShutdown$3();
|
|
3742
3746
|
};
|
|
3743
|
-
main$
|
|
3747
|
+
main$d();
|
|
3744
3748
|
|
|
3745
3749
|
const stopBacktestList$1 = async () => {
|
|
3746
3750
|
for (const item of await BacktestKit.Backtest.list()) {
|
|
@@ -3804,7 +3808,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
|
|
|
3804
3808
|
const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
|
|
3805
3809
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
3806
3810
|
});
|
|
3807
|
-
const main$
|
|
3811
|
+
const main$c = async () => {
|
|
3808
3812
|
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)))) {
|
|
3809
3813
|
return;
|
|
3810
3814
|
}
|
|
@@ -3860,7 +3864,7 @@ const main$b = async () => {
|
|
|
3860
3864
|
listenGracefulShutdown$2();
|
|
3861
3865
|
await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
|
|
3862
3866
|
};
|
|
3863
|
-
main$
|
|
3867
|
+
main$c();
|
|
3864
3868
|
|
|
3865
3869
|
const MODE_MODULE = {
|
|
3866
3870
|
backtest: "backtest.module",
|
|
@@ -3963,7 +3967,7 @@ const createGracefulShutdown = (mode) => {
|
|
|
3963
3967
|
process.on("SIGINT", handler);
|
|
3964
3968
|
});
|
|
3965
3969
|
};
|
|
3966
|
-
const main$
|
|
3970
|
+
const main$b = async () => {
|
|
3967
3971
|
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)))) {
|
|
3968
3972
|
return;
|
|
3969
3973
|
}
|
|
@@ -4039,7 +4043,7 @@ const main$a = async () => {
|
|
|
4039
4043
|
}
|
|
4040
4044
|
await getEntrySubject().next(absolutePath);
|
|
4041
4045
|
};
|
|
4042
|
-
main$
|
|
4046
|
+
main$b();
|
|
4043
4047
|
|
|
4044
4048
|
const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
4045
4049
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -4050,7 +4054,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
|
4050
4054
|
const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
|
|
4051
4055
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
4052
4056
|
});
|
|
4053
|
-
const main$
|
|
4057
|
+
const main$a = async () => {
|
|
4054
4058
|
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)))) {
|
|
4055
4059
|
return;
|
|
4056
4060
|
}
|
|
@@ -4060,7 +4064,7 @@ const main$9 = async () => {
|
|
|
4060
4064
|
}
|
|
4061
4065
|
listenGracefulShutdown$1();
|
|
4062
4066
|
};
|
|
4063
|
-
main$
|
|
4067
|
+
main$a();
|
|
4064
4068
|
|
|
4065
4069
|
const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
4066
4070
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -4071,7 +4075,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
|
4071
4075
|
const listenGracefulShutdown = functoolsKit.singleshot(() => {
|
|
4072
4076
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
4073
4077
|
});
|
|
4074
|
-
const main$
|
|
4078
|
+
const main$9 = async () => {
|
|
4075
4079
|
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)))) {
|
|
4076
4080
|
return;
|
|
4077
4081
|
}
|
|
@@ -4081,7 +4085,7 @@ const main$8 = async () => {
|
|
|
4081
4085
|
}
|
|
4082
4086
|
listenGracefulShutdown();
|
|
4083
4087
|
};
|
|
4084
|
-
main$
|
|
4088
|
+
main$9();
|
|
4085
4089
|
|
|
4086
4090
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
4087
4091
|
const keys = Object.keys(schema);
|
|
@@ -4103,7 +4107,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
4103
4107
|
}
|
|
4104
4108
|
return rows;
|
|
4105
4109
|
};
|
|
4106
|
-
const main$
|
|
4110
|
+
const main$8 = async () => {
|
|
4107
4111
|
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)))) {
|
|
4108
4112
|
return;
|
|
4109
4113
|
}
|
|
@@ -4203,9 +4207,9 @@ const main$7 = async () => {
|
|
|
4203
4207
|
console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
|
|
4204
4208
|
process.exit(0);
|
|
4205
4209
|
};
|
|
4206
|
-
main$
|
|
4210
|
+
main$8();
|
|
4207
4211
|
|
|
4208
|
-
const main$
|
|
4212
|
+
const main$7 = async () => {
|
|
4209
4213
|
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)))) {
|
|
4210
4214
|
return;
|
|
4211
4215
|
}
|
|
@@ -4268,9 +4272,9 @@ const main$6 = async () => {
|
|
|
4268
4272
|
};
|
|
4269
4273
|
process.on("SIGINT", beforeExit);
|
|
4270
4274
|
};
|
|
4271
|
-
main$
|
|
4275
|
+
main$7();
|
|
4272
4276
|
|
|
4273
|
-
const main$
|
|
4277
|
+
const main$6 = async () => {
|
|
4274
4278
|
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)))) {
|
|
4275
4279
|
return;
|
|
4276
4280
|
}
|
|
@@ -4351,9 +4355,9 @@ const main$5 = async () => {
|
|
|
4351
4355
|
console.log(JSON.stringify(candles, null, 2));
|
|
4352
4356
|
process.exit(0);
|
|
4353
4357
|
};
|
|
4354
|
-
main$
|
|
4358
|
+
main$6();
|
|
4355
4359
|
|
|
4356
|
-
const main$
|
|
4360
|
+
const main$5 = async () => {
|
|
4357
4361
|
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)))) {
|
|
4358
4362
|
return;
|
|
4359
4363
|
}
|
|
@@ -4467,6 +4471,221 @@ const main$4 = async () => {
|
|
|
4467
4471
|
}
|
|
4468
4472
|
process.exit(0);
|
|
4469
4473
|
};
|
|
4474
|
+
main$5();
|
|
4475
|
+
|
|
4476
|
+
const SIMULATOR_NAME = "cli_simulator";
|
|
4477
|
+
const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
|
|
4478
|
+
const validateIdea = (idea, line) => {
|
|
4479
|
+
if (typeof idea !== "object" || idea === null) {
|
|
4480
|
+
return `line ${line}: not an object`;
|
|
4481
|
+
}
|
|
4482
|
+
if (typeof idea.id !== "number") {
|
|
4483
|
+
return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
|
|
4484
|
+
}
|
|
4485
|
+
if (typeof idea.ts !== "number") {
|
|
4486
|
+
return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
|
|
4487
|
+
}
|
|
4488
|
+
if (typeof idea.symbol !== "string" || !idea.symbol) {
|
|
4489
|
+
return `line ${line}: "symbol" must be a non-empty string`;
|
|
4490
|
+
}
|
|
4491
|
+
if (!IDEA_DIRECTIONS.includes(idea.direction)) {
|
|
4492
|
+
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
4493
|
+
}
|
|
4494
|
+
if (typeof idea.author !== "string" || !idea.author) {
|
|
4495
|
+
return `line ${line}: "author" must be a non-empty string`;
|
|
4496
|
+
}
|
|
4497
|
+
return null;
|
|
4498
|
+
};
|
|
4499
|
+
const toMarkdown = (result) => {
|
|
4500
|
+
const lines = [];
|
|
4501
|
+
lines.push(`# Simulator Report — ${result.symbol}`);
|
|
4502
|
+
lines.push("");
|
|
4503
|
+
lines.push(`| Metric | Value |`);
|
|
4504
|
+
lines.push(`| --- | --- |`);
|
|
4505
|
+
lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
|
|
4506
|
+
lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
|
|
4507
|
+
lines.push(`| Authors allowed / banned | ${result.allowedAuthors.length} / ${result.bannedAuthors.length} |`);
|
|
4508
|
+
lines.push(`| Grid points | ${result.reports.length} |`);
|
|
4509
|
+
lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
|
|
4510
|
+
lines.push("");
|
|
4511
|
+
lines.push(`## Ranking winners`);
|
|
4512
|
+
lines.push("");
|
|
4513
|
+
lines.push(`| Criterion | Stop% | Trailing% | Hold | Aligned | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
|
|
4514
|
+
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
|
|
4515
|
+
for (const best of result.best) {
|
|
4516
|
+
if (!best.report) {
|
|
4517
|
+
lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — | — | — |`);
|
|
4518
|
+
continue;
|
|
4519
|
+
}
|
|
4520
|
+
const { point } = best.report;
|
|
4521
|
+
lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.trailingTakePercent} | ${point.holdMinutes / 60}h | ${point.minIdeasAligned} | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
|
|
4522
|
+
`${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
|
|
4523
|
+
}
|
|
4524
|
+
lines.push("");
|
|
4525
|
+
lines.push(`## Allowed authors (production whitelist)`);
|
|
4526
|
+
lines.push("");
|
|
4527
|
+
lines.push(`| Author | Ideas | Hits | HitRate |`);
|
|
4528
|
+
lines.push(`| --- | --- | --- | --- |`);
|
|
4529
|
+
for (const stat of result.authorStats.filter(({ banned }) => !banned)) {
|
|
4530
|
+
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
|
|
4531
|
+
}
|
|
4532
|
+
return lines.join("\n");
|
|
4533
|
+
};
|
|
4534
|
+
const main$4 = async () => {
|
|
4535
|
+
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)))) {
|
|
4536
|
+
return;
|
|
4537
|
+
}
|
|
4538
|
+
const { values, positionals } = getArgs();
|
|
4539
|
+
if (!values.simulator) {
|
|
4540
|
+
return;
|
|
4541
|
+
}
|
|
4542
|
+
const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
|
|
4543
|
+
if (!ideasPath) {
|
|
4544
|
+
console.error("Error: positional path to an ideas .jsonl file is required");
|
|
4545
|
+
process.exit(1);
|
|
4546
|
+
}
|
|
4547
|
+
let ideas = [];
|
|
4548
|
+
{
|
|
4549
|
+
let content;
|
|
4550
|
+
try {
|
|
4551
|
+
content = await fs$1.readFile(path.resolve(ideasPath), "utf-8");
|
|
4552
|
+
}
|
|
4553
|
+
catch (error) {
|
|
4554
|
+
console.error(`Error: cannot read ideas file: ${ideasPath}`);
|
|
4555
|
+
process.exit(1);
|
|
4556
|
+
}
|
|
4557
|
+
const rows = content.split("\n").filter(Boolean);
|
|
4558
|
+
if (!rows.length) {
|
|
4559
|
+
console.error(`Error: ideas file is empty: ${ideasPath}`);
|
|
4560
|
+
process.exit(1);
|
|
4561
|
+
}
|
|
4562
|
+
for (let i = 0; i < rows.length; i++) {
|
|
4563
|
+
let idea;
|
|
4564
|
+
try {
|
|
4565
|
+
idea = JSON.parse(rows[i]);
|
|
4566
|
+
}
|
|
4567
|
+
catch {
|
|
4568
|
+
console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
|
|
4569
|
+
process.exit(1);
|
|
4570
|
+
}
|
|
4571
|
+
const problem = validateIdea(idea, i + 1);
|
|
4572
|
+
if (problem) {
|
|
4573
|
+
console.error(`Error: ideas file does not match the idea structure — ${problem}`);
|
|
4574
|
+
console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
|
|
4575
|
+
process.exit(1);
|
|
4576
|
+
}
|
|
4577
|
+
ideas.push(idea);
|
|
4578
|
+
}
|
|
4579
|
+
}
|
|
4580
|
+
{
|
|
4581
|
+
const cwd = process.cwd();
|
|
4582
|
+
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
4583
|
+
}
|
|
4584
|
+
await cli.configConnectionService.loadConfig("setup.config");
|
|
4585
|
+
{
|
|
4586
|
+
const loader = await cli.configConnectionService.loadConfig("loader.config");
|
|
4587
|
+
try {
|
|
4588
|
+
if (typeof loader === "function") {
|
|
4589
|
+
await loader();
|
|
4590
|
+
}
|
|
4591
|
+
if (typeof loader?.loader === "function") {
|
|
4592
|
+
await loader.loader();
|
|
4593
|
+
}
|
|
4594
|
+
}
|
|
4595
|
+
catch (error) {
|
|
4596
|
+
console.error("Module loader failed", error);
|
|
4597
|
+
process.exit(-1);
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
await cli.moduleConnectionService.loadModule("simulator.module");
|
|
4601
|
+
{
|
|
4602
|
+
await cli.exchangeSchemaService.addSchema();
|
|
4603
|
+
await cli.symbolSchemaService.addSchema();
|
|
4604
|
+
}
|
|
4605
|
+
const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
|
|
4606
|
+
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
4607
|
+
const symbol = values.symbol || "BTCUSDT";
|
|
4608
|
+
BacktestKit.addSimulatorSchema({
|
|
4609
|
+
simulatorName: SIMULATOR_NAME,
|
|
4610
|
+
exchangeName,
|
|
4611
|
+
callbacks: {
|
|
4612
|
+
onIdeas: (symbol, ideasTotal, ideasDirectional) => {
|
|
4613
|
+
if (values.verbose) {
|
|
4614
|
+
console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
|
|
4615
|
+
}
|
|
4616
|
+
},
|
|
4617
|
+
onProfiles: (symbol, profiles, truncatedCount) => {
|
|
4618
|
+
if (values.verbose) {
|
|
4619
|
+
console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
|
|
4620
|
+
}
|
|
4621
|
+
},
|
|
4622
|
+
onAuthorsTrained: (symbol, stats, bannedIdeas) => {
|
|
4623
|
+
if (values.verbose) {
|
|
4624
|
+
console.log("onAuthorsTrained", {
|
|
4625
|
+
symbol,
|
|
4626
|
+
authors: stats.length,
|
|
4627
|
+
banned: stats.filter(({ banned }) => banned).length,
|
|
4628
|
+
bannedIdeas,
|
|
4629
|
+
});
|
|
4630
|
+
}
|
|
4631
|
+
},
|
|
4632
|
+
onGridPoint: (symbol, report) => {
|
|
4633
|
+
if (values.verbose) {
|
|
4634
|
+
console.log("onGridPoint", {
|
|
4635
|
+
symbol,
|
|
4636
|
+
point: report.point,
|
|
4637
|
+
trades: report.trades,
|
|
4638
|
+
pnl: +report.totalPnlPercent.toFixed(2),
|
|
4639
|
+
sharpe: +report.sharpe.toFixed(2),
|
|
4640
|
+
});
|
|
4641
|
+
}
|
|
4642
|
+
},
|
|
4643
|
+
onRanking: (symbol, criterion, _sorted, best) => {
|
|
4644
|
+
if (values.verbose) {
|
|
4645
|
+
console.log("onRanking", {
|
|
4646
|
+
symbol,
|
|
4647
|
+
criterion,
|
|
4648
|
+
point: best.report?.point ?? null,
|
|
4649
|
+
pnl: best.report ? +best.report.totalPnlPercent.toFixed(2) : null,
|
|
4650
|
+
});
|
|
4651
|
+
}
|
|
4652
|
+
},
|
|
4653
|
+
onDone: (symbol, result) => {
|
|
4654
|
+
if (values.verbose) {
|
|
4655
|
+
console.log("onDone", {
|
|
4656
|
+
symbol,
|
|
4657
|
+
reports: result.reports.length,
|
|
4658
|
+
allowedAuthors: result.allowedAuthors.length,
|
|
4659
|
+
bannedAuthors: result.bannedAuthors.length,
|
|
4660
|
+
});
|
|
4661
|
+
}
|
|
4662
|
+
},
|
|
4663
|
+
},
|
|
4664
|
+
});
|
|
4665
|
+
const result = await BacktestKit.Simulator.run({
|
|
4666
|
+
symbol,
|
|
4667
|
+
simulatorName: SIMULATOR_NAME,
|
|
4668
|
+
ideas,
|
|
4669
|
+
});
|
|
4670
|
+
const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
|
|
4671
|
+
const dumpDir = path.join(process.cwd(), "dump");
|
|
4672
|
+
if (values.json) {
|
|
4673
|
+
const filePath = path.resolve(dumpDir, `${dumpName}.json`);
|
|
4674
|
+
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
4675
|
+
await fs$1.writeFile(filePath, JSON.stringify(result, null, 2), "utf-8");
|
|
4676
|
+
console.log(`Saved: ${filePath}`);
|
|
4677
|
+
process.exit(0);
|
|
4678
|
+
}
|
|
4679
|
+
if (values.markdown) {
|
|
4680
|
+
const filePath = path.resolve(dumpDir, `${dumpName}.md`);
|
|
4681
|
+
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
4682
|
+
await fs$1.writeFile(filePath, toMarkdown(result), "utf-8");
|
|
4683
|
+
console.log(`Saved: ${filePath}`);
|
|
4684
|
+
process.exit(0);
|
|
4685
|
+
}
|
|
4686
|
+
console.log(toMarkdown(result));
|
|
4687
|
+
process.exit(0);
|
|
4688
|
+
};
|
|
4470
4689
|
main$4();
|
|
4471
4690
|
|
|
4472
4691
|
const __filename$2 = 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)));
|
|
@@ -4665,6 +4884,7 @@ Modes:
|
|
|
4665
4884
|
--dump Fetch and save raw OHLCV candles
|
|
4666
4885
|
--pnldebug Simulate PnL per minute for a given entry price and direction
|
|
4667
4886
|
--brokerdebug Fire a single broker commit against the live broker adapter
|
|
4887
|
+
--simulator <ideas.jsonl> Sweep exit/entry parameters over crowd ideas, train author whitelist
|
|
4668
4888
|
--flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
|
|
4669
4889
|
--init Scaffold a new project in the current directory
|
|
4670
4890
|
--docker Scaffold a Docker workspace for running strategies in a container
|
|
@@ -4785,6 +5005,33 @@ Broker debug flags (--brokerdebug):
|
|
|
4785
5005
|
Loads ./live.module, fetches the last candle for --symbol/--timeframe, and calls
|
|
4786
5006
|
the selected broker commit with synthetic payload values derived from current price.
|
|
4787
5007
|
|
|
5008
|
+
Simulator flags (--simulator):
|
|
5009
|
+
|
|
5010
|
+
--symbol <string> Trading pair to simulate (default: BTCUSDT)
|
|
5011
|
+
--exchange <string> Exchange name (default: first registered)
|
|
5012
|
+
--output <string> Output file base name (default: simulator_{SYMBOL}_{TIMESTAMP})
|
|
5013
|
+
--json Save full ISimulatorResult to ./dump/<output>.json
|
|
5014
|
+
--markdown Save summary report to ./dump/<output>.md
|
|
5015
|
+
--verbose Log every simulator lifecycle callback to the console
|
|
5016
|
+
|
|
5017
|
+
Positional: path to an ideas .jsonl file — one idea per line with the exact shape
|
|
5018
|
+
{ "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL",
|
|
5019
|
+
"author": string }. The file is validated BEFORE any work; a line that does not
|
|
5020
|
+
match the structure aborts the run with an error.
|
|
5021
|
+
|
|
5022
|
+
Runs the backtest-kit Simulator entity: one 5-day candle pass per idea, flood
|
|
5023
|
+
dedupe (one idea per author per direction per 8h), default-ban author filter
|
|
5024
|
+
(ban thresholds are grid axes), grid evaluation with time-based Sharpe/Sortino,
|
|
5025
|
+
three ranking winners. The report includes the production author whitelist.
|
|
5026
|
+
Ideas of other symbols are filtered out — one shared feed serves any --symbol.
|
|
5027
|
+
|
|
5028
|
+
No output flag → print the Markdown summary to stdout. With --verbose every
|
|
5029
|
+
simulator lifecycle callback (onIdeas, onProfiles, onAuthorsTrained, onGridPoint,
|
|
5030
|
+
onRanking, onDone) is logged to the console as it fires.
|
|
5031
|
+
|
|
5032
|
+
Module file ./modules/simulator.module is loaded automatically if it exists
|
|
5033
|
+
(register your exchange there); without it CCXT Binance is used by default.
|
|
5034
|
+
|
|
4788
5035
|
Flush flags (--flush):
|
|
4789
5036
|
|
|
4790
5037
|
One or more positional entry points. For each entry point the following
|
|
@@ -4818,6 +5065,7 @@ Module hooks (loaded automatically by each mode):
|
|
|
4818
5065
|
modules/dump.module --dump Exchange schema for candle dumps
|
|
4819
5066
|
modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
|
|
4820
5067
|
modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
|
|
5068
|
+
modules/simulator.module --simulator Exchange schema for crowd-ideas simulation
|
|
4821
5069
|
|
|
4822
5070
|
--flush has no associated module. It only removes dump subdirectories.
|
|
4823
5071
|
|
|
@@ -4846,6 +5094,8 @@ Examples:
|
|
|
4846
5094
|
node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
|
|
4847
5095
|
node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
|
|
4848
5096
|
node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
|
|
5097
|
+
node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/ts-ideas.normalized.jsonl
|
|
5098
|
+
node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_sweep ./assets/ts-ideas.normalized.jsonl
|
|
4849
5099
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
|
|
4850
5100
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
|
|
4851
5101
|
node ${ENTRY_PATH} --init --output my-trading-bot
|
|
@@ -4859,7 +5109,7 @@ const main$1 = async () => {
|
|
|
4859
5109
|
if (!values.help) {
|
|
4860
5110
|
return;
|
|
4861
5111
|
}
|
|
4862
|
-
process.stdout.write(`@backtest-kit/cli ${"16.
|
|
5112
|
+
process.stdout.write(`@backtest-kit/cli ${"16.4.0"}\n\n`);
|
|
4863
5113
|
process.stdout.write(HELP_TEXT);
|
|
4864
5114
|
process.exit(0);
|
|
4865
5115
|
};
|
|
@@ -4873,7 +5123,7 @@ const main = async () => {
|
|
|
4873
5123
|
if (!values.version) {
|
|
4874
5124
|
return;
|
|
4875
5125
|
}
|
|
4876
|
-
process.stdout.write(`@backtest-kit/cli ${"16.
|
|
5126
|
+
process.stdout.write(`@backtest-kit/cli ${"16.4.0"}\n`);
|
|
4877
5127
|
process.exit(0);
|
|
4878
5128
|
};
|
|
4879
5129
|
main();
|
package/build/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as BacktestKit from 'backtest-kit';
|
|
3
|
-
import { setConfig, listenExit, emitters, Notification, Cron, Recent, Storage, Markdown, Report, Dump, State, Memory, MarkdownWriter, ReportWriter, PersistSignalAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistPartialAdapter, PersistBreakevenAdapter, PersistCandleAdapter, PersistStorageAdapter, PersistNotificationAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistIntervalAdapter, PersistMemoryAdapter, PersistRecentAdapter, PersistStateAdapter, PersistSessionAdapter, PersistStrategyAdapter, Log, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, RecentLive, RecentBacktest, Broker, SessionLive, SessionBacktest, MemoryLive, MemoryBacktest, StateLive, StateBacktest, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, System, Cache, Interval, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenSync, listenSignalNotify, Exchange } from 'backtest-kit';
|
|
3
|
+
import { setConfig, listenExit, emitters, Notification, Cron, Recent, Storage, Markdown, Report, Dump, State, Memory, MarkdownWriter, ReportWriter, PersistSignalAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistPartialAdapter, PersistBreakevenAdapter, PersistCandleAdapter, PersistStorageAdapter, PersistNotificationAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistIntervalAdapter, PersistMemoryAdapter, PersistRecentAdapter, PersistStateAdapter, PersistSessionAdapter, PersistStrategyAdapter, Log, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, RecentLive, RecentBacktest, Broker, SessionLive, SessionBacktest, MemoryLive, MemoryBacktest, StateLive, StateBacktest, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, System, Cache, Interval, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenSync, listenSignalNotify, Exchange, addSimulatorSchema, Simulator } from 'backtest-kit';
|
|
4
4
|
import { singleshot, sleep, getErrorMessage, errorData, BehaviorSubject, Subject, str, compose, createAwaiter, execpool, queued, randomString, TIMEOUT_SYMBOL, typo, retry, trycatch, memoize, isObject } from 'functools-kit';
|
|
5
5
|
import { exec, spawn } from 'child_process';
|
|
6
6
|
import fs, { constants, realpathSync } from 'fs';
|
|
@@ -28,7 +28,7 @@ import Module, { 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
|
+
import { run as run$1, Code, toMarkdown as toMarkdown$1 } from '@backtest-kit/pinets';
|
|
32
32
|
import * as BacktestKitSignals from '@backtest-kit/signals';
|
|
33
33
|
import open from 'open';
|
|
34
34
|
|
|
@@ -208,14 +208,14 @@ const getReadySubject = singleshot(() => {
|
|
|
208
208
|
return new Subject();
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
-
const main$
|
|
211
|
+
const main$j = () => {
|
|
212
212
|
const entrySubject = getEntrySubject();
|
|
213
213
|
entrySubject.subscribe(async (path) => {
|
|
214
214
|
console.log("Running", path);
|
|
215
215
|
await getReadySubject().next();
|
|
216
216
|
});
|
|
217
217
|
};
|
|
218
|
-
main$
|
|
218
|
+
main$j();
|
|
219
219
|
|
|
220
220
|
const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
|
|
221
221
|
function dumpStackTrace(error) {
|
|
@@ -866,6 +866,10 @@ const getArgs = singleshot(() => {
|
|
|
866
866
|
type: "boolean",
|
|
867
867
|
default: false,
|
|
868
868
|
},
|
|
869
|
+
simulator: {
|
|
870
|
+
type: "boolean",
|
|
871
|
+
default: false,
|
|
872
|
+
},
|
|
869
873
|
commit: {
|
|
870
874
|
type: "string",
|
|
871
875
|
default: "",
|
|
@@ -3502,14 +3506,14 @@ const cli = {
|
|
|
3502
3506
|
};
|
|
3503
3507
|
init();
|
|
3504
3508
|
|
|
3505
|
-
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "flush", "init", "docker", "help", "version"];
|
|
3509
|
+
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "flush", "init", "docker", "help", "version"];
|
|
3506
3510
|
const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
|
|
3507
3511
|
const HELP_TEXT$1 = `
|
|
3508
3512
|
Example:
|
|
3509
3513
|
|
|
3510
3514
|
node ${ENTRY_PATH$1} --help
|
|
3511
3515
|
`.trimStart();
|
|
3512
|
-
const main$
|
|
3516
|
+
const main$i = async () => {
|
|
3513
3517
|
if (!getEntry(import.meta.url)) {
|
|
3514
3518
|
return;
|
|
3515
3519
|
}
|
|
@@ -3517,14 +3521,14 @@ const main$h = async () => {
|
|
|
3517
3521
|
if (MODES.some((mode) => values[mode])) {
|
|
3518
3522
|
return;
|
|
3519
3523
|
}
|
|
3520
|
-
process.stdout.write(`@backtest-kit/cli ${"16.
|
|
3524
|
+
process.stdout.write(`@backtest-kit/cli ${"16.4.0"}\n`);
|
|
3521
3525
|
process.stdout.write("\n");
|
|
3522
3526
|
process.stdout.write(`Run with --help to see available commands.\n`);
|
|
3523
3527
|
process.stdout.write("\n");
|
|
3524
3528
|
process.stdout.write(HELP_TEXT$1);
|
|
3525
3529
|
process.exit(0);
|
|
3526
3530
|
};
|
|
3527
|
-
main$
|
|
3531
|
+
main$i();
|
|
3528
3532
|
|
|
3529
3533
|
const notifyShutdown = singleshot(async () => {
|
|
3530
3534
|
console.log("Graceful shutdown initiated.");
|
|
@@ -3540,7 +3544,7 @@ const flush = async (entryPoint) => {
|
|
|
3540
3544
|
console.log(`Removed: ${target}`);
|
|
3541
3545
|
}
|
|
3542
3546
|
};
|
|
3543
|
-
const main$
|
|
3547
|
+
const main$h = async () => {
|
|
3544
3548
|
if (!getEntry(import.meta.url)) {
|
|
3545
3549
|
return;
|
|
3546
3550
|
}
|
|
@@ -3557,7 +3561,7 @@ const main$g = async () => {
|
|
|
3557
3561
|
}
|
|
3558
3562
|
process.exit(0);
|
|
3559
3563
|
};
|
|
3560
|
-
main$
|
|
3564
|
+
main$h();
|
|
3561
3565
|
|
|
3562
3566
|
const BEFORE_EXIT_FN$6 = singleshot(async () => {
|
|
3563
3567
|
process.off("SIGINT", BEFORE_EXIT_FN$6);
|
|
@@ -3580,7 +3584,7 @@ const BEFORE_EXIT_FN$6 = singleshot(async () => {
|
|
|
3580
3584
|
const listenGracefulShutdown$6 = singleshot(() => {
|
|
3581
3585
|
process.on("SIGINT", BEFORE_EXIT_FN$6);
|
|
3582
3586
|
});
|
|
3583
|
-
const main$
|
|
3587
|
+
const main$g = async () => {
|
|
3584
3588
|
if (!getEntry(import.meta.url)) {
|
|
3585
3589
|
return;
|
|
3586
3590
|
}
|
|
@@ -3598,7 +3602,7 @@ const main$f = async () => {
|
|
|
3598
3602
|
await cli.backtestMainService.connect();
|
|
3599
3603
|
listenGracefulShutdown$6();
|
|
3600
3604
|
};
|
|
3601
|
-
main$
|
|
3605
|
+
main$g();
|
|
3602
3606
|
|
|
3603
3607
|
const BEFORE_EXIT_FN$5 = singleshot(async () => {
|
|
3604
3608
|
process.off("SIGINT", BEFORE_EXIT_FN$5);
|
|
@@ -3617,7 +3621,7 @@ const BEFORE_EXIT_FN$5 = singleshot(async () => {
|
|
|
3617
3621
|
const listenGracefulShutdown$5 = singleshot(() => {
|
|
3618
3622
|
process.on("SIGINT", BEFORE_EXIT_FN$5);
|
|
3619
3623
|
});
|
|
3620
|
-
const main$
|
|
3624
|
+
const main$f = async () => {
|
|
3621
3625
|
if (!getEntry(import.meta.url)) {
|
|
3622
3626
|
return;
|
|
3623
3627
|
}
|
|
@@ -3636,7 +3640,7 @@ const main$e = async () => {
|
|
|
3636
3640
|
listenGracefulShutdown$5();
|
|
3637
3641
|
await cli.walkerMainService.connect();
|
|
3638
3642
|
};
|
|
3639
|
-
main$
|
|
3643
|
+
main$f();
|
|
3640
3644
|
|
|
3641
3645
|
const BEFORE_EXIT_FN$4 = singleshot(async () => {
|
|
3642
3646
|
process.off("SIGINT", BEFORE_EXIT_FN$4);
|
|
@@ -3658,7 +3662,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
|
|
|
3658
3662
|
const listenGracefulShutdown$4 = singleshot(() => {
|
|
3659
3663
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
3660
3664
|
});
|
|
3661
|
-
const main$
|
|
3665
|
+
const main$e = async () => {
|
|
3662
3666
|
if (!getEntry(import.meta.url)) {
|
|
3663
3667
|
return;
|
|
3664
3668
|
}
|
|
@@ -3672,7 +3676,7 @@ const main$d = async () => {
|
|
|
3672
3676
|
await cli.paperMainService.connect();
|
|
3673
3677
|
listenGracefulShutdown$4();
|
|
3674
3678
|
};
|
|
3675
|
-
main$
|
|
3679
|
+
main$e();
|
|
3676
3680
|
|
|
3677
3681
|
const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
3678
3682
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -3694,7 +3698,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
|
3694
3698
|
const listenGracefulShutdown$3 = singleshot(() => {
|
|
3695
3699
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
3696
3700
|
});
|
|
3697
|
-
const main$
|
|
3701
|
+
const main$d = async () => {
|
|
3698
3702
|
if (!getEntry(import.meta.url)) {
|
|
3699
3703
|
return;
|
|
3700
3704
|
}
|
|
@@ -3708,7 +3712,7 @@ const main$c = async () => {
|
|
|
3708
3712
|
await cli.liveMainService.connect();
|
|
3709
3713
|
listenGracefulShutdown$3();
|
|
3710
3714
|
};
|
|
3711
|
-
main$
|
|
3715
|
+
main$d();
|
|
3712
3716
|
|
|
3713
3717
|
const stopBacktestList$1 = async () => {
|
|
3714
3718
|
for (const item of await Backtest.list()) {
|
|
@@ -3772,7 +3776,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
|
|
|
3772
3776
|
const listenGracefulShutdown$2 = singleshot(() => {
|
|
3773
3777
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
3774
3778
|
});
|
|
3775
|
-
const main$
|
|
3779
|
+
const main$c = async () => {
|
|
3776
3780
|
if (!getEntry(import.meta.url)) {
|
|
3777
3781
|
return;
|
|
3778
3782
|
}
|
|
@@ -3828,7 +3832,7 @@ const main$b = async () => {
|
|
|
3828
3832
|
listenGracefulShutdown$2();
|
|
3829
3833
|
await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
|
|
3830
3834
|
};
|
|
3831
|
-
main$
|
|
3835
|
+
main$c();
|
|
3832
3836
|
|
|
3833
3837
|
const MODE_MODULE = {
|
|
3834
3838
|
backtest: "backtest.module",
|
|
@@ -3931,7 +3935,7 @@ const createGracefulShutdown = (mode) => {
|
|
|
3931
3935
|
process.on("SIGINT", handler);
|
|
3932
3936
|
});
|
|
3933
3937
|
};
|
|
3934
|
-
const main$
|
|
3938
|
+
const main$b = async () => {
|
|
3935
3939
|
if (!getEntry(import.meta.url)) {
|
|
3936
3940
|
return;
|
|
3937
3941
|
}
|
|
@@ -4007,7 +4011,7 @@ const main$a = async () => {
|
|
|
4007
4011
|
}
|
|
4008
4012
|
await getEntrySubject().next(absolutePath);
|
|
4009
4013
|
};
|
|
4010
|
-
main$
|
|
4014
|
+
main$b();
|
|
4011
4015
|
|
|
4012
4016
|
const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
4013
4017
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -4018,7 +4022,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
|
4018
4022
|
const listenGracefulShutdown$1 = singleshot(() => {
|
|
4019
4023
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
4020
4024
|
});
|
|
4021
|
-
const main$
|
|
4025
|
+
const main$a = async () => {
|
|
4022
4026
|
if (!getEntry(import.meta.url)) {
|
|
4023
4027
|
return;
|
|
4024
4028
|
}
|
|
@@ -4028,7 +4032,7 @@ const main$9 = async () => {
|
|
|
4028
4032
|
}
|
|
4029
4033
|
listenGracefulShutdown$1();
|
|
4030
4034
|
};
|
|
4031
|
-
main$
|
|
4035
|
+
main$a();
|
|
4032
4036
|
|
|
4033
4037
|
const BEFORE_EXIT_FN = singleshot(async () => {
|
|
4034
4038
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -4039,7 +4043,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
|
|
|
4039
4043
|
const listenGracefulShutdown = singleshot(() => {
|
|
4040
4044
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
4041
4045
|
});
|
|
4042
|
-
const main$
|
|
4046
|
+
const main$9 = async () => {
|
|
4043
4047
|
if (!getEntry(import.meta.url)) {
|
|
4044
4048
|
return;
|
|
4045
4049
|
}
|
|
@@ -4049,7 +4053,7 @@ const main$8 = async () => {
|
|
|
4049
4053
|
}
|
|
4050
4054
|
listenGracefulShutdown();
|
|
4051
4055
|
};
|
|
4052
|
-
main$
|
|
4056
|
+
main$9();
|
|
4053
4057
|
|
|
4054
4058
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
4055
4059
|
const keys = Object.keys(schema);
|
|
@@ -4071,7 +4075,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
4071
4075
|
}
|
|
4072
4076
|
return rows;
|
|
4073
4077
|
};
|
|
4074
|
-
const main$
|
|
4078
|
+
const main$8 = async () => {
|
|
4075
4079
|
if (!getEntry(import.meta.url)) {
|
|
4076
4080
|
return;
|
|
4077
4081
|
}
|
|
@@ -4163,17 +4167,17 @@ const main$7 = async () => {
|
|
|
4163
4167
|
if (values.markdown) {
|
|
4164
4168
|
const filePath = resolve(dumpDir, `${dumpName}.md`);
|
|
4165
4169
|
await mkdir(dumpDir, { recursive: true });
|
|
4166
|
-
await writeFile(filePath, await toMarkdown(signalId, plots, signalSchema), "utf-8");
|
|
4170
|
+
await writeFile(filePath, await toMarkdown$1(signalId, plots, signalSchema), "utf-8");
|
|
4167
4171
|
console.log(`Saved: ${filePath}`);
|
|
4168
4172
|
process.exit(0);
|
|
4169
4173
|
return;
|
|
4170
4174
|
}
|
|
4171
|
-
console.log(await toMarkdown(signalId, plots, signalSchema));
|
|
4175
|
+
console.log(await toMarkdown$1(signalId, plots, signalSchema));
|
|
4172
4176
|
process.exit(0);
|
|
4173
4177
|
};
|
|
4174
|
-
main$
|
|
4178
|
+
main$8();
|
|
4175
4179
|
|
|
4176
|
-
const main$
|
|
4180
|
+
const main$7 = async () => {
|
|
4177
4181
|
if (!getEntry(import.meta.url)) {
|
|
4178
4182
|
return;
|
|
4179
4183
|
}
|
|
@@ -4236,9 +4240,9 @@ const main$6 = async () => {
|
|
|
4236
4240
|
};
|
|
4237
4241
|
process.on("SIGINT", beforeExit);
|
|
4238
4242
|
};
|
|
4239
|
-
main$
|
|
4243
|
+
main$7();
|
|
4240
4244
|
|
|
4241
|
-
const main$
|
|
4245
|
+
const main$6 = async () => {
|
|
4242
4246
|
if (!getEntry(import.meta.url)) {
|
|
4243
4247
|
return;
|
|
4244
4248
|
}
|
|
@@ -4319,9 +4323,9 @@ const main$5 = async () => {
|
|
|
4319
4323
|
console.log(JSON.stringify(candles, null, 2));
|
|
4320
4324
|
process.exit(0);
|
|
4321
4325
|
};
|
|
4322
|
-
main$
|
|
4326
|
+
main$6();
|
|
4323
4327
|
|
|
4324
|
-
const main$
|
|
4328
|
+
const main$5 = async () => {
|
|
4325
4329
|
if (!getEntry(import.meta.url)) {
|
|
4326
4330
|
return;
|
|
4327
4331
|
}
|
|
@@ -4435,6 +4439,221 @@ const main$4 = async () => {
|
|
|
4435
4439
|
}
|
|
4436
4440
|
process.exit(0);
|
|
4437
4441
|
};
|
|
4442
|
+
main$5();
|
|
4443
|
+
|
|
4444
|
+
const SIMULATOR_NAME = "cli_simulator";
|
|
4445
|
+
const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
|
|
4446
|
+
const validateIdea = (idea, line) => {
|
|
4447
|
+
if (typeof idea !== "object" || idea === null) {
|
|
4448
|
+
return `line ${line}: not an object`;
|
|
4449
|
+
}
|
|
4450
|
+
if (typeof idea.id !== "number") {
|
|
4451
|
+
return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
|
|
4452
|
+
}
|
|
4453
|
+
if (typeof idea.ts !== "number") {
|
|
4454
|
+
return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
|
|
4455
|
+
}
|
|
4456
|
+
if (typeof idea.symbol !== "string" || !idea.symbol) {
|
|
4457
|
+
return `line ${line}: "symbol" must be a non-empty string`;
|
|
4458
|
+
}
|
|
4459
|
+
if (!IDEA_DIRECTIONS.includes(idea.direction)) {
|
|
4460
|
+
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
4461
|
+
}
|
|
4462
|
+
if (typeof idea.author !== "string" || !idea.author) {
|
|
4463
|
+
return `line ${line}: "author" must be a non-empty string`;
|
|
4464
|
+
}
|
|
4465
|
+
return null;
|
|
4466
|
+
};
|
|
4467
|
+
const toMarkdown = (result) => {
|
|
4468
|
+
const lines = [];
|
|
4469
|
+
lines.push(`# Simulator Report — ${result.symbol}`);
|
|
4470
|
+
lines.push("");
|
|
4471
|
+
lines.push(`| Metric | Value |`);
|
|
4472
|
+
lines.push(`| --- | --- |`);
|
|
4473
|
+
lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
|
|
4474
|
+
lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
|
|
4475
|
+
lines.push(`| Authors allowed / banned | ${result.allowedAuthors.length} / ${result.bannedAuthors.length} |`);
|
|
4476
|
+
lines.push(`| Grid points | ${result.reports.length} |`);
|
|
4477
|
+
lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
|
|
4478
|
+
lines.push("");
|
|
4479
|
+
lines.push(`## Ranking winners`);
|
|
4480
|
+
lines.push("");
|
|
4481
|
+
lines.push(`| Criterion | Stop% | Trailing% | Hold | Aligned | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
|
|
4482
|
+
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
|
|
4483
|
+
for (const best of result.best) {
|
|
4484
|
+
if (!best.report) {
|
|
4485
|
+
lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — | — | — |`);
|
|
4486
|
+
continue;
|
|
4487
|
+
}
|
|
4488
|
+
const { point } = best.report;
|
|
4489
|
+
lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.trailingTakePercent} | ${point.holdMinutes / 60}h | ${point.minIdeasAligned} | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
|
|
4490
|
+
`${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
|
|
4491
|
+
}
|
|
4492
|
+
lines.push("");
|
|
4493
|
+
lines.push(`## Allowed authors (production whitelist)`);
|
|
4494
|
+
lines.push("");
|
|
4495
|
+
lines.push(`| Author | Ideas | Hits | HitRate |`);
|
|
4496
|
+
lines.push(`| --- | --- | --- | --- |`);
|
|
4497
|
+
for (const stat of result.authorStats.filter(({ banned }) => !banned)) {
|
|
4498
|
+
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
|
|
4499
|
+
}
|
|
4500
|
+
return lines.join("\n");
|
|
4501
|
+
};
|
|
4502
|
+
const main$4 = async () => {
|
|
4503
|
+
if (!getEntry(import.meta.url)) {
|
|
4504
|
+
return;
|
|
4505
|
+
}
|
|
4506
|
+
const { values, positionals } = getArgs();
|
|
4507
|
+
if (!values.simulator) {
|
|
4508
|
+
return;
|
|
4509
|
+
}
|
|
4510
|
+
const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
|
|
4511
|
+
if (!ideasPath) {
|
|
4512
|
+
console.error("Error: positional path to an ideas .jsonl file is required");
|
|
4513
|
+
process.exit(1);
|
|
4514
|
+
}
|
|
4515
|
+
let ideas = [];
|
|
4516
|
+
{
|
|
4517
|
+
let content;
|
|
4518
|
+
try {
|
|
4519
|
+
content = await readFile(resolve(ideasPath), "utf-8");
|
|
4520
|
+
}
|
|
4521
|
+
catch (error) {
|
|
4522
|
+
console.error(`Error: cannot read ideas file: ${ideasPath}`);
|
|
4523
|
+
process.exit(1);
|
|
4524
|
+
}
|
|
4525
|
+
const rows = content.split("\n").filter(Boolean);
|
|
4526
|
+
if (!rows.length) {
|
|
4527
|
+
console.error(`Error: ideas file is empty: ${ideasPath}`);
|
|
4528
|
+
process.exit(1);
|
|
4529
|
+
}
|
|
4530
|
+
for (let i = 0; i < rows.length; i++) {
|
|
4531
|
+
let idea;
|
|
4532
|
+
try {
|
|
4533
|
+
idea = JSON.parse(rows[i]);
|
|
4534
|
+
}
|
|
4535
|
+
catch {
|
|
4536
|
+
console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
|
|
4537
|
+
process.exit(1);
|
|
4538
|
+
}
|
|
4539
|
+
const problem = validateIdea(idea, i + 1);
|
|
4540
|
+
if (problem) {
|
|
4541
|
+
console.error(`Error: ideas file does not match the idea structure — ${problem}`);
|
|
4542
|
+
console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
|
|
4543
|
+
process.exit(1);
|
|
4544
|
+
}
|
|
4545
|
+
ideas.push(idea);
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
{
|
|
4549
|
+
const cwd = process.cwd();
|
|
4550
|
+
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
4551
|
+
}
|
|
4552
|
+
await cli.configConnectionService.loadConfig("setup.config");
|
|
4553
|
+
{
|
|
4554
|
+
const loader = await cli.configConnectionService.loadConfig("loader.config");
|
|
4555
|
+
try {
|
|
4556
|
+
if (typeof loader === "function") {
|
|
4557
|
+
await loader();
|
|
4558
|
+
}
|
|
4559
|
+
if (typeof loader?.loader === "function") {
|
|
4560
|
+
await loader.loader();
|
|
4561
|
+
}
|
|
4562
|
+
}
|
|
4563
|
+
catch (error) {
|
|
4564
|
+
console.error("Module loader failed", error);
|
|
4565
|
+
process.exit(-1);
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
await cli.moduleConnectionService.loadModule("simulator.module");
|
|
4569
|
+
{
|
|
4570
|
+
await cli.exchangeSchemaService.addSchema();
|
|
4571
|
+
await cli.symbolSchemaService.addSchema();
|
|
4572
|
+
}
|
|
4573
|
+
const [defaultExchangeName = null] = await listExchangeSchema();
|
|
4574
|
+
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
4575
|
+
const symbol = values.symbol || "BTCUSDT";
|
|
4576
|
+
addSimulatorSchema({
|
|
4577
|
+
simulatorName: SIMULATOR_NAME,
|
|
4578
|
+
exchangeName,
|
|
4579
|
+
callbacks: {
|
|
4580
|
+
onIdeas: (symbol, ideasTotal, ideasDirectional) => {
|
|
4581
|
+
if (values.verbose) {
|
|
4582
|
+
console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
|
|
4583
|
+
}
|
|
4584
|
+
},
|
|
4585
|
+
onProfiles: (symbol, profiles, truncatedCount) => {
|
|
4586
|
+
if (values.verbose) {
|
|
4587
|
+
console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
|
|
4588
|
+
}
|
|
4589
|
+
},
|
|
4590
|
+
onAuthorsTrained: (symbol, stats, bannedIdeas) => {
|
|
4591
|
+
if (values.verbose) {
|
|
4592
|
+
console.log("onAuthorsTrained", {
|
|
4593
|
+
symbol,
|
|
4594
|
+
authors: stats.length,
|
|
4595
|
+
banned: stats.filter(({ banned }) => banned).length,
|
|
4596
|
+
bannedIdeas,
|
|
4597
|
+
});
|
|
4598
|
+
}
|
|
4599
|
+
},
|
|
4600
|
+
onGridPoint: (symbol, report) => {
|
|
4601
|
+
if (values.verbose) {
|
|
4602
|
+
console.log("onGridPoint", {
|
|
4603
|
+
symbol,
|
|
4604
|
+
point: report.point,
|
|
4605
|
+
trades: report.trades,
|
|
4606
|
+
pnl: +report.totalPnlPercent.toFixed(2),
|
|
4607
|
+
sharpe: +report.sharpe.toFixed(2),
|
|
4608
|
+
});
|
|
4609
|
+
}
|
|
4610
|
+
},
|
|
4611
|
+
onRanking: (symbol, criterion, _sorted, best) => {
|
|
4612
|
+
if (values.verbose) {
|
|
4613
|
+
console.log("onRanking", {
|
|
4614
|
+
symbol,
|
|
4615
|
+
criterion,
|
|
4616
|
+
point: best.report?.point ?? null,
|
|
4617
|
+
pnl: best.report ? +best.report.totalPnlPercent.toFixed(2) : null,
|
|
4618
|
+
});
|
|
4619
|
+
}
|
|
4620
|
+
},
|
|
4621
|
+
onDone: (symbol, result) => {
|
|
4622
|
+
if (values.verbose) {
|
|
4623
|
+
console.log("onDone", {
|
|
4624
|
+
symbol,
|
|
4625
|
+
reports: result.reports.length,
|
|
4626
|
+
allowedAuthors: result.allowedAuthors.length,
|
|
4627
|
+
bannedAuthors: result.bannedAuthors.length,
|
|
4628
|
+
});
|
|
4629
|
+
}
|
|
4630
|
+
},
|
|
4631
|
+
},
|
|
4632
|
+
});
|
|
4633
|
+
const result = await Simulator.run({
|
|
4634
|
+
symbol,
|
|
4635
|
+
simulatorName: SIMULATOR_NAME,
|
|
4636
|
+
ideas,
|
|
4637
|
+
});
|
|
4638
|
+
const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
|
|
4639
|
+
const dumpDir = join(process.cwd(), "dump");
|
|
4640
|
+
if (values.json) {
|
|
4641
|
+
const filePath = resolve(dumpDir, `${dumpName}.json`);
|
|
4642
|
+
await mkdir(dumpDir, { recursive: true });
|
|
4643
|
+
await writeFile(filePath, JSON.stringify(result, null, 2), "utf-8");
|
|
4644
|
+
console.log(`Saved: ${filePath}`);
|
|
4645
|
+
process.exit(0);
|
|
4646
|
+
}
|
|
4647
|
+
if (values.markdown) {
|
|
4648
|
+
const filePath = resolve(dumpDir, `${dumpName}.md`);
|
|
4649
|
+
await mkdir(dumpDir, { recursive: true });
|
|
4650
|
+
await writeFile(filePath, toMarkdown(result), "utf-8");
|
|
4651
|
+
console.log(`Saved: ${filePath}`);
|
|
4652
|
+
process.exit(0);
|
|
4653
|
+
}
|
|
4654
|
+
console.log(toMarkdown(result));
|
|
4655
|
+
process.exit(0);
|
|
4656
|
+
};
|
|
4438
4657
|
main$4();
|
|
4439
4658
|
|
|
4440
4659
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
@@ -4633,6 +4852,7 @@ Modes:
|
|
|
4633
4852
|
--dump Fetch and save raw OHLCV candles
|
|
4634
4853
|
--pnldebug Simulate PnL per minute for a given entry price and direction
|
|
4635
4854
|
--brokerdebug Fire a single broker commit against the live broker adapter
|
|
4855
|
+
--simulator <ideas.jsonl> Sweep exit/entry parameters over crowd ideas, train author whitelist
|
|
4636
4856
|
--flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
|
|
4637
4857
|
--init Scaffold a new project in the current directory
|
|
4638
4858
|
--docker Scaffold a Docker workspace for running strategies in a container
|
|
@@ -4753,6 +4973,33 @@ Broker debug flags (--brokerdebug):
|
|
|
4753
4973
|
Loads ./live.module, fetches the last candle for --symbol/--timeframe, and calls
|
|
4754
4974
|
the selected broker commit with synthetic payload values derived from current price.
|
|
4755
4975
|
|
|
4976
|
+
Simulator flags (--simulator):
|
|
4977
|
+
|
|
4978
|
+
--symbol <string> Trading pair to simulate (default: BTCUSDT)
|
|
4979
|
+
--exchange <string> Exchange name (default: first registered)
|
|
4980
|
+
--output <string> Output file base name (default: simulator_{SYMBOL}_{TIMESTAMP})
|
|
4981
|
+
--json Save full ISimulatorResult to ./dump/<output>.json
|
|
4982
|
+
--markdown Save summary report to ./dump/<output>.md
|
|
4983
|
+
--verbose Log every simulator lifecycle callback to the console
|
|
4984
|
+
|
|
4985
|
+
Positional: path to an ideas .jsonl file — one idea per line with the exact shape
|
|
4986
|
+
{ "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL",
|
|
4987
|
+
"author": string }. The file is validated BEFORE any work; a line that does not
|
|
4988
|
+
match the structure aborts the run with an error.
|
|
4989
|
+
|
|
4990
|
+
Runs the backtest-kit Simulator entity: one 5-day candle pass per idea, flood
|
|
4991
|
+
dedupe (one idea per author per direction per 8h), default-ban author filter
|
|
4992
|
+
(ban thresholds are grid axes), grid evaluation with time-based Sharpe/Sortino,
|
|
4993
|
+
three ranking winners. The report includes the production author whitelist.
|
|
4994
|
+
Ideas of other symbols are filtered out — one shared feed serves any --symbol.
|
|
4995
|
+
|
|
4996
|
+
No output flag → print the Markdown summary to stdout. With --verbose every
|
|
4997
|
+
simulator lifecycle callback (onIdeas, onProfiles, onAuthorsTrained, onGridPoint,
|
|
4998
|
+
onRanking, onDone) is logged to the console as it fires.
|
|
4999
|
+
|
|
5000
|
+
Module file ./modules/simulator.module is loaded automatically if it exists
|
|
5001
|
+
(register your exchange there); without it CCXT Binance is used by default.
|
|
5002
|
+
|
|
4756
5003
|
Flush flags (--flush):
|
|
4757
5004
|
|
|
4758
5005
|
One or more positional entry points. For each entry point the following
|
|
@@ -4786,6 +5033,7 @@ Module hooks (loaded automatically by each mode):
|
|
|
4786
5033
|
modules/dump.module --dump Exchange schema for candle dumps
|
|
4787
5034
|
modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
|
|
4788
5035
|
modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
|
|
5036
|
+
modules/simulator.module --simulator Exchange schema for crowd-ideas simulation
|
|
4789
5037
|
|
|
4790
5038
|
--flush has no associated module. It only removes dump subdirectories.
|
|
4791
5039
|
|
|
@@ -4814,6 +5062,8 @@ Examples:
|
|
|
4814
5062
|
node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
|
|
4815
5063
|
node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
|
|
4816
5064
|
node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
|
|
5065
|
+
node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/ts-ideas.normalized.jsonl
|
|
5066
|
+
node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_sweep ./assets/ts-ideas.normalized.jsonl
|
|
4817
5067
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
|
|
4818
5068
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
|
|
4819
5069
|
node ${ENTRY_PATH} --init --output my-trading-bot
|
|
@@ -4827,7 +5077,7 @@ const main$1 = async () => {
|
|
|
4827
5077
|
if (!values.help) {
|
|
4828
5078
|
return;
|
|
4829
5079
|
}
|
|
4830
|
-
process.stdout.write(`@backtest-kit/cli ${"16.
|
|
5080
|
+
process.stdout.write(`@backtest-kit/cli ${"16.4.0"}\n\n`);
|
|
4831
5081
|
process.stdout.write(HELP_TEXT);
|
|
4832
5082
|
process.exit(0);
|
|
4833
5083
|
};
|
|
@@ -4841,7 +5091,7 @@ const main = async () => {
|
|
|
4841
5091
|
if (!values.version) {
|
|
4842
5092
|
return;
|
|
4843
5093
|
}
|
|
4844
|
-
process.stdout.write(`@backtest-kit/cli ${"16.
|
|
5094
|
+
process.stdout.write(`@backtest-kit/cli ${"16.4.0"}\n`);
|
|
4845
5095
|
process.exit(0);
|
|
4846
5096
|
};
|
|
4847
5097
|
main();
|
package/docker/package.json
CHANGED
|
@@ -15,17 +15,17 @@
|
|
|
15
15
|
"@types/node": "25.6.0"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@backtest-kit/cli": "16.
|
|
19
|
-
"@backtest-kit/graph": "16.
|
|
20
|
-
"@backtest-kit/pinets": "16.
|
|
21
|
-
"@backtest-kit/signals": "16.
|
|
22
|
-
"@backtest-kit/ui": "16.
|
|
18
|
+
"@backtest-kit/cli": "16.4.0",
|
|
19
|
+
"@backtest-kit/graph": "16.4.0",
|
|
20
|
+
"@backtest-kit/pinets": "16.4.0",
|
|
21
|
+
"@backtest-kit/signals": "16.4.0",
|
|
22
|
+
"@backtest-kit/ui": "16.4.0",
|
|
23
23
|
"@tavily/core": "0.7.2",
|
|
24
24
|
"@tensorflow/tfjs": "4.22.0",
|
|
25
25
|
"@tensorflow/tfjs-backend-wasm": "4.22.0",
|
|
26
26
|
"@tensorflow/tfjs-core": "4.22.0",
|
|
27
27
|
"agent-swarm-kit": "6.0.0",
|
|
28
|
-
"backtest-kit": "16.
|
|
28
|
+
"backtest-kit": "16.4.0",
|
|
29
29
|
"dayjs": "1.11.20",
|
|
30
30
|
"functools-kit": "5.0.0",
|
|
31
31
|
"garch": "2.0.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backtest-kit/cli",
|
|
3
|
-
"version": "16.
|
|
3
|
+
"version": "16.4.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",
|
|
@@ -63,11 +63,11 @@
|
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@babel/plugin-transform-modules-umd": "7.27.1",
|
|
65
65
|
"@babel/standalone": "7.29.1",
|
|
66
|
-
"@backtest-kit/graph": "16.
|
|
67
|
-
"@backtest-kit/ollama": "16.
|
|
68
|
-
"@backtest-kit/pinets": "16.
|
|
69
|
-
"@backtest-kit/signals": "16.
|
|
70
|
-
"@backtest-kit/ui": "16.
|
|
66
|
+
"@backtest-kit/graph": "16.4.0",
|
|
67
|
+
"@backtest-kit/ollama": "16.4.0",
|
|
68
|
+
"@backtest-kit/pinets": "16.4.0",
|
|
69
|
+
"@backtest-kit/signals": "16.4.0",
|
|
70
|
+
"@backtest-kit/ui": "16.4.0",
|
|
71
71
|
"@rollup/plugin-replace": "6.0.3",
|
|
72
72
|
"@rollup/plugin-typescript": "11.1.6",
|
|
73
73
|
"@types/image-size": "0.7.0",
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
"@types/mustache": "4.2.6",
|
|
76
76
|
"@types/node": "22.9.0",
|
|
77
77
|
"@types/stack-trace": "0.0.33",
|
|
78
|
-
"backtest-kit": "16.
|
|
78
|
+
"backtest-kit": "16.4.0",
|
|
79
79
|
"glob": "11.0.1",
|
|
80
80
|
"markdown-it": "14.1.1",
|
|
81
81
|
"rimraf": "6.0.1",
|
|
@@ -92,14 +92,14 @@
|
|
|
92
92
|
"peerDependencies": {
|
|
93
93
|
"@babel/plugin-transform-modules-umd": "^7.27.1",
|
|
94
94
|
"@babel/standalone": "^7.29.1",
|
|
95
|
-
"@backtest-kit/graph": "^16.
|
|
96
|
-
"@backtest-kit/ollama": "^16.
|
|
97
|
-
"@backtest-kit/pinets": "^16.
|
|
98
|
-
"@backtest-kit/signals": "^16.
|
|
99
|
-
"@backtest-kit/ui": "^16.
|
|
95
|
+
"@backtest-kit/graph": "^16.4.0",
|
|
96
|
+
"@backtest-kit/ollama": "^16.4.0",
|
|
97
|
+
"@backtest-kit/pinets": "^16.4.0",
|
|
98
|
+
"@backtest-kit/signals": "^16.4.0",
|
|
99
|
+
"@backtest-kit/ui": "^16.4.0",
|
|
100
100
|
"pinets": "^0.9.29",
|
|
101
101
|
"ccxt": "^4.5.39",
|
|
102
|
-
"backtest-kit": "^16.
|
|
102
|
+
"backtest-kit": "^16.4.0",
|
|
103
103
|
"markdown-it": "^14.1.1",
|
|
104
104
|
"typescript": "^5.0.0"
|
|
105
105
|
},
|
|
@@ -13,12 +13,12 @@
|
|
|
13
13
|
"license": "ISC",
|
|
14
14
|
"type": "commonjs",
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@backtest-kit/cli": "^16.
|
|
17
|
-
"@backtest-kit/graph": "^16.
|
|
18
|
-
"@backtest-kit/pinets": "^16.
|
|
19
|
-
"@backtest-kit/ui": "^16.
|
|
16
|
+
"@backtest-kit/cli": "^16.4.0",
|
|
17
|
+
"@backtest-kit/graph": "^16.4.0",
|
|
18
|
+
"@backtest-kit/pinets": "^16.4.0",
|
|
19
|
+
"@backtest-kit/ui": "^16.4.0",
|
|
20
20
|
"agent-swarm-kit": "^6.0.0",
|
|
21
|
-
"backtest-kit": "^16.
|
|
21
|
+
"backtest-kit": "^16.4.0",
|
|
22
22
|
"functools-kit": "^5.0.0",
|
|
23
23
|
"garch": "^2.0.0",
|
|
24
24
|
"get-moment-stamp": "^3.0.0",
|