@backtest-kit/cli 6.1.4 → 6.2.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/build/index.mjs CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import * as BacktestKit from 'backtest-kit';
3
- import { Storage, Notification, Markdown, Report, Dump, Memory, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, setConfig, Log, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenSync, alignToInterval, Exchange } from 'backtest-kit';
4
- import { getErrorMessage, errorData, singleshot, str, BehaviorSubject, compose, execpool, queued, sleep, randomString, createAwaiter, TIMEOUT_SYMBOL, typo, retry, trycatch, memoize } from 'functools-kit';
3
+ import { Storage, Notification, Markdown, Report, Dump, Memory, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, setConfig, Log, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenSync, Exchange } from 'backtest-kit';
4
+ import { getErrorMessage, errorData, singleshot, str, BehaviorSubject, compose, createAwaiter, execpool, queued, sleep, randomString, TIMEOUT_SYMBOL, typo, retry, trycatch, memoize } from 'functools-kit';
5
5
  import fs, { constants } from 'fs';
6
6
  import * as stackTrace from 'stack-trace';
7
- import path, { basename, extname, join, resolve, dirname } from 'path';
7
+ import path, { join, resolve, basename, extname, dirname } from 'path';
8
8
  import fs$1, { access, readFile, mkdir, writeFile, readdir, copyFile } from 'fs/promises';
9
9
  import dotenv from 'dotenv';
10
10
  import { createActivator } from 'di-kit';
@@ -224,6 +224,7 @@ const connectionServices$1 = {
224
224
  };
225
225
  const mainServices$1 = {
226
226
  backtestMainService: Symbol('backtestMainService'),
227
+ walkerMainService: Symbol('walkerMainService'),
227
228
  paperMainService: Symbol('paperMainService'),
228
229
  liveMainService: Symbol('liveMainService'),
229
230
  };
@@ -292,6 +293,19 @@ class ResolveService {
292
293
  _is_launched = true;
293
294
  return await readFile(absolutePath, "utf-8");
294
295
  };
