@backtest-kit/cli 9.6.0 β†’ 9.8.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 CHANGED
@@ -16,6 +16,50 @@ Point the CLI at your strategy file, choose a mode, and it handles exchange conn
16
16
 
17
17
  > **New to backtest-kit?** The fastest way to get a real, production-ready setup is to clone the [reference implementation](https://github.com/tripolskypetr/backtest-kit/tree/master/example) β€” a fully working news-sentiment AI trading system with LLM forecasting, multi-timeframe data, and a documented February 2026 backtest. Start there instead of from scratch.
18
18
 
19
+ ## 🏎️ CLI Init
20
+
21
+ Minimal scaffold β€” all boilerplate stays inside @backtest-kit/cli:
22
+
23
+ ```bash
24
+ npx @backtest-kit/cli --init --output backtest-kit-project
25
+ cd backtest-kit-project
26
+ npm install
27
+ npm start -- --help
28
+ ```
29
+
30
+ ## πŸ€” Philosophy
31
+
32
+ `@backtest-kit/cli` is designed to do **two things well** β€” and the same tool covers both.
33
+
34
+ **1. The lightest possible runner for a solo quant on day one.**
35
+
36
+ You write a strategy file, point the CLI at it, and you're trading. No DI container to learn, no project scaffold to fight, no infrastructure code to copy-paste. One developer, one strategy, one command:
37
+
38
+ ```bash
39
+ npx @backtest-kit/cli --init
40
+ npx @backtest-kit/cli --backtest ./content/feb_2026.strategy/index.ts
41
+ ```
42
+
43
+ That's the whole onboarding. The first day you have an idea, you can backtest it. The first week you have an edge, you can paper-trade it. The first month you have a P&L, you can run it live β€” same CLI, different flag.
44
+
45
+ **2. Built-in monorepo tooling for when the business takes off.**
46
+
47
+ The moment you start making money is the worst possible moment to rewrite your stack in another language. So the CLI is also a monorepo-grade runner from day one β€” even if you don't use it that way at first.
48
+
49
+ ```
50
+ monorepo/
51
+ β”œβ”€β”€ content/
52
+ β”‚ β”œβ”€β”€ feb_2026.strategy/
53
+ β”‚ β”œβ”€β”€ feb_2026.strategy.ts # strategy production code
54
+ β”‚ β”œβ”€β”€ feb_2026.test.ts # developer playground
55
+ β”œβ”€β”€ packages/
56
+ β”‚ β”œβ”€β”€ shared-broker/ # shared broker code
57
+ β”‚ β”œβ”€β”€ shared-signals/ # common indicators (RSI, MACD)
58
+ ```
59
+
60
+
61
+ As a result: you used to backtest your first idea is the same tool you use to run a desk of strategies in production. No rewrite, no language switch, no framework migration when the business scales β€” only more files in the monorepo.
62
+
19
63
  ## ✨ Features
20
64
 
21
65
  - πŸš€ **Zero Config**: Run `npx @backtest-kit/cli --backtest ./strategy.mjs` β€” no boilerplate needed
@@ -772,6 +816,78 @@ setup({
772
816
 
773
817
  No changes to strategy code are needed β€” `setup()` wires up the adapters transparently before `backtest-kit` makes its first persistence call.
774
818
 
819
+ ## 🧩 Module Loader (`config/loader.config`)
820
+
821
+ `@backtest-kit/cli` loads a `{projectRoot}/config/loader.config` file **after** `setup.config` but **before** any strategy or module code runs. Unlike `setup.config` (which is loaded for its side effects), `loader.config` exports a function that the CLI explicitly `await`s. Use it whenever you need to **wait for an async dependency** to be ready before the backtest starts.
822
+
823
+ ```bash
824
+ monorepo/
825
+ β”œβ”€β”€ packages/
826
+ β”‚ β”œβ”€β”€ shared-broker/ # shared broker code
827
+ β”‚ β”œβ”€β”€ shared-signals/ # common indicators (RSI, MACD)
828
+ β”‚ β”œβ”€β”€ shared-db/ # mongodb wiring
829
+ β”‚ β”œβ”€β”€ strategy-momentum/ # strategy code
830
+ β”‚ └── strategy-mean-reversion/
831
+ ```
832
+
833
+ **When to use it:**
834
+
835
+ - **Wire microfrontends in a monorepo** β€” resolve and pre-load sibling packages, register cross-package services, or hydrate a shared DI container before strategies import from neighboring workspaces.
836
+ - **Wait for a database connection** β€” open a Mongo/Postgres/Redis connection and verify it's reachable before the first persistence call, so the backtest fails fast instead of mid-run.
837
+ - **Warm up caches or external APIs** β€” pre-fetch reference data (instruments list, calendar, fee tables) so the strategy's first tick doesn't pay the round-trip cost.
838
+ - **Run schema migrations** β€” apply any pending migrations to the persistence backend before signals start flowing.
839
+
840
+ `loader.config` supports exactly one of two export styles β€” **never both at once**. If both are present, the `default` export wins and the named `loader` is ignored.
841
+
842
+ ```ts
843
+ // config/loader.config.ts β€” default export (preferred, ESM style)
844
+ export default async () => {
845
+ await mongoose.connect(process.env.CC_MONGO_CONNECTION_STRING!);
846
+ await redis.ping();
847
+ };
848
+ ```
849
+
850
+ ```ts
851
+ // config/loader.config.ts β€” named export
852
+ export const loader = async () => {
853
+ await mongoose.connect(process.env.CC_MONGO_CONNECTION_STRING!);
854
+ await redis.ping();
855
+ };
856
+ ```
857
+
858
+ ### Example: wait for MongoDB before running a backtest
859
+
860
+ `@backtest-kit/mongo`'s `setup()` registers the adapters synchronously but doesn't block until the connection is established. If your backtest depends on data that must be present in Mongo before the first signal fires, use `loader.config` to gate the run on a real connection:
861
+
862
+ ```ts
863
+ // config/setup.config.ts
864
+ import { setup } from '@backtest-kit/mongo';
865
+
866
+ setup();
867
+ ```
868
+
869
+ ```ts
870
+ // config/loader.config.ts
871
+ import mongoose from 'mongoose';
872
+
873
+ export default async () => {
874
+ await mongoose.connect(process.env.CC_MONGO_CONNECTION_STRING!);
875
+ console.log('mongo connection verified, starting backtest');
876
+ };
877
+ ```
878
+
879
+ ### Example: stitch microfrontends in a monorepo
880
+
881
+ When `backtest-kit` strategies live in one workspace and shared services (broker adapters, signal feeds, dashboards) live in sibling workspaces, `loader.config` is the place to wire them together before the runner starts:
882
+
883
+ ```ts
884
+ // config/loader.config.ts
885
+ import "@my-org/brokers";
886
+ import "@my-org/signals";
887
+ ```
888
+
889
+ The `@my-org` alias should be declared in `config/alias.config`.
890
+
775
891
  ## πŸ”€ Import Aliases (`config/alias.config`)
776
892
 
777
893
  `@backtest-kit/cli` lets you override any nodejs module import β€” without touching the strategy code. Drop a `config/alias.config` file in your project root and export a mapping from module name to replacement module.
package/build/index.cjs CHANGED
@@ -910,9 +910,15 @@ class BacktestMainService {
910
910
  this.loggerService.log("backtestMainService run", {
911
911
  payload,
912
912
  });
913
+ await this.configConnectionService.loadConfig("setup.config");
913
914
  {
914
- await this.configConnectionService.loadConfig("setup.config");
915
- await this.configConnectionService.loadConfig("loader.config");
915
+ const loader = await this.configConnectionService.loadConfig("loader.config");
916
+ if (typeof loader === "function") {
917
+ await loader();
918
+ }
919
+ if (typeof loader?.loader === "function") {
920
+ await loader.loader();
921
+ }
916
922
  }
917
923
  {
918
924
  await this.configService.waitForInit();
@@ -1032,9 +1038,15 @@ class WalkerMainService {
1032
1038
  this.configConnectionService = inject(TYPES.configConnectionService);
1033
1039
  this.run = functoolsKit.singleshot(async (payload) => {
1034
1040
  this.loggerService.log("walkerMainService run", { payload });
1041
+ await this.configConnectionService.loadConfig("setup.config");
1035
1042
  {
1036
- await this.configConnectionService.loadConfig("setup.config");
1037
- await this.configConnectionService.loadConfig("loader.config");
1043
+ const loader = await this.configConnectionService.loadConfig("loader.config");
1044
+ if (typeof loader === "function") {
1045
+ await loader();
1046
+ }
1047
+ if (typeof loader?.loader === "function") {
1048
+ await loader.loader();
1049
+ }
1038
1050
  }
1039
1051
  {
1040
1052
  await this.configService.waitForInit();
@@ -1260,9 +1272,15 @@ class LiveMainService {
1260
1272
  this.loggerService.log("liveMainService run", {
1261
1273
  payload,
1262
1274
  });
1275
+ await this.configConnectionService.loadConfig("setup.config");
1263
1276
  {
1264
- await this.configConnectionService.loadConfig("setup.config");
1265
- await this.configConnectionService.loadConfig("loader.config");
1277
+ const loader = await this.configConnectionService.loadConfig("loader.config");
1278
+ if (typeof loader === "function") {
1279
+ await loader();
1280
+ }
1281
+ if (typeof loader?.loader === "function") {
1282
+ await loader.loader();
1283
+ }
1266
1284
  }
1267
1285
  {
1268
1286
  await this.configService.waitForInit();
@@ -1349,9 +1367,15 @@ class PaperMainService {
1349
1367
  this.configConnectionService = inject(TYPES.configConnectionService);
1350
1368
  this.run = functoolsKit.singleshot(async (payload) => {
1351
1369
  this.loggerService.log("paperMainService init");
1370
+ await this.configConnectionService.loadConfig("setup.config");
1352
1371
  {
1353
- await this.configConnectionService.loadConfig("setup.config");
1354
- await this.configConnectionService.loadConfig("loader.config");
1372
+ const loader = await this.configConnectionService.loadConfig("loader.config");
1373
+ if (typeof loader === "function") {
1374
+ await loader();
1375
+ }
1376
+ if (typeof loader?.loader === "function") {
1377
+ await loader.loader();
1378
+ }
1355
1379
  }
1356
1380
  {
1357
1381
  await this.configService.waitForInit();
@@ -2623,14 +2647,29 @@ class BabelService {
2623
2647
  }
2624
2648
  }
2625
2649
 
2626
- const require$1 = Module.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
2627
2650
  const ModuleWithCache = Module;
2651
+ const LOOKUP_PATHS = ModuleWithCache._nodeModulePaths(process.cwd());
2652
+ const VIRTUAL_MAP = new Map();
2653
+ {
2654
+ const originalResolveFilename = ModuleWithCache._resolveFilename;
2655
+ ModuleWithCache._resolveFilename = function (request, parent, isMain, options) {
2656
+ const virtualKey = VIRTUAL_MAP.get(request);
2657
+ if (virtualKey) {
2658
+ return virtualKey;
2659
+ }
2660
+ return originalResolveFilename.call(this, request, parent, isMain, options);
2661
+ };
2662
+ }
2628
2663
  function overrideModule(moduleName, newExports) {
2629
2664
  const cache = ModuleWithCache._cache;
2630
- const key = require$1.resolve(moduleName);
2665
+ const resolved = ModuleWithCache._findPath(moduleName, LOOKUP_PATHS, false);
2666
+ const key = resolved || moduleName;
2667
+ VIRTUAL_MAP.set(moduleName, key);
2631
2668
  if (!cache[key]) {
2632
2669
  cache[key] = new ModuleWithCache(key);
2633
2670
  cache[key].loaded = true;
2671
+ cache[key].filename = key;
2672
+ cache[key].paths = [];
2634
2673
  }
2635
2674
  cache[key].exports = newExports;
2636
2675
  }
@@ -2913,8 +2952,14 @@ const GET_ALIAS_EXPORTS_FN = (self) => {
2913
2952
  continue;
2914
2953
  }
2915
2954
  const instance = self.getInstance(baseDir);
2916
- const exports = instance.import(filePath);
2917
- return "default" in exports ? exports.default : exports;
2955
+ const alias = instance.import(filePath);
2956
+ if (!alias) {
2957
+ return null;
2958
+ }
2959
+ if ("default" in alias) {
2960
+ return alias.default;
2961
+ }
2962
+ return alias;
2918
2963
  }
2919
2964
  return null;
2920
2965
  };
@@ -3013,6 +3058,10 @@ class ConfigConnectionService {
3013
3058
  const config = await LOAD_CONFIG_CONFIG_FN(fileName, this);
3014
3059
  if (!config) {
3015
3060
  this.loadConfig.clear(fileName);
3061
+ return null;
3062
+ }
3063
+ if ("default" in config) {
3064
+ return config.default;
3016
3065
  }
3017
3066
  return config;
3018
3067
  });
@@ -3171,7 +3220,7 @@ const main$g = async () => {
3171
3220
  if (MODES.some((mode) => values[mode])) {
3172
3221
  return;
3173
3222
  }
3174
- process.stdout.write(`@backtest-kit/cli ${"9.6.0"}\n`);
3223
+ process.stdout.write(`@backtest-kit/cli ${"9.8.0"}\n`);
3175
3224
  process.stdout.write("\n");
3176
3225
  process.stdout.write(`Run with --help to see available commands.\n`);
3177
3226
  process.stdout.write("\n");
@@ -3462,9 +3511,15 @@ const main$a = async () => {
3462
3511
  }
3463
3512
  await flush(entryPoint);
3464
3513
  }
3514
+ await cli.configConnectionService.loadConfig("setup.config");
3465
3515
  {
3466
- await cli.configConnectionService.loadConfig("setup.config");
3467
- await cli.configConnectionService.loadConfig("loader.config");
3516
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
3517
+ if (typeof loader === "function") {
3518
+ await loader();
3519
+ }
3520
+ if (typeof loader?.loader === "function") {
3521
+ await loader.loader();
3522
+ }
3468
3523
  }
3469
3524
  {
3470
3525
  await cli.configService.waitForInit();
@@ -3476,6 +3531,9 @@ const main$a = async () => {
3476
3531
  const cwd = process.cwd();
3477
3532
  dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
3478
3533
  }
3534
+ if (entryPoints.length === 1) {
3535
+ process.chdir(path.dirname(path.resolve(entryPoints[0])));
3536
+ }
3479
3537
  await cli.moduleConnectionService.loadModule(MODE_MODULE[mode]);
3480
3538
  listenFinish();
3481
3539
  createGracefulShutdown(mode)();
@@ -3566,9 +3624,15 @@ const main$7 = async () => {
3566
3624
  const cwd = process.cwd();
3567
3625
  dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
3568
3626
  }
3627
+ await cli.configConnectionService.loadConfig("setup.config");
3569
3628
  {
3570
- await cli.configConnectionService.loadConfig("setup.config");
3571
- await cli.configConnectionService.loadConfig("loader.config");
3629
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
3630
+ if (typeof loader === "function") {
3631
+ await loader();
3632
+ }
3633
+ if (typeof loader?.loader === "function") {
3634
+ await loader.loader();
3635
+ }
3572
3636
  }
3573
3637
  await cli.moduleConnectionService.loadModule("pine.module");
3574
3638
  {
@@ -3647,9 +3711,15 @@ const main$6 = async () => {
3647
3711
  console.warn("--editor and --pine are mutually exclusive. Use one at a time.");
3648
3712
  process.exit(1);
3649
3713
  }
3714
+ await cli.configConnectionService.loadConfig("setup.config");
3650
3715
  {
3651
- await cli.configConnectionService.loadConfig("setup.config");
3652
- await cli.configConnectionService.loadConfig("loader.config");
3716
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
3717
+ if (typeof loader === "function") {
3718
+ await loader();
3719
+ }
3720
+ if (typeof loader?.loader === "function") {
3721
+ await loader.loader();
3722
+ }
3653
3723
  }
3654
3724
  {
3655
3725
  await cli.configService.waitForInit();
@@ -3692,9 +3762,15 @@ const main$5 = async () => {
3692
3762
  const cwd = process.cwd();
3693
3763
  dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
3694
3764
  }
3765
+ await cli.configConnectionService.loadConfig("setup.config");
3695
3766
  {
3696
- await cli.configConnectionService.loadConfig("setup.config");
3697
- await cli.configConnectionService.loadConfig("loader.config");
3767
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
3768
+ if (typeof loader === "function") {
3769
+ await loader();
3770
+ }
3771
+ if (typeof loader?.loader === "function") {
3772
+ await loader.loader();
3773
+ }
3698
3774
  }
3699
3775
  await cli.moduleConnectionService.loadModule("dump.module");
3700
3776
  {
@@ -3763,9 +3839,15 @@ const main$4 = async () => {
3763
3839
  const cwd = process.cwd();
3764
3840
  dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
3765
3841
  }
3842
+ await cli.configConnectionService.loadConfig("setup.config");
3766
3843
  {
3767
- await cli.configConnectionService.loadConfig("setup.config");
3768
- await cli.configConnectionService.loadConfig("loader.config");
3844
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
3845
+ if (typeof loader === "function") {
3846
+ await loader();
3847
+ }
3848
+ if (typeof loader?.loader === "function") {
3849
+ await loader.loader();
3850
+ }
3769
3851
  }
3770
3852
  await cli.moduleConnectionService.loadModule("pnldebug.module");
3771
3853
  {
@@ -4221,7 +4303,7 @@ const main$1 = async () => {
4221
4303
  if (!values.help) {
4222
4304
  return;
4223
4305
  }
4224
- process.stdout.write(`@backtest-kit/cli ${"9.6.0"}\n\n`);
4306
+ process.stdout.write(`@backtest-kit/cli ${"9.8.0"}\n\n`);
4225
4307
  process.stdout.write(HELP_TEXT);
4226
4308
  process.exit(0);
4227
4309
  };
@@ -4235,7 +4317,7 @@ const main = async () => {
4235
4317
  if (!values.version) {
4236
4318
  return;
4237
4319
  }
4238
- process.stdout.write(`@backtest-kit/cli ${"9.6.0"}\n`);
4320
+ process.stdout.write(`@backtest-kit/cli ${"9.8.0"}\n`);
4239
4321
  process.exit(0);
4240
4322
  };
4241
4323
  main();
package/build/index.mjs CHANGED
@@ -885,9 +885,15 @@ class BacktestMainService {
885
885
  this.loggerService.log("backtestMainService run", {
886
886
  payload,
887
887
  });
888
+ await this.configConnectionService.loadConfig("setup.config");
888
889
  {
889
- await this.configConnectionService.loadConfig("setup.config");
890
- await this.configConnectionService.loadConfig("loader.config");
890
+ const loader = await this.configConnectionService.loadConfig("loader.config");
891
+ if (typeof loader === "function") {
892
+ await loader();
893
+ }
894
+ if (typeof loader?.loader === "function") {
895
+ await loader.loader();
896
+ }
891
897
  }
892
898
  {
893
899
  await this.configService.waitForInit();
@@ -1007,9 +1013,15 @@ class WalkerMainService {
1007
1013
  this.configConnectionService = inject(TYPES.configConnectionService);
1008
1014
  this.run = singleshot(async (payload) => {
1009
1015
  this.loggerService.log("walkerMainService run", { payload });
1016
+ await this.configConnectionService.loadConfig("setup.config");
1010
1017
  {
1011
- await this.configConnectionService.loadConfig("setup.config");
1012
- await this.configConnectionService.loadConfig("loader.config");
1018
+ const loader = await this.configConnectionService.loadConfig("loader.config");
1019
+ if (typeof loader === "function") {
1020
+ await loader();
1021
+ }
1022
+ if (typeof loader?.loader === "function") {
1023
+ await loader.loader();
1024
+ }
1013
1025
  }
1014
1026
  {
1015
1027
  await this.configService.waitForInit();
@@ -1235,9 +1247,15 @@ class LiveMainService {
1235
1247
  this.loggerService.log("liveMainService run", {
1236
1248
  payload,
1237
1249
  });
1250
+ await this.configConnectionService.loadConfig("setup.config");
1238
1251
  {
1239
- await this.configConnectionService.loadConfig("setup.config");
1240
- await this.configConnectionService.loadConfig("loader.config");
1252
+ const loader = await this.configConnectionService.loadConfig("loader.config");
1253
+ if (typeof loader === "function") {
1254
+ await loader();
1255
+ }
1256
+ if (typeof loader?.loader === "function") {
1257
+ await loader.loader();
1258
+ }
1241
1259
  }
1242
1260
  {
1243
1261
  await this.configService.waitForInit();
@@ -1324,9 +1342,15 @@ class PaperMainService {
1324
1342
  this.configConnectionService = inject(TYPES.configConnectionService);
1325
1343
  this.run = singleshot(async (payload) => {
1326
1344
  this.loggerService.log("paperMainService init");
1345
+ await this.configConnectionService.loadConfig("setup.config");
1327
1346
  {
1328
- await this.configConnectionService.loadConfig("setup.config");
1329
- await this.configConnectionService.loadConfig("loader.config");
1347
+ const loader = await this.configConnectionService.loadConfig("loader.config");
1348
+ if (typeof loader === "function") {
1349
+ await loader();
1350
+ }
1351
+ if (typeof loader?.loader === "function") {
1352
+ await loader.loader();
1353
+ }
1330
1354
  }
1331
1355
  {
1332
1356
  await this.configService.waitForInit();
@@ -2598,14 +2622,29 @@ class BabelService {
2598
2622
  }
2599
2623
  }
2600
2624
 
2601
- const require = createRequire(import.meta.url);
2602
2625
  const ModuleWithCache = Module;
2626
+ const LOOKUP_PATHS = ModuleWithCache._nodeModulePaths(process.cwd());
2627
+ const VIRTUAL_MAP = new Map();
2628
+ {
2629
+ const originalResolveFilename = ModuleWithCache._resolveFilename;
2630
+ ModuleWithCache._resolveFilename = function (request, parent, isMain, options) {
2631
+ const virtualKey = VIRTUAL_MAP.get(request);
2632
+ if (virtualKey) {
2633
+ return virtualKey;
2634
+ }
2635
+ return originalResolveFilename.call(this, request, parent, isMain, options);
2636
+ };
2637
+ }
2603
2638
  function overrideModule(moduleName, newExports) {
2604
2639
  const cache = ModuleWithCache._cache;
2605
- const key = require.resolve(moduleName);
2640
+ const resolved = ModuleWithCache._findPath(moduleName, LOOKUP_PATHS, false);
2641
+ const key = resolved || moduleName;
2642
+ VIRTUAL_MAP.set(moduleName, key);
2606
2643
  if (!cache[key]) {
2607
2644
  cache[key] = new ModuleWithCache(key);
2608
2645
  cache[key].loaded = true;
2646
+ cache[key].filename = key;
2647
+ cache[key].paths = [];
2609
2648
  }
2610
2649
  cache[key].exports = newExports;
2611
2650
  }
@@ -2884,8 +2923,14 @@ const GET_ALIAS_EXPORTS_FN = (self) => {
2884
2923
  continue;
2885
2924
  }
2886
2925
  const instance = self.getInstance(baseDir);
2887
- const exports = instance.import(filePath);
2888
- return "default" in exports ? exports.default : exports;
2926
+ const alias = instance.import(filePath);
2927
+ if (!alias) {
2928
+ return null;
2929
+ }
2930
+ if ("default" in alias) {
2931
+ return alias.default;
2932
+ }
2933
+ return alias;
2889
2934
  }
2890
2935
  return null;
2891
2936
  };
@@ -2984,6 +3029,10 @@ class ConfigConnectionService {
2984
3029
  const config = await LOAD_CONFIG_CONFIG_FN(fileName, this);
2985
3030
  if (!config) {
2986
3031
  this.loadConfig.clear(fileName);
3032
+ return null;
3033
+ }
3034
+ if ("default" in config) {
3035
+ return config.default;
2987
3036
  }
2988
3037
  return config;
2989
3038
  });
@@ -3142,7 +3191,7 @@ const main$g = async () => {
3142
3191
  if (MODES.some((mode) => values[mode])) {
3143
3192
  return;
3144
3193
  }
3145
- process.stdout.write(`@backtest-kit/cli ${"9.6.0"}\n`);
3194
+ process.stdout.write(`@backtest-kit/cli ${"9.8.0"}\n`);
3146
3195
  process.stdout.write("\n");
3147
3196
  process.stdout.write(`Run with --help to see available commands.\n`);
3148
3197
  process.stdout.write("\n");
@@ -3433,9 +3482,15 @@ const main$a = async () => {
3433
3482
  }
3434
3483
  await flush(entryPoint);
3435
3484
  }
3485
+ await cli.configConnectionService.loadConfig("setup.config");
3436
3486
  {
3437
- await cli.configConnectionService.loadConfig("setup.config");
3438
- await cli.configConnectionService.loadConfig("loader.config");
3487
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
3488
+ if (typeof loader === "function") {
3489
+ await loader();
3490
+ }
3491
+ if (typeof loader?.loader === "function") {
3492
+ await loader.loader();
3493
+ }
3439
3494
  }
3440
3495
  {
3441
3496
  await cli.configService.waitForInit();
@@ -3447,6 +3502,9 @@ const main$a = async () => {
3447
3502
  const cwd = process.cwd();
3448
3503
  dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
3449
3504
  }
3505
+ if (entryPoints.length === 1) {
3506
+ process.chdir(path.dirname(path.resolve(entryPoints[0])));
3507
+ }
3450
3508
  await cli.moduleConnectionService.loadModule(MODE_MODULE[mode]);
3451
3509
  listenFinish();
3452
3510
  createGracefulShutdown(mode)();
@@ -3537,9 +3595,15 @@ const main$7 = async () => {
3537
3595
  const cwd = process.cwd();
3538
3596
  dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
3539
3597
  }
3598
+ await cli.configConnectionService.loadConfig("setup.config");
3540
3599
  {
3541
- await cli.configConnectionService.loadConfig("setup.config");
3542
- await cli.configConnectionService.loadConfig("loader.config");
3600
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
3601
+ if (typeof loader === "function") {
3602
+ await loader();
3603
+ }
3604
+ if (typeof loader?.loader === "function") {
3605
+ await loader.loader();
3606
+ }
3543
3607
  }
3544
3608
  await cli.moduleConnectionService.loadModule("pine.module");
3545
3609
  {
@@ -3618,9 +3682,15 @@ const main$6 = async () => {
3618
3682
  console.warn("--editor and --pine are mutually exclusive. Use one at a time.");
3619
3683
  process.exit(1);
3620
3684
  }
3685
+ await cli.configConnectionService.loadConfig("setup.config");
3621
3686
  {
3622
- await cli.configConnectionService.loadConfig("setup.config");
3623
- await cli.configConnectionService.loadConfig("loader.config");
3687
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
3688
+ if (typeof loader === "function") {
3689
+ await loader();
3690
+ }
3691
+ if (typeof loader?.loader === "function") {
3692
+ await loader.loader();
3693
+ }
3624
3694
  }
3625
3695
  {
3626
3696
  await cli.configService.waitForInit();
@@ -3663,9 +3733,15 @@ const main$5 = async () => {
3663
3733
  const cwd = process.cwd();
3664
3734
  dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
3665
3735
  }
3736
+ await cli.configConnectionService.loadConfig("setup.config");
3666
3737
  {
3667
- await cli.configConnectionService.loadConfig("setup.config");
3668
- await cli.configConnectionService.loadConfig("loader.config");
3738
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
3739
+ if (typeof loader === "function") {
3740
+ await loader();
3741
+ }
3742
+ if (typeof loader?.loader === "function") {
3743
+ await loader.loader();
3744
+ }
3669
3745
  }
3670
3746
  await cli.moduleConnectionService.loadModule("dump.module");
3671
3747
  {
@@ -3734,9 +3810,15 @@ const main$4 = async () => {
3734
3810
  const cwd = process.cwd();
3735
3811
  dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
3736
3812
  }
3813
+ await cli.configConnectionService.loadConfig("setup.config");
3737
3814
  {
3738
- await cli.configConnectionService.loadConfig("setup.config");
3739
- await cli.configConnectionService.loadConfig("loader.config");
3815
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
3816
+ if (typeof loader === "function") {
3817
+ await loader();
3818
+ }
3819
+ if (typeof loader?.loader === "function") {
3820
+ await loader.loader();
3821
+ }
3740
3822
  }
3741
3823
  await cli.moduleConnectionService.loadModule("pnldebug.module");
3742
3824
  {
@@ -4192,7 +4274,7 @@ const main$1 = async () => {
4192
4274
  if (!values.help) {
4193
4275
  return;
4194
4276
  }
4195
- process.stdout.write(`@backtest-kit/cli ${"9.6.0"}\n\n`);
4277
+ process.stdout.write(`@backtest-kit/cli ${"9.8.0"}\n\n`);
4196
4278
  process.stdout.write(HELP_TEXT);
4197
4279
  process.exit(0);
4198
4280
  };
@@ -4206,7 +4288,7 @@ const main = async () => {
4206
4288
  if (!values.version) {
4207
4289
  return;
4208
4290
  }
4209
- process.stdout.write(`@backtest-kit/cli ${"9.6.0"}\n`);
4291
+ process.stdout.write(`@backtest-kit/cli ${"9.8.0"}\n`);
4210
4292
  process.exit(0);
4211
4293
  };
4212
4294
  main();
package/docker/README.md CHANGED
@@ -19,6 +19,7 @@ A self-contained Docker workspace for running [backtest-kit](https://github.com/
19
19
  | Strategy | Ticker | Period | Signal source | Net PNL | Sharpe |
20
20
  |---|---|---|---|---|---|
21
21
  | [DOTUSDT Feb 2021 β€” Python EMA Crossover](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/feb_2021.strategy) | DOTUSDT | Feb 2021 | Python EMA(9)/EMA(21) crossover via WebAssembly | **+5.52%** | **0.09** |
22
+ | [BTCUSDT Apr 2024 β€” Polymarket Ξ”prob](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/apr_2024.strategy) | BTCUSDT | Apr 2024 | Polymarket "yes" probability shifts on crypto-prices markets | **+0.63%** | **0.055** |
22
23
  | [BTCUSDT Oct 2021 β€” TensorFlow Neural Network](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/oct_2021.strategy) | BTCUSDT | Oct 2021 | TensorFlow NN predicting next candle close | **+18.26%** | **0.31** |
23
24
  | [BTCUSDT Dec 2025 β€” Pine Script Range Breakout](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/dec_2025.strategy) | BTCUSDT | Dec 2025 | Pine Script BB + range detector + volume spike | **+2.40%** | **0.06** |
24
25
  | [TRXUSDT Jan 2026 β€” Liquidity Harvesting](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content//jan_2026.strategy) | TRXUSDT | Jan 2026 | Telegram channel signals (inverted) | **+8.58%** | **1.14** |
@@ -15,17 +15,17 @@
15
15
  "@types/node": "25.6.0"
16
16
  },
17
17
  "dependencies": {
18
- "@backtest-kit/cli": "9.6.0",
19
- "@backtest-kit/graph": "9.6.0",
20
- "@backtest-kit/pinets": "9.6.0",
21
- "@backtest-kit/signals": "9.6.0",
22
- "@backtest-kit/ui": "9.6.0",
18
+ "@backtest-kit/cli": "9.8.0",
19
+ "@backtest-kit/graph": "9.8.0",
20
+ "@backtest-kit/pinets": "9.8.0",
21
+ "@backtest-kit/signals": "9.8.0",
22
+ "@backtest-kit/ui": "9.8.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": "2.6.0",
28
- "backtest-kit": "9.6.0",
28
+ "backtest-kit": "9.8.0",
29
29
  "dayjs": "1.11.20",
30
30
  "functools-kit": "2.3.0",
31
31
  "garch": "1.2.3",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtest-kit/cli",
3
- "version": "9.6.0",
3
+ "version": "9.8.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": "9.6.0",
67
- "@backtest-kit/ollama": "9.6.0",
68
- "@backtest-kit/pinets": "9.6.0",
69
- "@backtest-kit/signals": "9.6.0",
70
- "@backtest-kit/ui": "9.6.0",
66
+ "@backtest-kit/graph": "9.8.0",
67
+ "@backtest-kit/ollama": "9.8.0",
68
+ "@backtest-kit/pinets": "9.8.0",
69
+ "@backtest-kit/signals": "9.8.0",
70
+ "@backtest-kit/ui": "9.8.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": "9.6.0",
78
+ "backtest-kit": "9.8.0",
79
79
  "glob": "11.0.1",
80
80
  "markdown-it": "14.1.1",
81
81
  "rimraf": "6.0.1",
@@ -90,12 +90,12 @@
90
90
  "peerDependencies": {
91
91
  "@babel/plugin-transform-modules-umd": "^7.27.1",
92
92
  "@babel/standalone": "^7.29.1",
93
- "@backtest-kit/graph": "^9.6.0",
94
- "@backtest-kit/ollama": "^9.6.0",
95
- "@backtest-kit/pinets": "^9.6.0",
96
- "@backtest-kit/signals": "^9.6.0",
97
- "@backtest-kit/ui": "^9.6.0",
98
- "backtest-kit": "^9.6.0",
93
+ "@backtest-kit/graph": "^9.8.0",
94
+ "@backtest-kit/ollama": "^9.8.0",
95
+ "@backtest-kit/pinets": "^9.8.0",
96
+ "@backtest-kit/signals": "^9.8.0",
97
+ "@backtest-kit/ui": "^9.8.0",
98
+ "backtest-kit": "^9.8.0",
99
99
  "markdown-it": "^14.1.1",
100
100
  "typescript": "^5.0.0"
101
101
  },
@@ -17,6 +17,7 @@ A minimal project scaffold for [backtest-kit](https://github.com/tripolskypetr/b
17
17
  | Strategy | Ticker | Period | Signal source | Net PNL | Sharpe |
18
18
  |---|---|---|---|---|---|
19
19
  | [DOTUSDT Feb 2021 β€” Python EMA Crossover](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/feb_2021.strategy) | DOTUSDT | Feb 2021 | Python EMA(9)/EMA(21) crossover via WebAssembly | **+5.52%** | **0.09** |
20
+ | [BTCUSDT Apr 2024 β€” Polymarket Ξ”prob](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/apr_2024.strategy) | BTCUSDT | Apr 2024 | Polymarket "yes" probability shifts on crypto-prices markets | **+0.63%** | **0.055** |
20
21
  | [BTCUSDT Oct 2021 β€” TensorFlow Neural Network](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/oct_2021.strategy) | BTCUSDT | Oct 2021 | TensorFlow NN predicting next candle close | **+18.26%** | **0.31** |
21
22
  | [BTCUSDT Dec 2025 β€” Pine Script Range Breakout](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content/dec_2025.strategy) | BTCUSDT | Dec 2025 | Pine Script BB + range detector + volume spike | **+2.40%** | **0.06** |
22
23
  | [TRXUSDT Jan 2026 β€” Liquidity Harvesting](https://github.com/tripolskypetr/backtest-kit/tree/master/example/content//jan_2026.strategy) | TRXUSDT | Jan 2026 | Telegram channel signals (inverted) | **+8.58%** | **1.14** |
@@ -13,12 +13,12 @@
13
13
  "license": "ISC",
14
14
  "type": "commonjs",
15
15
  "dependencies": {
16
- "@backtest-kit/cli": "^9.6.0",
17
- "@backtest-kit/graph": "^9.6.0",
18
- "@backtest-kit/pinets": "^9.6.0",
19
- "@backtest-kit/ui": "^9.6.0",
16
+ "@backtest-kit/cli": "^9.8.0",
17
+ "@backtest-kit/graph": "^9.8.0",
18
+ "@backtest-kit/pinets": "^9.8.0",
19
+ "@backtest-kit/ui": "^9.8.0",
20
20
  "agent-swarm-kit": "^2.6.0",
21
- "backtest-kit": "^9.6.0",
21
+ "backtest-kit": "^9.8.0",
22
22
  "functools-kit": "^2.3.0",
23
23
  "garch": "^1.2.3",
24
24
  "get-moment-stamp": "^1.1.2",
package/types.d.ts CHANGED
@@ -240,17 +240,12 @@ declare class SymbolSchemaService {
240
240
  addSchema: (() => Promise<void>) & functools_kit.ISingleshotClearable<() => Promise<void>>;
241
241
  }
242
242
 
243
- type ModuleExports = {
244
- [key: string]: any;
245
- default?: any;
246
- };
247
-
248
243
  declare class ConfigConnectionService {
249
244
  readonly loggerService: LoggerService;
250
245
  readonly resolveService: ResolveService;
251
246
  readonly loaderService: LoaderService;
252
247
  hasConfig: (fileName: string) => boolean;
253
- loadConfig: ((fileName: string) => Promise<ModuleExports>) & functools_kit.IClearableMemoize<string> & functools_kit.IControlMemoize<string, Promise<ModuleExports>>;
248
+ loadConfig: ((fileName: string) => Promise<any>) & functools_kit.IClearableMemoize<string> & functools_kit.IControlMemoize<string, Promise<any>>;
254
249
  }
255
250
 
256
251
  declare class FrontendProviderService {