@backtest-kit/cli 6.1.5 → 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.cjs CHANGED
@@ -249,6 +249,7 @@ const connectionServices$1 = {
249
249
  };
250
250
  const mainServices$1 = {
251
251
  backtestMainService: Symbol('backtestMainService'),
252
+ walkerMainService: Symbol('walkerMainService'),
252
253
  paperMainService: Symbol('paperMainService'),
253
254
  liveMainService: Symbol('liveMainService'),
254
255
  };
@@ -317,6 +318,19 @@ class ResolveService {
317
318
  _is_launched = true;
318
319
  return await fs$1.readFile(absolutePath, "utf-8");
319
320
  };
321
+ this.attachStrategy = async (jsPath) => {
322
+ this.loggerService.log("resolveService attachStrategy", {
323
+ jsPath
324
+ });
325
+ const absolutePath = path.resolve(jsPath);
326
+ await fs$1.access(absolutePath, fs.constants.F_OK | fs.constants.R_OK);
327
+ const moduleRoot = path.dirname(absolutePath);
328
+ {
329
+ const cwd = process.cwd();
330
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
331
+ }
332
+ this.loaderService.import(absolutePath, moduleRoot);
333
+ };
320
334
  this.attachJavascript = async (jsPath) => {
321
335
  this.loggerService.log("resolveService attachJavascript", {
322
336
  jsPath
@@ -484,6 +498,10 @@ const getArgs = functoolsKit.singleshot(() => {
484
498
  type: "boolean",
485
499
  default: false,
486
500
  },
501
+ walker: {
502
+ type: "boolean",
503
+ default: false,
504
+ },
487
505
  live: {
488
506
  type: "boolean",
489
507
  default: false,
@@ -574,12 +592,11 @@ const getArgs = functoolsKit.singleshot(() => {
574
592
  positionals,
575
593
  };
576
594
  });
577
- const getPositional = functoolsKit.singleshot(() => {
595
+ const getPositionals = functoolsKit.singleshot(() => {
578
596
  const { positionals = [] } = getArgs();
579
- const result = positionals
597
+ return positionals
580
598
  .filter((value) => !DISALLOWED_PATHS.some((path) => value.includes(path)))
581
- .find((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
582
- return result || null;
599
+ .filter((value) => ALLOWED_EXTENSIONS.some((ext) => value.endsWith(ext)));
583
600
  });
584
601
 
585
602
  const ADD_FRAME_FN = (self) => {
@@ -684,11 +701,11 @@ const notifyVerbose = functoolsKit.singleshot(() => {
684
701
  });
685
702
  });
686
703
 
687
- const DEFAULT_CACHE_LIST = ["1m", "15m", "30m", "1h", "4h"];
688
- const GET_CACHE_INTERVAL_LIST_FN = () => {
704
+ const DEFAULT_CACHE_LIST$1 = ["1m", "15m", "30m", "1h", "4h"];
705
+ const GET_CACHE_INTERVAL_LIST_FN$1 = () => {
689
706
  const { values } = getArgs();
690
707
  if (!values.cacheInterval) {
691
- return DEFAULT_CACHE_LIST;
708
+ return DEFAULT_CACHE_LIST$1;
692
709
  }
693
710
  return String(values.cacheInterval)
694
711
  .split(",")
@@ -713,7 +730,10 @@ class BacktestMainService {
713
730
  this.frontendProviderService.connect();
714
731
  this.telegramProviderService.connect();
715
732
  }
716
- await this.resolveService.attachJavascript(payload.entryPoint);
733
+ {
734
+ await this.resolveService.attachJavascript(payload.entryPoint);
735
+ await this.moduleConnectionService.loadModule("./backtest.module");
736
+ }
717
737
  {
718
738
  this.exchangeSchemaService.addSchema();
719
739
  this.symbolSchemaService.addSchema();
@@ -753,7 +773,6 @@ class BacktestMainService {
753
773
  });
754
774
  notifyVerbose();
755
775
  }
756
- await this.moduleConnectionService.loadModule("./backtest.module");
757
776
  BacktestKit.Backtest.background(symbol, {
758
777
  strategyName,
759
778
  frameName,
@@ -770,11 +789,11 @@ class BacktestMainService {
770
789
  if (!values.backtest) {
771
790
  return;
772
791
  }
773
- const entryPoint = getPositional();
792
+ const [entryPoint = null] = getPositionals();
774
793
  if (!entryPoint) {
775
794
  throw new Error("Entry point is required");
776
795
  }
777
- const cacheInterval = GET_CACHE_INTERVAL_LIST_FN();
796
+ const cacheInterval = GET_CACHE_INTERVAL_LIST_FN$1();
778
797
  return await this.run({
779
798
  symbol: values.symbol,
780
799
  entryPoint,
@@ -789,6 +808,170 @@ class BacktestMainService {
789
808
  }
790
809
  }
791
810
 
811
+ const DEFAULT_CACHE_LIST = ["1m", "15m", "30m", "1h", "4h"];
812
+ const WALKER_NAME = "cli-walker";
813
+ const GET_CACHE_INTERVAL_LIST_FN = () => {
814
+ const { values } = getArgs();
815
+ if (!values.cacheInterval) {
816
+ return DEFAULT_CACHE_LIST;
817
+ }
818
+ return String(values.cacheInterval)
819
+ .split(",")
820
+ .map((timeframe) => timeframe.trim());
821
+ };
822
+ class WalkerMainService {
823
+ constructor() {
824
+ this.loggerService = inject(TYPES.loggerService);
825
+ this.resolveService = inject(TYPES.resolveService);
826
+ this.exchangeSchemaService = inject(TYPES.exchangeSchemaService);
827
+ this.symbolSchemaService = inject(TYPES.symbolSchemaService);
828
+ this.cacheLogicService = inject(TYPES.cacheLogicService);
829
+ this.moduleConnectionService = inject(TYPES.moduleConnectionService);
830
+ this.run = functoolsKit.singleshot(async (payload) => {
831
+ this.loggerService.log("walkerMainService run", { payload });
832
+ for (const entryPoint of payload.entryPoints) {
833
+ await this.resolveService.attachStrategy(entryPoint);
834
+ }
835
+ await this.moduleConnectionService.loadModule("./walker.module");
836
+ {
837
+ this.exchangeSchemaService.addSchema();
838
+ this.symbolSchemaService.addSchema();
839
+ }
840
+ {
841
+ const { length } = await BacktestKit.listFrameSchema();
842
+ if (!length) {
843
+ const endDate = BacktestKit.alignToInterval(new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), "1m");
844
+ const startDate = BacktestKit.alignToInterval(new Date(Date.now() - 31 * 24 * 60 * 60 * 1000), "1m");
845
+ 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.`);
846
+ BacktestKit.addFrameSchema({
847
+ frameName: FrameName$1.DefaultFrame,
848
+ interval: "1m",
849
+ startDate,
850
+ endDate,
851
+ });
852
+ }
853
+ }
854
+ const symbol = payload.symbol || "BTCUSDT";
855
+ const strategyList = await BacktestKit.listStrategySchema();
856
+ const strategyNames = strategyList.map((s) => s.strategyName);
857
+ if (!strategyNames.length) {
858
+ throw new Error("No strategies found in provided entry points");
859
+ }
860
+ const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
861
+ const [defaultFrameName = null] = await BacktestKit.listFrameSchema();
862
+ const exchangeName = defaultExchangeName?.exchangeName;
863
+ const frameName = defaultFrameName?.frameName;
864
+ if (!exchangeName) {
865
+ throw new Error("Exchange name is required");
866
+ }
867
+ if (!frameName) {
868
+ throw new Error("Frame name is required");
869
+ }
870
+ BacktestKit.addWalkerSchema({
871
+ walkerName: WALKER_NAME,
872
+ exchangeName,
873
+ frameName,
874
+ strategies: strategyNames,
875
+ });
876
+ if (!payload.noCache) {
877
+ await this.cacheLogicService.execute(payload.cacheInterval, {
878
+ exchangeName,
879
+ frameName,
880
+ symbol,
881
+ });
882
+ }
883
+ if (payload.verbose) {
884
+ BacktestKit.overrideExchangeSchema({
885
+ exchangeName,
886
+ callbacks: {
887
+ onCandleData(symbol, interval, since) {
888
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
889
+ },
890
+ },
891
+ });
892
+ notifyVerbose();
893
+ }
894
+ if (payload.verbose) {
895
+ BacktestKit.overrideWalkerSchema({
896
+ walkerName: WALKER_NAME,
897
+ callbacks: {
898
+ onStrategyStart(strategyName, symbol) {
899
+ console.log(`Strategy started: ${strategyName} for symbol: ${symbol}`);
900
+ },
901
+ onStrategyError(strategyName, symbol, error) {
902
+ console.error(`Strategy error: ${strategyName} for symbol: ${symbol}`, error);
903
+ },
904
+ onStrategyComplete(strategyName, symbol) {
905
+ console.log(`Strategy completed: ${strategyName} for symbol: ${symbol}`);
906
+ },
907
+ onComplete(results) {
908
+ console.log(`Walker completed for symbol: ${results.symbol}`, results);
909
+ }
910
+ }
911
+ });
912
+ }
913
+ BacktestKit.Walker.background(symbol, { walkerName: WALKER_NAME });
914
+ const [awaiter, { resolve: res }] = functoolsKit.createAwaiter();
915
+ const unWalker = BacktestKit.listenDoneWalker(() => {
916
+ console.log("Walker comparison finished");
917
+ unWalker();
918
+ res();
919
+ });
920
+ payload.verbose && console.time("Walker");
921
+ await awaiter;
922
+ payload.verbose && console.timeEnd("Walker");
923
+ const dumpName = payload.output || `walker_${symbol}_${Date.now()}`;
924
+ const dumpDir = path.join(process.cwd(), "dump");
925
+ if (payload.json) {
926
+ const filePath = path.resolve(dumpDir, `${dumpName}.json`);
927
+ const data = await BacktestKit.Walker.getData(symbol, { walkerName: WALKER_NAME });
928
+ await fs$1.mkdir(dumpDir, { recursive: true });
929
+ await fs$1.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
930
+ console.log(`Saved: ${filePath}`);
931
+ process.exit(0);
932
+ return;
933
+ }
934
+ if (payload.markdown) {
935
+ const filePath = path.resolve(dumpDir, `${dumpName}.md`);
936
+ const report = await BacktestKit.Walker.getReport(symbol, { walkerName: WALKER_NAME });
937
+ await fs$1.mkdir(dumpDir, { recursive: true });
938
+ await fs$1.writeFile(filePath, report, "utf-8");
939
+ console.log(`Saved: ${filePath}`);
940
+ process.exit(0);
941
+ return;
942
+ }
943
+ const report = await BacktestKit.Walker.getReport(symbol, { walkerName: WALKER_NAME });
944
+ console.log(report);
945
+ process.exit(0);
946
+ });
947
+ this.connect = functoolsKit.singleshot(async () => {
948
+ this.loggerService.log("walkerMainService connect");
949
+ 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)))) {
950
+ return;
951
+ }
952
+ const { values } = getArgs();
953
+ if (!values.walker) {
954
+ return;
955
+ }
956
+ const entryPoints = getPositionals();
957
+ if (!entryPoints.length) {
958
+ throw new Error("At least one entry point is required");
959
+ }
960
+ const cacheInterval = GET_CACHE_INTERVAL_LIST_FN();
961
+ return await this.run({
962
+ entryPoints,
963
+ json: values.json,
964
+ markdown: values.markdown,
965
+ symbol: values.symbol,
966
+ output: values.output,
967
+ cacheInterval,
968
+ verbose: values.verbose,
969
+ noCache: values.noCache,
970
+ });
971
+ });
972
+ }
973
+ }
974
+
792
975
  class LiveMainService {
793
976
  constructor() {
794
977
  this.loggerService = inject(TYPES.loggerService);
@@ -806,7 +989,10 @@ class LiveMainService {
806
989
  this.frontendProviderService.connect();
807
990
  this.telegramProviderService.connect();
808
991
  }
809
- await this.resolveService.attachJavascript(payload.entryPoint);
992
+ {
993
+ await this.resolveService.attachJavascript(payload.entryPoint);
994
+ await this.moduleConnectionService.loadModule("./live.module");
995
+ }
810
996
  {
811
997
  this.exchangeSchemaService.addSchema();
812
998
  this.symbolSchemaService.addSchema();
@@ -833,7 +1019,6 @@ class LiveMainService {
833
1019
  });
834
1020
  notifyVerbose();
835
1021
  }
836
- await this.moduleConnectionService.loadModule("./live.module");
837
1022
  BacktestKit.Live.background(symbol, {
838
1023
  strategyName,
839
1024
  exchangeName,
@@ -849,7 +1034,7 @@ class LiveMainService {
849
1034
  if (!values.live) {
850
1035
  return;
851
1036
  }
852
- const entryPoint = getPositional();
1037
+ const [entryPoint = null] = getPositionals();
853
1038
  if (!entryPoint) {
854
1039
  throw new Error("Entry point is required");
855
1040
  }
@@ -879,7 +1064,10 @@ class PaperMainService {
879
1064
  this.frontendProviderService.connect();
880
1065
  this.telegramProviderService.connect();
881
1066
  }
882
- await this.resolveService.attachJavascript(payload.entryPoint);
1067
+ {
1068
+ await this.resolveService.attachJavascript(payload.entryPoint);
1069
+ await this.moduleConnectionService.loadModule("./paper.module");
1070
+ }
883
1071
  {
884
1072
  this.exchangeSchemaService.addSchema();
885
1073
  this.symbolSchemaService.addSchema();
@@ -906,7 +1094,6 @@ class PaperMainService {
906
1094
  });
907
1095
  notifyVerbose();
908
1096
  }
909
- await this.moduleConnectionService.loadModule("./paper.module");
910
1097
  BacktestKit.Live.background(symbol, {
911
1098
  strategyName,
912
1099
  exchangeName,
@@ -922,7 +1109,7 @@ class PaperMainService {
922
1109
  if (!values.paper) {
923
1110
  return;
924
1111
  }
925
- const entryPoint = getPositional();
1112
+ const [entryPoint = null] = getPositionals();
926
1113
  if (!entryPoint) {
927
1114
  throw new Error("Entry point is required");
928
1115
  }
@@ -1923,8 +2110,7 @@ class BabelService {
1923
2110
  }
1924
2111
  }
1925
2112
 
1926
- const TRANSPILE_FN = (code, self) => {
1927
- const require = self.getBaseRequire();
2113
+ const TRANSPILE_FN = functoolsKit.memoize(([path]) => `${path}`, (path, code, self, require) => {
1928
2114
  const __filename = self.__filename;
1929
2115
  const __dirname = self.__dirname;
1930
2116
  const module = { exports: {} };
@@ -1933,7 +2119,7 @@ const TRANSPILE_FN = (code, self) => {
1933
2119
  eval(self.params.babel.transpile(code));
1934
2120
  }
1935
2121
  catch (error) {
1936
- console.log(`Error during transpilation error=\`${functoolsKit.getErrorMessage(error)}\` __filename=\`${__filename}\` __dirname=\`${__dirname}\``);
2122
+ console.log(`Error during transpilation error=\`${functoolsKit.getErrorMessage(error)}\` path=${path} __filename=\`${__filename}\` __dirname=\`${__dirname}\``);
1937
2123
  process.exit(-1);
1938
2124
  }
1939
2125
  return {
@@ -1943,9 +2129,9 @@ const TRANSPILE_FN = (code, self) => {
1943
2129
  exports,
1944
2130
  module,
1945
2131
  };
1946
- };
1947
- const REQUIRE_ENTRY_FACTORY = (filePath, self) => {
1948
- const baseRequire = self.getBaseRequire();
2132
+ });
2133
+ const REQUIRE_ENTRY_FACTORY = (filePath, self, seen) => {
2134
+ const baseRequire = CREATE_BASE_REQUIRE_FN(self, seen);
1949
2135
  try {
1950
2136
  return baseRequire(filePath);
1951
2137
  }
@@ -1953,12 +2139,12 @@ const REQUIRE_ENTRY_FACTORY = (filePath, self) => {
1953
2139
  return null;
1954
2140
  }
1955
2141
  };
1956
- const BABEL_ENTRY_FACTORY = (filePath, self) => {
2142
+ const BABEL_ENTRY_FACTORY = (filePath, self, seen) => {
1957
2143
  try {
1958
2144
  const resolvedPath = path.resolve(self.__dirname, filePath);
1959
2145
  const code = fs.readFileSync(resolvedPath, "utf-8");
1960
2146
  const child = self.fork(path.dirname(resolvedPath));
1961
- const { module } = TRANSPILE_FN(code, child);
2147
+ const { module } = TRANSPILE_FN(resolvedPath, code, child, CREATE_BASE_REQUIRE_FN(child, seen));
1962
2148
  return "default" in module.exports
1963
2149
  ? module.exports.default
1964
2150
  : module.exports;
@@ -1999,21 +2185,20 @@ const GET_RESOLVED_EXT_FN = (filePath) => {
1999
2185
  }
2000
2186
  return filePath;
2001
2187
  };
2002
- const ENTRY_FACTORY = (filePath, self) => {
2003
- filePath = GET_RESOLVED_EXT_FN(filePath);
2188
+ const ENTRY_FACTORY = (filePath, self, seen) => {
2004
2189
  {
2005
2190
  let result = null;
2006
- if ((result = REQUIRE_ENTRY_FACTORY(filePath, self))) {
2191
+ if ((result = REQUIRE_ENTRY_FACTORY(filePath, self, seen))) {
2007
2192
  return result;
2008
2193
  }
2009
- if ((result = BABEL_ENTRY_FACTORY(filePath, self))) {
2194
+ if ((result = BABEL_ENTRY_FACTORY(filePath, self, seen))) {
2010
2195
  return result;
2011
2196
  }
2012
2197
  }
2013
2198
  throw new Error(`Failed to load module at ${filePath} (basepath: ${self.params.path})`);
2014
2199
  };
2015
- const CREATE_BASE_REQUIRE_FN = (self) => {
2016
- const baseRequire = module$1.createRequire(self.__filename);
2200
+ const CREATE_BASE_REQUIRE_FN = (self, seen) => {
2201
+ const baseRequire = self.baseRequire();
2017
2202
  return new Proxy(baseRequire, {
2018
2203
  apply(_target, _this, args) {
2019
2204
  const id = args[0];
@@ -2034,7 +2219,7 @@ const CREATE_BASE_REQUIRE_FN = (self) => {
2034
2219
  if (id.startsWith("./") || id.startsWith("../")) {
2035
2220
  const resolved = path.resolve(self.__dirname, id);
2036
2221
  const child = self.fork(path.dirname(resolved));
2037
- return child.import(resolved);
2222
+ return child.import(resolved, seen);
2038
2223
  }
2039
2224
  return baseRequire(id);
2040
2225
  },
@@ -2048,11 +2233,11 @@ const BacktestKitCli = new Proxy({}, {
2048
2233
  class ClientLoader {
2049
2234
  constructor(params) {
2050
2235
  this.params = params;
2051
- this.getBaseRequire = functoolsKit.singleshot(() => {
2052
- this.params.logger.log("ClientLoader getBaseRequire", {
2236
+ this.baseRequire = functoolsKit.singleshot(() => {
2237
+ this.params.logger.log("ClientLoader baseRequire", {
2053
2238
  basePath: this.params.path,
2054
2239
  });
2055
- return CREATE_BASE_REQUIRE_FN(this);
2240
+ return module$1.createRequire(this.__filename);
2056
2241
  });
2057
2242
  this.__filename = path.join(params.path, "index.cjs");
2058
2243
  this.__dirname = path.dirname(this.__filename);
@@ -2068,12 +2253,21 @@ class ClientLoader {
2068
2253
  logger: this.params.logger,
2069
2254
  });
2070
2255
  }
2071
- import(filePath) {
2256
+ import(filePath, seen = new Set()) {
2072
2257
  this.params.logger.log("ClientLoader import", {
2073
2258
  filePath,
2074
2259
  basePath: this.params.path,
2075
2260
  });
2076
- return ENTRY_FACTORY(filePath, this);
2261
+ const resolved = GET_RESOLVED_EXT_FN(filePath);
2262
+ if (seen.has(resolved)) {
2263
+ throw new Error(`Circular dependency detected: ${resolved} (seen: ${[...seen].join("->")}->${resolved})`);
2264
+ }
2265
+ const currentSeen = new Set(seen);
2266
+ if (!seen.size) {
2267
+ currentSeen.add(path.resolve(this.__dirname, filePath));
2268
+ }
2269
+ currentSeen.add(resolved);
2270
+ return ENTRY_FACTORY(resolved, this, currentSeen);
2077
2271
  }
2078
2272
  check(filePath) {
2079
2273
  this.params.logger.log("ClientLoader check", {
@@ -2141,6 +2335,7 @@ class LoaderService {
2141
2335
  }
2142
2336
  {
2143
2337
  provide(TYPES.backtestMainService, () => new BacktestMainService());
2338
+ provide(TYPES.walkerMainService, () => new WalkerMainService());
2144
2339
  provide(TYPES.paperMainService, () => new PaperMainService());
2145
2340
  provide(TYPES.liveMainService, () => new LiveMainService());
2146
2341
  }
@@ -2180,6 +2375,7 @@ const connectionServices = {
2180
2375
  };
2181
2376
  const mainServices = {
2182
2377
  backtestMainService: inject(TYPES.backtestMainService),
2378
+ walkerMainService: inject(TYPES.walkerMainService),
2183
2379
  paperMainService: inject(TYPES.paperMainService),
2184
2380
  liveMainService: inject(TYPES.liveMainService),
2185
2381
  };
@@ -2215,14 +2411,14 @@ const cli = {
2215
2411
  };
2216
2412
  init();
2217
2413
 
2218
- const MODES = ["backtest", "paper", "live", "pine", "dump", "init", "help", "version"];
2414
+ const MODES = ["backtest", "walker", "paper", "live", "pine", "dump", "init", "help", "version"];
2219
2415
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
2220
- const HELP_TEXT$1 = `
2221
- Example:
2222
-
2223
- node ${ENTRY_PATH$1} --help
2416
+ const HELP_TEXT$1 = `
2417
+ Example:
2418
+
2419
+ node ${ENTRY_PATH$1} --help
2224
2420
  `.trimStart();
2225
- const main$a = async () => {
2421
+ const main$b = async () => {
2226
2422
  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)))) {
2227
2423
  return;
2228
2424
  }
@@ -2230,21 +2426,21 @@ const main$a = async () => {
2230
2426
  if (MODES.some((mode) => values[mode])) {
2231
2427
  return;
2232
2428
  }
2233
- process.stdout.write(`@backtest-kit/cli ${"6.1.1"}\n`);
2429
+ process.stdout.write(`@backtest-kit/cli ${"6.2.0"}\n`);
2234
2430
  process.stdout.write("\n");
2235
2431
  process.stdout.write(`Run with --help to see available commands.\n`);
2236
2432
  process.stdout.write("\n");
2237
2433
  process.stdout.write(HELP_TEXT$1);
2238
2434
  process.exit(0);
2239
2435
  };
2240
- main$a();
2436
+ main$b();
2241
2437
 
2242
2438
  const notifyShutdown = functoolsKit.singleshot(async () => {
2243
2439
  console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
2244
2440
  });
2245
2441
 
2246
- const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
2247
- process.off("SIGINT", BEFORE_EXIT_FN$4);
2442
+ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
2443
+ process.off("SIGINT", BEFORE_EXIT_FN$5);
2248
2444
  const [running = null] = await BacktestKit.Backtest.list();
2249
2445
  if (!running) {
2250
2446
  return;
@@ -2260,6 +2456,35 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
2260
2456
  frameName,
2261
2457
  });
2262
2458
  });
2459
+ const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
2460
+ process.on("SIGINT", BEFORE_EXIT_FN$5);
2461
+ });
2462
+ const main$a = async () => {
2463
+ 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)))) {
2464
+ return;
2465
+ }
2466
+ const { values } = getArgs();
2467
+ if (!values.backtest) {
2468
+ return;
2469
+ }
2470
+ await cli.backtestMainService.connect();
2471
+ listenGracefulShutdown$5();
2472
+ };
2473
+ main$a();
2474
+
2475
+ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
2476
+ process.off("SIGINT", BEFORE_EXIT_FN$4);
2477
+ const [running = null] = await BacktestKit.Walker.list();
2478
+ if (!running) {
2479
+ return;
2480
+ }
2481
+ notifyShutdown();
2482
+ const { walkerName, symbol, status } = running;
2483
+ if (status === "fulfilled") {
2484
+ return;
2485
+ }
2486
+ BacktestKit.Walker.stop(symbol, { walkerName });
2487
+ });
2263
2488
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
2264
2489
  process.on("SIGINT", BEFORE_EXIT_FN$4);
2265
2490
  });
@@ -2268,11 +2493,11 @@ const main$9 = async () => {
2268
2493
  return;
2269
2494
  }
2270
2495
  const { values } = getArgs();
2271
- if (!values.backtest) {
2496
+ if (!values.walker) {
2272
2497
  return;
2273
2498
  }
2274
- await cli.backtestMainService.connect();
2275
2499
  listenGracefulShutdown$4();
2500
+ await cli.walkerMainService.connect();
2276
2501
  };
2277
2502
  main$9();
2278
2503
 
@@ -2408,7 +2633,7 @@ const main$4 = async () => {
2408
2633
  if (!values.pine) {
2409
2634
  return;
2410
2635
  }
2411
- const entryPoint = getPositional();
2636
+ const [entryPoint = null] = getPositionals();
2412
2637
  if (!entryPoint) {
2413
2638
  return;
2414
2639
  }
@@ -2628,103 +2853,122 @@ const main$2 = async () => {
2628
2853
  main$2();
2629
2854
 
2630
2855
  const ENTRY_PATH = "./node_modules/@backtest-kit/cli/build/index.mjs";
2631
- const HELP_TEXT = `
2632
- @backtest-kit/cli
2633
-
2634
- Usage:
2635
- node index.mjs --<mode> [flags] [entry-point]
2636
-
2637
- Modes:
2638
-
2639
- --backtest <entry> Run strategy against historical candle data
2640
- --paper <entry> Paper trading (live prices, no real orders)
2641
- --live <entry> Live trading with real orders
2642
- --pine <entry> Execute a local .pine indicator file
2643
- --dump Fetch and save raw OHLCV candles
2644
- --init Scaffold a new project in the current directory
2645
- --help Print this help message
2646
-
2647
- Backtest flags:
2648
-
2649
- --symbol <string> Trading pair (default: BTCUSDT)
2650
- --strategy <string> Strategy name from addStrategySchema (default: first registered)
2651
- --exchange <string> Exchange name from addExchangeSchema (default: first registered)
2652
- --frame <string> Frame name from addFrameSchema (default: first registered)
2653
- --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
2654
- --noCache Skip candle cache warming before the run
2655
- --verbose Log every candle fetch to stdout
2656
- --ui Start web dashboard at http://localhost:60050
2657
- --telegram Send trade notifications to Telegram
2658
-
2659
- Paper / Live flags:
2660
-
2661
- --symbol <string> Trading pair (default: BTCUSDT)
2662
- --strategy <string> Strategy name (default: first registered)
2663
- --exchange <string> Exchange name (default: first registered)
2664
- --verbose Log every candle fetch to stdout
2665
- --ui Start web dashboard
2666
- --telegram Send Telegram notifications
2667
-
2668
- PineScript flags (--pine):
2669
-
2670
- --symbol <string> Trading pair (default: BTCUSDT)
2671
- --timeframe <string> Candle interval (default: 15m)
2672
- --limit <string> Number of candles to fetch (default: 250)
2673
- --when <string> End date ISO 8601 or Unix ms (default: now)
2674
- --exchange <string> Exchange name (default: first registered)
2675
- --output <string> Output file base name without extension
2676
- --json Save output as JSON array to <pine-dir>/dump/<output>.json
2677
- --jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
2678
- --markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
2679
-
2680
- Only plot() calls with display=display.data_window produce output columns.
2681
- Module file ./modules/pine.module is loaded automatically if it exists.
2682
-
2683
- Candle dump flags (--dump):
2684
-
2685
- --symbol <string> Trading pair (default: BTCUSDT)
2686
- --timeframe <string> Candle interval (default: 15m)
2687
- --limit <string> Number of candles (default: 250)
2688
- --when <string> End date ISO 8601 or Unix ms (default: now)
2689
- --exchange <string> Exchange name (default: first registered)
2690
- --output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
2691
- --json Save as JSON array to ./dump/<output>.json
2692
- --jsonl Save as JSONL to ./dump/<output>.jsonl
2693
-
2694
- Module file ./modules/dump.module is loaded automatically if it exists.
2695
-
2696
- Init flags (--init):
2697
-
2698
- --output <string> Target directory name (default: backtest-kit-project)
2699
-
2700
- Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
2701
-
2702
- Module hooks (loaded automatically by each mode):
2703
-
2704
- modules/backtest.module --backtest Broker adapter for backtest
2705
- modules/paper.module --paper Broker adapter for paper trading
2706
- modules/live.module --live Broker adapter for live trading
2707
- modules/pine.module --pine Exchange schema for PineScript runs
2708
- modules/dump.module --dump Exchange schema for candle dumps
2709
-
2710
- Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
2711
-
2712
- Environment variables:
2713
-
2714
- CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
2715
- CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
2716
- CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
2717
- CC_WWWROOT_PORT UI server port (default: 60050)
2718
-
2719
- Examples:
2720
-
2721
- node ${ENTRY_PATH} --backtest ./content/feb_2026.strategy.ts
2722
- node ${ENTRY_PATH} --backtest --symbol BTCUSDT --noCache --ui ./content/feb_2026.strategy.ts
2723
- node ${ENTRY_PATH} --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
2724
- node ${ENTRY_PATH} --live --ui --telegram ./content/feb_2026.strategy.ts
2725
- node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
2726
- node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
2727
- node ${ENTRY_PATH} --init --output my-trading-bot
2856
+ const HELP_TEXT = `
2857
+ Usage:
2858
+ node index.mjs --<mode> [flags] [entry-point]
2859
+
2860
+ Modes:
2861
+
2862
+ --backtest <entry> Run strategy against historical candle data
2863
+ --walker <entry...> Run Walker A/B strategy comparison across multiple strategies
2864
+ --paper <entry> Paper trading (live prices, no real orders)
2865
+ --live <entry> Live trading with real orders
2866
+ --pine <entry> Execute a local .pine indicator file
2867
+ --dump Fetch and save raw OHLCV candles
2868
+ --init Scaffold a new project in the current directory
2869
+ --help Print this help message
2870
+
2871
+ Backtest flags:
2872
+
2873
+ --symbol <string> Trading pair (default: BTCUSDT)
2874
+ --strategy <string> Strategy name from addStrategySchema (default: first registered)
2875
+ --exchange <string> Exchange name from addExchangeSchema (default: first registered)
2876
+ --frame <string> Frame name from addFrameSchema (default: first registered)
2877
+ --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
2878
+ --noCache Skip candle cache warming before the run
2879
+ --verbose Log every candle fetch to stdout
2880
+ --ui Start web dashboard at http://localhost:60050
2881
+ --telegram Send trade notifications to Telegram
2882
+
2883
+ Walker flags (--walker):
2884
+
2885
+ --symbol <string> Trading pair (default: BTCUSDT)
2886
+ --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
2887
+ --noCache Skip candle cache warming before the run
2888
+ --verbose Log every candle fetch to stdout
2889
+ --output <string> Output file base name (default: walker_{SYMBOL}_{TIMESTAMP})
2890
+ --json Save results as JSON to ./dump/<output>.json
2891
+ --markdown Save report as Markdown to ./dump/<output>.md
2892
+
2893
+ Each positional argument is a strategy entry point. All strategy files are loaded without
2894
+ changing process.cwd() — .env is read from the working directory only.
2895
+ addWalkerSchema is called automatically using the registered exchange and frame.
2896
+ After comparison completes the report is printed to stdout (or saved if --json/--markdown).
2897
+
2898
+ Module file ./modules/walker.module is loaded automatically if it exists.
2899
+
2900
+ Paper / Live flags:
2901
+
2902
+ --symbol <string> Trading pair (default: BTCUSDT)
2903
+ --strategy <string> Strategy name (default: first registered)
2904
+ --exchange <string> Exchange name (default: first registered)
2905
+ --verbose Log every candle fetch to stdout
2906
+ --ui Start web dashboard
2907
+ --telegram Send Telegram notifications
2908
+
2909
+ PineScript flags (--pine):
2910
+
2911
+ --symbol <string> Trading pair (default: BTCUSDT)
2912
+ --timeframe <string> Candle interval (default: 15m)
2913
+ --limit <string> Number of candles to fetch (default: 250)
2914
+ --when <string> End date — ISO 8601 or Unix ms (default: now)
2915
+ --exchange <string> Exchange name (default: first registered)
2916
+ --output <string> Output file base name without extension
2917
+ --json Save output as JSON array to <pine-dir>/dump/<output>.json
2918
+ --jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
2919
+ --markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
2920
+
2921
+ Only plot() calls with display=display.data_window produce output columns.
2922
+ Module file ./modules/pine.module is loaded automatically if it exists.
2923
+
2924
+ Candle dump flags (--dump):
2925
+
2926
+ --symbol <string> Trading pair (default: BTCUSDT)
2927
+ --timeframe <string> Candle interval (default: 15m)
2928
+ --limit <string> Number of candles (default: 250)
2929
+ --when <string> End date ISO 8601 or Unix ms (default: now)
2930
+ --exchange <string> Exchange name (default: first registered)
2931
+ --output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
2932
+ --json Save as JSON array to ./dump/<output>.json
2933
+ --jsonl Save as JSONL to ./dump/<output>.jsonl
2934
+
2935
+ Module file ./modules/dump.module is loaded automatically if it exists.
2936
+
2937
+ Init flags (--init):
2938
+
2939
+ --output <string> Target directory name (default: backtest-kit-project)
2940
+
2941
+ Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
2942
+
2943
+ Module hooks (loaded automatically by each mode):
2944
+
2945
+ modules/backtest.module --backtest Broker adapter for backtest
2946
+ modules/walker.module --walker Broker adapter for walker comparison
2947
+ modules/paper.module --paper Broker adapter for paper trading
2948
+ modules/live.module --live Broker adapter for live trading
2949
+ modules/pine.module --pine Exchange schema for PineScript runs
2950
+ modules/dump.module --dump Exchange schema for candle dumps
2951
+
2952
+ Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
2953
+
2954
+ Environment variables:
2955
+
2956
+ CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
2957
+ CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
2958
+ CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
2959
+ CC_WWWROOT_PORT UI server port (default: 60050)
2960
+
2961
+ Examples:
2962
+
2963
+ node ${ENTRY_PATH} --backtest ./content/feb_2026.strategy.ts
2964
+ node ${ENTRY_PATH} --backtest --symbol BTCUSDT --noCache --ui ./content/feb_2026.strategy.ts
2965
+ node ${ENTRY_PATH} --walker ./content/feb_2026_v1.strategy.ts ./content/feb_2026_v2.strategy.ts ./content/feb_2026_v3.strategy.ts
2966
+ node ${ENTRY_PATH} --walker --symbol BTCUSDT --noCache --markdown ./content/feb_2026_v1.ts ./content/feb_2026_v2.ts
2967
+ node ${ENTRY_PATH} --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
2968
+ node ${ENTRY_PATH} --live --ui --telegram ./content/feb_2026.strategy.ts
2969
+ node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
2970
+ node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
2971
+ node ${ENTRY_PATH} --init --output my-trading-bot
2728
2972
  `.trimStart();
2729
2973
  const main$1 = async () => {
2730
2974
  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)))) {
@@ -2734,6 +2978,7 @@ const main$1 = async () => {
2734
2978
  if (!values.help) {
2735
2979
  return;
2736
2980
  }
2981
+ process.stdout.write(`@backtest-kit/cli ${"6.2.0"}\n\n`);
2737
2982
  process.stdout.write(HELP_TEXT);
2738
2983
  process.exit(0);
2739
2984
  };
@@ -2747,7 +2992,7 @@ const main = async () => {
2747
2992
  if (!values.version) {
2748
2993
  return;
2749
2994
  }
2750
- process.stdout.write(`@backtest-kit/cli ${"6.1.1"}\n`);
2995
+ process.stdout.write(`@backtest-kit/cli ${"6.2.0"}\n`);
2751
2996
  process.exit(0);
2752
2997
  };
2753
2998
  main();
@@ -2769,7 +3014,7 @@ async function run(mode, args) {
2769
3014
  }
2770
3015
  if (mode === "backtest") {
2771
3016
  await cli.backtestMainService.run(args);
2772
- listenGracefulShutdown$4();
3017
+ listenGracefulShutdown$5();
2773
3018
  return;
2774
3019
  }
2775
3020
  if (mode === "paper") {