296
+ this.attachStrategy = async (jsPath) => {
297
+ this.loggerService.log("resolveService attachStrategy", {
298
+ jsPath
299
+ });
300
+ const absolutePath = path.resolve(jsPath);
301
+ await access(absolutePath, constants.F_OK | constants.R_OK);
302
+ const moduleRoot = path.dirname(absolutePath);
303
+ {
304
+ const cwd = process.cwd();
305
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
306
+ }
307
+ this.loaderService.import(absolutePath, moduleRoot);
308
+ };
295
309
  this.attachJavascript = async (jsPath) => {
296
310
  this.loggerService.log("resolveService attachJavascript", {
297
311
  jsPath
@@ -459,6 +473,10 @@ const getArgs = singleshot(() => {
459
473
  type: "boolean",
460
474
  default: false,
461
475
  },
476
+ walker: {
477
+ type: "boolean",
478
+ default: false,
479
+ },
462
480
  live: {
463
481
  type: "boolean",
464
482
  default: false,
@@ -549,12 +567,11 @@ const getArgs = singleshot(() => {
549
567
  positionals,
550
568
  };
551
569
  });
552
- const getPositional = singleshot(() => {
570
+ const getPositionals = singleshot(() => {
553
571
  const { positionals = [] } = getArgs();
554
- const result = positionals
572
+ return positionals
555
573
  .filter((value) => !DISALLOWED_PATHS.some((path) => value.includes(path)))
556
- .find((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
557
- return result || null;
574
+ .filter((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
558
575
  });
559
576
 
560
577
  const ADD_FRAME_FN = (self) => {
@@ -659,11 +676,11 @@ const notifyVerbose = singleshot(() => {
659
676
  });
660
677
  });
661
678
 
662
- const DEFAULT_CACHE_LIST = ["1m", "15m", "30m", "1h", "4h"];
663
- const GET_CACHE_INTERVAL_LIST_FN = () => {
679
+ const DEFAULT_CACHE_LIST$1 = ["1m", "15m", "30m", "1h", "4h"];
680
+ const GET_CACHE_INTERVAL_LIST_FN$1 = () => {
664
681
  const { values } = getArgs();
665
682
  if (!values.cacheInterval) {
666
- return DEFAULT_CACHE_LIST;
683
+ return DEFAULT_CACHE_LIST$1;
667
684
  }
668
685
  return String(values.cacheInterval)
669
686
  .split(",")
@@ -688,7 +705,10 @@ class BacktestMainService {
688
705
  this.frontendProviderService.connect();
689
706
  this.telegramProviderService.connect();
690
707
  }
691
- await this.resolveService.attachJavascript(payload.entryPoint);
708
+ {
709
+ await this.resolveService.attachJavascript(payload.entryPoint);
710
+ await this.moduleConnectionService.loadModule("./backtest.module");
711
+ }
692
712
  {
693
713
  this.exchangeSchemaService.addSchema();
694
714
  this.symbolSchemaService.addSchema();
@@ -728,7 +748,6 @@ class BacktestMainService {
728
748
  });
729
749
  notifyVerbose();
730
750
  }
731
- await this.moduleConnectionService.loadModule("./backtest.module");
732
751
  Backtest.background(symbol, {
733
752
  strategyName,
734
753
  frameName,
@@ -745,11 +764,11 @@ class BacktestMainService {
745
764
  if (!values.backtest) {
746
765
  return;
747
766
  }
748
- const entryPoint = getPositional();
767
+ const [entryPoint = null] = getPositionals();
749
768
  if (!entryPoint) {
750
769
  throw new Error("Entry point is required");
751
770
  }
752
- const cacheInterval = GET_CACHE_INTERVAL_LIST_FN();
771
+ const cacheInterval = GET_CACHE_INTERVAL_LIST_FN$1();
753
772
  return await this.run({
754
773
  symbol: values.symbol,
755
774
  entryPoint,
@@ -764,6 +783,170 @@ class BacktestMainService {
764
783
  }
765
784
  }
766
785
 
786
+ const DEFAULT_CACHE_LIST = ["1m", "15m", "30m", "1h", "4h"];
787
+ const WALKER_NAME = "cli-walker";
788
+ const GET_CACHE_INTERVAL_LIST_FN = () => {
789
+ const { values } = getArgs();
790
+ if (!values.cacheInterval) {
791
+ return DEFAULT_CACHE_LIST;
792
+ }
793
+ return String(values.cacheInterval)
794
+ .split(",")
795
+ .map((timeframe) => timeframe.trim());
796
+ };
797
+ class WalkerMainService {
798
+ constructor() {
799
+ this.loggerService = inject(TYPES.loggerService);
800
+ this.resolveService = inject(TYPES.resolveService);
801
+ this.exchangeSchemaService = inject(TYPES.exchangeSchemaService);
802
+ this.symbolSchemaService = inject(TYPES.symbolSchemaService);
803
+ this.cacheLogicService = inject(TYPES.cacheLogicService);
804
+ this.moduleConnectionService = inject(TYPES.moduleConnectionService);
805
+ this.run = singleshot(async (payload) => {
806
+ this.loggerService.log("walkerMainService run", { payload });
807
+ for (const entryPoint of payload.entryPoints) {
808
+ await this.resolveService.attachStrategy(entryPoint);
809
+ }
810
+ await this.moduleConnectionService.loadModule("./walker.module");
811
+ {
812
+ this.exchangeSchemaService.addSchema();
813
+ this.symbolSchemaService.addSchema();
814
+ }
815
+ {
816
+ const { length } = await listFrameSchema();
817
+ if (!length) {
818
+ const endDate = alignToInterval(new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), "1m");
819
+ const startDate = alignToInterval(new Date(Date.now() - 31 * 24 * 60 * 60 * 1000), "1m");
820
+ console.warn(`Warning: The default frame schema is set to the interval ${startDate.toISOString()} — ${endDate.toISOString()}. Please make sure to update it according to your needs using addFrameSchema in your strategy files.`);
821
+ addFrameSchema({
822
+ frameName: FrameName$1.DefaultFrame,
823
+ interval: "1m",
824
+ startDate,
825
+ endDate,
826
+ });
827
+ }
828
+ }
829
+ const symbol = payload.symbol || "BTCUSDT";
830
+ const strategyList = await listStrategySchema();
831
+ const strategyNames = strategyList.map((s) => s.strategyName);
832
+ if (!strategyNames.length) {
833
+ throw new Error("No strategies found in provided entry points");
834
+ }
835
+ const [defaultExchangeName = null] = await listExchangeSchema();
836
+ const [defaultFrameName = null] = await listFrameSchema();
837
+ const exchangeName = defaultExchangeName?.exchangeName;
838
+ const frameName = defaultFrameName?.frameName;
839
+ if (!exchangeName) {
840
+ throw new Error("Exchange name is required");
841
+ }
842
+ if (!frameName) {
843
+ throw new Error("Frame name is required");
844
+ }
845
+ addWalkerSchema({
846
+ walkerName: WALKER_NAME,
847
+ exchangeName,
848
+ frameName,
849
+ strategies: strategyNames,
850
+ });
851
+ if (!payload.noCache) {
852
+ await this.cacheLogicService.execute(payload.cacheInterval, {
853
+ exchangeName,
854
+ frameName,
855
+ symbol,
856
+ });
857
+ }
858
+ if (payload.verbose) {
859
+ overrideExchangeSchema({
860
+ exchangeName,
861
+ callbacks: {
862
+ onCandleData(symbol, interval, since) {
863
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
864
+ },
865
+ },
866
+ });
867
+ notifyVerbose();
868
+ }
869
+ if (payload.verbose) {
870
+ overrideWalkerSchema({
871
+ walkerName: WALKER_NAME,
872
+ callbacks: {
873
+ onStrategyStart(strategyName, symbol) {
874
+ console.log(`Strategy started: ${strategyName} for symbol: ${symbol}`);
875
+ },
876
+ onStrategyError(strategyName, symbol, error) {
877
+ console.error(`Strategy error: ${strategyName} for symbol: ${symbol}`, error);
878
+ },
879
+ onStrategyComplete(strategyName, symbol) {
880
+ console.log(`Strategy completed: ${strategyName} for symbol: ${symbol}`);
881
+ },
882
+ onComplete(results) {
883
+ console.log(`Walker completed for symbol: ${results.symbol}`, results);
884
+ }
885
+ }
886
+ });
887
+ }
888
+ Walker.background(symbol, { walkerName: WALKER_NAME });
889
+ const [awaiter, { resolve: res }] = createAwaiter();
890
+ const unWalker = listenDoneWalker(() => {
891
+ console.log("Walker comparison finished");
892
+ unWalker();
893
+ res();
894
+ });
895
+ payload.verbose && console.time("Walker");
896
+ await awaiter;
897
+ payload.verbose && console.timeEnd("Walker");
898
+ const dumpName = payload.output || `walker_${symbol}_${Date.now()}`;
899
+ const dumpDir = join(process.cwd(), "dump");
900
+ if (payload.json) {
901
+ const filePath = resolve(dumpDir, `${dumpName}.json`);
902
+ const data = await Walker.getData(symbol, { walkerName: WALKER_NAME });
903
+ await mkdir(dumpDir, { recursive: true });
904
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
905
+ console.log(`Saved: ${filePath}`);
906
+ process.exit(0);
907
+ return;
908
+ }
909
+ if (payload.markdown) {
910
+ const filePath = resolve(dumpDir, `${dumpName}.md`);
911
+ const report = await Walker.getReport(symbol, { walkerName: WALKER_NAME });
912
+ await mkdir(dumpDir, { recursive: true });
913
+ await writeFile(filePath, report, "utf-8");
914
+ console.log(`Saved: ${filePath}`);
915
+ process.exit(0);
916
+ return;
917
+ }
918
+ const report = await Walker.getReport(symbol, { walkerName: WALKER_NAME });
919
+ console.log(report);
920
+ process.exit(0);
921
+ });
922
+ this.connect = singleshot(async () => {
923
+ this.loggerService.log("walkerMainService connect");
924
+ if (!getEntry(import.meta.url)) {
925
+ return;
926
+ }
927
+ const { values } = getArgs();
928
+ if (!values.walker) {
929
+ return;
930
+ }
931
+ const entryPoints = getPositionals();
932
+ if (!entryPoints.length) {
933
+ throw new Error("At least one entry point is required");
934
+ }
935
+ const cacheInterval = GET_CACHE_INTERVAL_LIST_FN();
936
+ return await this.run({
937
+ entryPoints,
938
+ json: values.json,
939
+ markdown: values.markdown,
940
+ symbol: values.symbol,
941
+ output: values.output,
942
+ cacheInterval,
943
+ verbose: values.verbose,
944
+ noCache: values.noCache,
945
+ });
946
+ });
947
+ }
948
+ }
949
+
767
950
  class LiveMainService {
768
951
  constructor() {
769
952
  this.loggerService = inject(TYPES.loggerService);
@@ -781,7 +964,10 @@ class LiveMainService {
781
964
  this.frontendProviderService.connect();
782
965
  this.telegramProviderService.connect();
783
966
  }
784
- await this.resolveService.attachJavascript(payload.entryPoint);
967
+ {
968
+ await this.resolveService.attachJavascript(payload.entryPoint);
969
+ await this.moduleConnectionService.loadModule("./live.module");
970
+ }
785
971
  {
786
972
  this.exchangeSchemaService.addSchema();
787
973
  this.symbolSchemaService.addSchema();
@@ -808,7 +994,6 @@ class LiveMainService {
808
994
  });
809
995
  notifyVerbose();
810
996
  }
811
- await this.moduleConnectionService.loadModule("./live.module");
812
997
  Live.background(symbol, {
813
998
  strategyName,
814
999
  exchangeName,
@@ -824,7 +1009,7 @@ class LiveMainService {
824
1009
  if (!values.live) {
825
1010
  return;
826
1011
  }
827
- const entryPoint = getPositional();
1012
+ const [entryPoint = null] = getPositionals();
828
1013
  if (!entryPoint) {
829
1014
  throw new Error("Entry point is required");
830
1015
  }
@@ -854,7 +1039,10 @@ class PaperMainService {
854
1039
  this.frontendProviderService.connect();
855
1040
  this.telegramProviderService.connect();
856
1041
  }
857
- await this.resolveService.attachJavascript(payload.entryPoint);
1042
+ {
1043
+ await this.resolveService.attachJavascript(payload.entryPoint);
1044
+ await this.moduleConnectionService.loadModule("./paper.module");
1045
+ }
858
1046
  {
859
1047
  this.exchangeSchemaService.addSchema();
860
1048
  this.symbolSchemaService.addSchema();
@@ -881,7 +1069,6 @@ class PaperMainService {
881
1069
  });
882
1070
  notifyVerbose();
883
1071
  }
884
- await this.moduleConnectionService.loadModule("./paper.module");
885
1072
  Live.background(symbol, {
886
1073
  strategyName,
887
1074
  exchangeName,
@@ -897,7 +1084,7 @@ class PaperMainService {
897
1084
  if (!values.paper) {
898
1085
  return;
899
1086
  }
900
- const entryPoint = getPositional();
1087
+ const [entryPoint = null] = getPositionals();
901
1088
  if (!entryPoint) {
902
1089
  throw new Error("Entry point is required");
903
1090
  }
@@ -1898,8 +2085,7 @@ class BabelService {
1898
2085
  }
1899
2086
  }
1900
2087
 
1901
- const TRANSPILE_FN = (code, self) => {
1902
- const require = self.getBaseRequire();
2088
+ const TRANSPILE_FN = memoize(([path]) => `${path}`, (path, code, self, require) => {
1903
2089
  const __filename = self.__filename;
1904
2090
  const __dirname = self.__dirname;
1905
2091
  const module = { exports: {} };
@@ -1908,7 +2094,7 @@ const TRANSPILE_FN = (code, self) => {
1908
2094
  eval(self.params.babel.transpile(code));
1909
2095
  }
1910
2096
  catch (error) {
1911
- console.log(`Error during transpilation error=\`${getErrorMessage(error)}\` __filename=\`${__filename}\` __dirname=\`${__dirname}\``);
2097
+ console.log(`Error during transpilation error=\`${getErrorMessage(error)}\` path=${path} __filename=\`${__filename}\` __dirname=\`${__dirname}\``);
1912
2098
  process.exit(-1);
1913
2099
  }
1914
2100
  return {
@@ -1918,18 +2104,18 @@ const TRANSPILE_FN = (code, self) => {
1918
2104
  exports,
1919
2105
  module,
1920
2106
  };
1921
- };
1922
- const REQUIRE_ENTRY_FACTORY = (filePath, self) => {
2107
+ });
2108
+ const REQUIRE_ENTRY_FACTORY = (filePath, self, seen) => {
1923
2109
  {
1924
2110
  return null;
1925
2111
  }
1926
2112
  };
1927
- const BABEL_ENTRY_FACTORY = (filePath, self) => {
2113
+ const BABEL_ENTRY_FACTORY = (filePath, self, seen) => {
1928
2114
  try {
1929
2115
  const resolvedPath = path.resolve(self.__dirname, filePath);
1930
2116
  const code = fs.readFileSync(resolvedPath, "utf-8");
1931
2117
  const child = self.fork(path.dirname(resolvedPath));
1932
- const { module } = TRANSPILE_FN(code, child);
2118
+ const { module } = TRANSPILE_FN(resolvedPath, code, child, CREATE_BASE_REQUIRE_FN(child, seen));
1933
2119
  return "default" in module.exports
1934
2120
  ? module.exports.default
1935
2121
  : module.exports;
@@ -1970,21 +2156,20 @@ const GET_RESOLVED_EXT_FN = (filePath) => {
1970
2156
  }
1971
2157
  return filePath;
1972
2158
  };
1973
- const ENTRY_FACTORY = (filePath, self) => {
1974
- filePath = GET_RESOLVED_EXT_FN(filePath);
2159
+ const ENTRY_FACTORY = (filePath, self, seen) => {
1975
2160
  {
1976
2161
  let result = null;
1977
2162
  if ((result = REQUIRE_ENTRY_FACTORY())) {
1978
2163
  return result;
1979
2164
  }
1980
- if ((result = BABEL_ENTRY_FACTORY(filePath, self))) {
2165
+ if ((result = BABEL_ENTRY_FACTORY(filePath, self, seen))) {
1981
2166
  return result;
1982
2167
  }
1983
2168
  }
1984
2169
  throw new Error(`Failed to load module at ${filePath} (basepath: ${self.params.path})`);
1985
2170
  };
1986
- const CREATE_BASE_REQUIRE_FN = (self) => {
1987
- const baseRequire = createRequire(self.__filename);
2171
+ const CREATE_BASE_REQUIRE_FN = (self, seen) => {
2172
+ const baseRequire = self.baseRequire();
1988
2173
  return new Proxy(baseRequire, {
1989
2174
  apply(_target, _this, args) {
1990
2175
  const id = args[0];
@@ -2005,7 +2190,7 @@ const CREATE_BASE_REQUIRE_FN = (self) => {
2005
2190
  if (id.startsWith("./") || id.startsWith("../")) {
2006
2191
  const resolved = path.resolve(self.__dirname, id);
2007
2192
  const child = self.fork(path.dirname(resolved));
2008
- return child.import(resolved);
2193
+ return child.import(resolved, seen);
2009
2194
  }
2010
2195
  return baseRequire(id);
2011
2196
  },
@@ -2019,11 +2204,11 @@ const BacktestKitCli = new Proxy({}, {
2019
2204
  class ClientLoader {
2020
2205
  constructor(params) {
2021
2206
  this.params = params;
2022
- this.getBaseRequire = singleshot(() => {
2023
- this.params.logger.log("ClientLoader getBaseRequire", {
2207
+ this.baseRequire = singleshot(() => {
2208
+ this.params.logger.log("ClientLoader baseRequire", {
2024
2209
  basePath: this.params.path,
2025
2210
  });
2026
- return CREATE_BASE_REQUIRE_FN(this);
2211
+ return createRequire(this.__filename);
2027
2212
  });
2028
2213
  this.__filename = path.join(params.path, "index.cjs");
2029
2214
  this.__dirname = path.dirname(this.__filename);
@@ -2039,12 +2224,21 @@ class ClientLoader {
2039
2224
  logger: this.params.logger,
2040
2225
  });
2041
2226
  }
2042
- import(filePath) {
2227
+ import(filePath, seen = new Set()) {
2043
2228
  this.params.logger.log("ClientLoader import", {
2044
2229
  filePath,
2045
2230
  basePath: this.params.path,
2046
2231
  });
2047
- return ENTRY_FACTORY(filePath, this);
2232
+ const resolved = GET_RESOLVED_EXT_FN(filePath);
2233
+ if (seen.has(resolved)) {
2234
+ throw new Error(`Circular dependency detected: ${resolved} (seen: ${[...seen].join("->")}->${resolved})`);
2235
+ }
2236
+ const currentSeen = new Set(seen);
2237
+ if (!seen.size) {
2238
+ currentSeen.add(path.resolve(this.__dirname, filePath));
2239
+ }
2240
+ currentSeen.add(resolved);
2241
+ return ENTRY_FACTORY(resolved, this, currentSeen);
2048
2242
  }
2049
2243
  check(filePath) {
2050
2244
  this.params.logger.log("ClientLoader check", {
@@ -2112,6 +2306,7 @@ class LoaderService {
2112
2306
  }
2113
2307
  {
2114
2308
  provide(TYPES.backtestMainService, () => new BacktestMainService());
2309
+ provide(TYPES.walkerMainService, () => new WalkerMainService());
2115
2310
  provide(TYPES.paperMainService, () => new PaperMainService());
2116
2311
  provide(TYPES.liveMainService, () => new LiveMainService());
2117
2312
  }
@@ -2151,6 +2346,7 @@ const connectionServices = {
2151
2346
  };
2152
2347
  const mainServices = {
2153
2348
  backtestMainService: inject(TYPES.backtestMainService),
2349
+ walkerMainService: inject(TYPES.walkerMainService),
2154
2350
  paperMainService: inject(TYPES.paperMainService),
2155
2351
  liveMainService: inject(TYPES.liveMainService),
2156
2352
  };
@@ -2186,8 +2382,14 @@ const cli = {
2186
2382
  };
2187
2383
  init();
2188
2384
 
2189
- const MODES = ["backtest", "paper", "live", "pine", "dump", "init", "help", "version"];
2190
- const main$a = async () => {
2385
+ const MODES = ["backtest", "walker", "paper", "live", "pine", "dump", "init", "help", "version"];
2386
+ const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
2387
+ const HELP_TEXT$1 = `
2388
+ Example:
2389
+
2390
+ node ${ENTRY_PATH$1} --help
2391
+ `.trimStart();
2392
+ const main$b = async () => {
2191
2393
  if (!getEntry(import.meta.url)) {
2192
2394
  return;
2193
2395
  }
@@ -2195,18 +2397,21 @@ const main$a = async () => {
2195
2397
  if (MODES.some((mode) => values[mode])) {
2196
2398
  return;
2197
2399
  }
2198
- process.stdout.write(`@backtest-kit/cli ${"6.1.1"}\n`);
2400
+ process.stdout.write(`@backtest-kit/cli ${"6.2.0"}\n`);
2401
+ process.stdout.write("\n");
2199
2402
  process.stdout.write(`Run with --help to see available commands.\n`);
2403
+ process.stdout.write("\n");
2404
+ process.stdout.write(HELP_TEXT$1);
2200
2405
  process.exit(0);
2201
2406
  };
2202
- main$a();
2407
+ main$b();
2203
2408
 
2204
2409
  const notifyShutdown = singleshot(async () => {
2205
2410
  console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
2206
2411
  });
2207
2412
 
2208
- const BEFORE_EXIT_FN$4 = singleshot(async () => {
2209
- process.off("SIGINT", BEFORE_EXIT_FN$4);
2413
+ const BEFORE_EXIT_FN$5 = singleshot(async () => {
2414
+ process.off("SIGINT", BEFORE_EXIT_FN$5);
2210
2415
  const [running = null] = await Backtest.list();
2211
2416
  if (!running) {
2212
2417
  return;
@@ -2222,6 +2427,35 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
2222
2427
  frameName,
2223
2428
  });
2224
2429
  });
2430
+ const listenGracefulShutdown$5 = singleshot(() => {
2431
+ process.on("SIGINT", BEFORE_EXIT_FN$5);
2432
+ });
2433
+ const main$a = async () => {
2434
+ if (!getEntry(import.meta.url)) {
2435
+ return;
2436
+ }
2437
+ const { values } = getArgs();
2438
+ if (!values.backtest) {
2439
+ return;
2440
+ }
2441
+ await cli.backtestMainService.connect();
2442
+ listenGracefulShutdown$5();
2443
+ };
2444
+ main$a();
2445
+
2446
+ const BEFORE_EXIT_FN$4 = singleshot(async () => {
2447
+ process.off("SIGINT", BEFORE_EXIT_FN$4);
2448
+ const [running = null] = await Walker.list();
2449
+ if (!running) {
2450
+ return;
2451
+ }
2452
+ notifyShutdown();
2453
+ const { walkerName, symbol, status } = running;
2454
+ if (status === "fulfilled") {
2455
+ return;
2456
+ }
2457
+ Walker.stop(symbol, { walkerName });
2458
+ });
2225
2459
  const listenGracefulShutdown$4 = singleshot(() => {
2226
2460
  process.on("SIGINT", BEFORE_EXIT_FN$4);
2227
2461
  });
@@ -2230,11 +2464,11 @@ const main$9 = async () => {
2230
2464
  return;
2231
2465
  }
2232
2466
  const { values } = getArgs();
2233
- if (!values.backtest) {
2467
+ if (!values.walker) {
2234
2468
  return;
2235
2469
  }
2236
- await cli.backtestMainService.connect();
2237
2470
  listenGracefulShutdown$4();
2471
+ await cli.walkerMainService.connect();
2238
2472
  };
2239
2473
  main$9();
2240
2474
 
@@ -2370,7 +2604,7 @@ const main$4 = async () => {
2370
2604
  if (!values.pine) {
2371
2605
  return;
2372
2606
  }
2373
- const entryPoint = getPositional();
2607
+ const [entryPoint = null] = getPositionals();
2374
2608
  if (!entryPoint) {
2375
2609
  return;
2376
2610
  }
@@ -2590,103 +2824,122 @@ const main$2 = async () => {
2590
2824
  main$2();
2591
2825
 
2592
2826
  const ENTRY_PATH = "./node_modules/@backtest-kit/cli/build/index.mjs";
2593
- const HELP_TEXT = `
2594
- @backtest-kit/cli
2595
-
2596
- Usage:
2597
- node index.mjs --<mode> [flags] [entry-point]
2598
-
2599
- Modes:
2600
-
2601
- --backtest <entry> Run strategy against historical candle data
2602
- --paper <entry> Paper trading (live prices, no real orders)
2603
- --live <entry> Live trading with real orders
2604
- --pine <entry> Execute a local .pine indicator file
2605
- --dump Fetch and save raw OHLCV candles
2606
- --init Scaffold a new project in the current directory
2607
- --help Print this help message
2608
-
2609
- Backtest flags:
2610
-
2611
- --symbol <string> Trading pair (default: BTCUSDT)
2612
- --strategy <string> Strategy name from addStrategySchema (default: first registered)
2613
- --exchange <string> Exchange name from addExchangeSchema (default: first registered)
2614
- --frame <string> Frame name from addFrameSchema (default: first registered)
2615
- --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
2616
- --noCache Skip candle cache warming before the run
2617
- --verbose Log every candle fetch to stdout
2618
- --ui Start web dashboard at http://localhost:60050
2619
- --telegram Send trade notifications to Telegram
2620
-
2621
- Paper / Live flags:
2622
-
2623
- --symbol <string> Trading pair (default: BTCUSDT)
2624
- --strategy <string> Strategy name (default: first registered)
2625
- --exchange <string> Exchange name (default: first registered)
2626
- --verbose Log every candle fetch to stdout
2627
- --ui Start web dashboard
2628
- --telegram Send Telegram notifications
2629
-
2630
- PineScript flags (--pine):
2631
-
2632
- --symbol <string> Trading pair (default: BTCUSDT)
2633
- --timeframe <string> Candle interval (default: 15m)
2634
- --limit <string> Number of candles to fetch (default: 250)
2635
- --when <string> End date ISO 8601 or Unix ms (default: now)
2636
- --exchange <string> Exchange name (default: first registered)
2637
- --output <string> Output file base name without extension
2638
- --json Save output as JSON array to <pine-dir>/dump/<output>.json
2639
- --jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
2640
- --markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
2641
-
2642
- Only plot() calls with display=display.data_window produce output columns.
2643
- Module file ./modules/pine.module is loaded automatically if it exists.
2644
-
2645
- Candle dump flags (--dump):
2646
-
2647
- --symbol <string> Trading pair (default: BTCUSDT)
2648
- --timeframe <string> Candle interval (default: 15m)
2649
- --limit <string> Number of candles (default: 250)
2650
- --when <string> End date ISO 8601 or Unix ms (default: now)
2651
- --exchange <string> Exchange name (default: first registered)
2652
- --output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
2653
- --json Save as JSON array to ./dump/<output>.json
2654
- --jsonl Save as JSONL to ./dump/<output>.jsonl
2655
-
2656
- Module file ./modules/dump.module is loaded automatically if it exists.
2657
-
2658
- Init flags (--init):
2659
-
2660
- --output <string> Target directory name (default: backtest-kit-project)
2661
-
2662
- Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
2663
-
2664
- Module hooks (loaded automatically by each mode):
2665
-
2666
- modules/backtest.module --backtest Broker adapter for backtest
2667
- modules/paper.module --paper Broker adapter for paper trading
2668
- modules/live.module --live Broker adapter for live trading
2669
- modules/pine.module --pine Exchange schema for PineScript runs
2670
- modules/dump.module --dump Exchange schema for candle dumps
2671
-
2672
- Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
2673
-
2674
- Environment variables:
2675
-
2676
- CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
2677
- CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
2678
- CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
2679
- CC_WWWROOT_PORT UI server port (default: 60050)
2680
-
2681
- Examples:
2682
-
2683
- node ${ENTRY_PATH} --backtest ./content/feb_2026.strategy.ts
2684
- node ${ENTRY_PATH} --backtest --symbol BTCUSDT --noCache --ui ./content/feb_2026.strategy.ts
2685
- node ${ENTRY_PATH} --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
2686
- node ${ENTRY_PATH} --live --ui --telegram ./content/feb_2026.strategy.ts
2687
- node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
2688
- node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
2689
- node ${ENTRY_PATH} --init --output my-trading-bot
2827
+ const HELP_TEXT = `
2828
+ Usage:
2829
+ node index.mjs --<mode> [flags] [entry-point]
2830
+
2831
+ Modes:
2832
+
2833
+ --backtest <entry> Run strategy against historical candle data
2834
+ --walker <entry...> Run Walker A/B strategy comparison across multiple strategies
2835
+ --paper <entry> Paper trading (live prices, no real orders)
2836
+ --live <entry> Live trading with real orders
2837
+ --pine <entry> Execute a local .pine indicator file
2838
+ --dump Fetch and save raw OHLCV candles
2839
+ --init Scaffold a new project in the current directory
2840
+ --help Print this help message
2841
+
2842
+ Backtest flags:
2843
+
2844
+ --symbol <string> Trading pair (default: BTCUSDT)
2845
+ --strategy <string> Strategy name from addStrategySchema (default: first registered)
2846
+ --exchange <string> Exchange name from addExchangeSchema (default: first registered)
2847
+ --frame <string> Frame name from addFrameSchema (default: first registered)
2848
+ --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
2849
+ --noCache Skip candle cache warming before the run
2850
+ --verbose Log every candle fetch to stdout
2851
+ --ui Start web dashboard at http://localhost:60050
2852
+ --telegram Send trade notifications to Telegram
2853
+
2854
+ Walker flags (--walker):
2855
+
2856
+ --symbol <string> Trading pair (default: BTCUSDT)
2857
+ --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
2858
+ --noCache Skip candle cache warming before the run
2859
+ --verbose Log every candle fetch to stdout
2860
+ --output <string> Output file base name (default: walker_{SYMBOL}_{TIMESTAMP})
2861
+ --json Save results as JSON to ./dump/<output>.json
2862
+ --markdown Save report as Markdown to ./dump/<output>.md
2863
+
2864
+ Each positional argument is a strategy entry point. All strategy files are loaded without
2865
+ changing process.cwd() — .env is read from the working directory only.
2866
+ addWalkerSchema is called automatically using the registered exchange and frame.
2867
+ After comparison completes the report is printed to stdout (or saved if --json/--markdown).
2868
+
2869
+ Module file ./modules/walker.module is loaded automatically if it exists.
2870
+
2871
+ Paper / Live flags:
2872
+
2873
+ --symbol <string> Trading pair (default: BTCUSDT)
2874
+ --strategy <string> Strategy name (default: first registered)
2875
+ --exchange <string> Exchange name (default: first registered)
2876
+ --verbose Log every candle fetch to stdout
2877
+ --ui Start web dashboard
2878
+ --telegram Send Telegram notifications
2879
+
2880
+ PineScript flags (--pine):
2881
+
2882
+ --symbol <string> Trading pair (default: BTCUSDT)
2883
+ --timeframe <string> Candle interval (default: 15m)
2884
+ --limit <string> Number of candles to fetch (default: 250)
2885
+ --when <string> End date — ISO 8601 or Unix ms (default: now)
2886
+ --exchange <string> Exchange name (default: first registered)
2887
+ --output <string> Output file base name without extension
2888
+ --json Save output as JSON array to <pine-dir>/dump/<output>.json
2889
+ --jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
2890
+ --markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
2891
+
2892
+ Only plot() calls with display=display.data_window produce output columns.
2893
+ Module file ./modules/pine.module is loaded automatically if it exists.
2894
+
2895
+ Candle dump flags (--dump):
2896
+
2897
+ --symbol <string> Trading pair (default: BTCUSDT)
2898
+ --timeframe <string> Candle interval (default: 15m)
2899
+ --limit <string> Number of candles (default: 250)
2900
+ --when <string> End date ISO 8601 or Unix ms (default: now)
2901
+ --exchange <string> Exchange name (default: first registered)
2902
+ --output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
2903
+ --json Save as JSON array to ./dump/<output>.json
2904
+ --jsonl Save as JSONL to ./dump/<output>.jsonl
2905
+
2906
+ Module file ./modules/dump.module is loaded automatically if it exists.
2907
+
2908
+ Init flags (--init):
2909
+
2910
+ --output <string> Target directory name (default: backtest-kit-project)
2911
+
2912
+ Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
2913
+
2914
+ Module hooks (loaded automatically by each mode):
2915
+
2916
+ modules/backtest.module --backtest Broker adapter for backtest
2917
+ modules/walker.module --walker Broker adapter for walker comparison
2918
+ modules/paper.module --paper Broker adapter for paper trading
2919
+ modules/live.module --live Broker adapter for live trading
2920
+ modules/pine.module --pine Exchange schema for PineScript runs
2921
+ modules/dump.module --dump Exchange schema for candle dumps
2922
+
2923
+ Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
2924
+
2925
+ Environment variables:
2926
+
2927
+ CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
2928
+ CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
2929
+ CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
2930
+ CC_WWWROOT_PORT UI server port (default: 60050)
2931
+
2932
+ Examples:
2933
+
2934
+ node ${ENTRY_PATH} --backtest ./content/feb_2026.strategy.ts
2935
+ node ${ENTRY_PATH} --backtest --symbol BTCUSDT --noCache --ui ./content/feb_2026.strategy.ts
2936
+ node ${ENTRY_PATH} --walker ./content/feb_2026_v1.strategy.ts ./content/feb_2026_v2.strategy.ts ./content/feb_2026_v3.strategy.ts
2937
+ node ${ENTRY_PATH} --walker --symbol BTCUSDT --noCache --markdown ./content/feb_2026_v1.ts ./content/feb_2026_v2.ts
2938
+ node ${ENTRY_PATH} --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
2939
+ node ${ENTRY_PATH} --live --ui --telegram ./content/feb_2026.strategy.ts
2940
+ node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
2941
+ node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
2942
+ node ${ENTRY_PATH} --init --output my-trading-bot
2690
2943
  `.trimStart();
2691
2944
  const main$1 = async () => {
2692
2945
  if (!getEntry(import.meta.url)) {
@@ -2696,6 +2949,7 @@ const main$1 = async () => {
2696
2949
  if (!values.help) {
2697
2950
  return;
2698
2951
  }
2952
+ process.stdout.write(`@backtest-kit/cli ${"6.2.0"}\n\n`);
2699
2953
  process.stdout.write(HELP_TEXT);
2700
2954
  process.exit(0);
2701
2955
  };
@@ -2709,7 +2963,7 @@ const main = async () => {
2709
2963
  if (!values.version) {
2710
2964
  return;
2711
2965
  }
2712
- process.stdout.write(`@backtest-kit/cli ${"6.1.1"}\n`);
2966
+ process.stdout.write(`@backtest-kit/cli ${"6.2.0"}\n`);
2713
2967
  process.exit(0);
2714
2968
  };
2715
2969
  main();
@@ -2731,7 +2985,7 @@ async function run(mode, args) {
2731
2985
  }
2732
2986
  if (mode === "backtest") {
2733
2987
  await cli.backtestMainService.run(args);
2734
- listenGracefulShutdown$4();
2988
+ listenGracefulShutdown$5();
2735
2989
  return;
2736
2990
  }
2737
2991
  if (mode === "paper") {