@backtest-kit/cli 6.1.5 → 6.2.1

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,200 @@ 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
+ const strategyMap = new Map();
833
+ for (const entryPoint of payload.entryPoints) {
834
+ await this.resolveService.attachStrategy(entryPoint);
835
+ for (const { strategyName } of await BacktestKit.listStrategySchema()) {
836
+ if (strategyMap.has(strategyName)) {
837
+ continue;
838
+ }
839
+ strategyMap.set(strategyName, entryPoint);
840
+ }
841
+ }
842
+ await this.moduleConnectionService.loadModule("./walker.module");
843
+ {
844
+ this.exchangeSchemaService.addSchema();
845
+ this.symbolSchemaService.addSchema();
846
+ }
847
+ {
848
+ const { length } = await BacktestKit.listFrameSchema();
849
+ if (!length) {
850
+ const endDate = BacktestKit.alignToInterval(new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), "1m");
851
+ const startDate = BacktestKit.alignToInterval(new Date(Date.now() - 31 * 24 * 60 * 60 * 1000), "1m");
852
+ 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.`);
853
+ BacktestKit.addFrameSchema({
854
+ frameName: FrameName$1.DefaultFrame,
855
+ interval: "1m",
856
+ startDate,
857
+ endDate,
858
+ });
859
+ }
860
+ }
861
+ const symbol = payload.symbol || "BTCUSDT";
862
+ const strategyList = await BacktestKit.listStrategySchema();
863
+ const strategyNames = strategyList.map((s) => s.strategyName);
864
+ if (!strategyNames.length) {
865
+ throw new Error("No strategies found in provided entry points");
866
+ }
867
+ const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
868
+ const [defaultFrameName = null] = await BacktestKit.listFrameSchema();
869
+ const exchangeName = defaultExchangeName?.exchangeName;
870
+ const frameName = defaultFrameName?.frameName;
871
+ if (!exchangeName) {
872
+ throw new Error("Exchange name is required");
873
+ }
874
+ if (!frameName) {
875
+ throw new Error("Frame name is required");
876
+ }
877
+ const cwd = process.cwd();
878
+ const self = this;
879
+ const callbacks = {
880
+ async onStrategyStart(strategyName) {
881
+ const entryPoint = strategyMap.get(strategyName);
882
+ if (!entryPoint) {
883
+ return;
884
+ }
885
+ const absolutePath = path.resolve(entryPoint);
886
+ const moduleRoot = path.dirname(absolutePath);
887
+ {
888
+ process.chdir(moduleRoot);
889
+ cwd !== moduleRoot && BacktestKit.Log.useJsonl();
890
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
891
+ dotenv.config({ path: path.join(moduleRoot, '.env'), override: true, quiet: true });
892
+ }
893
+ if (!payload.noCache) {
894
+ await self.cacheLogicService.execute(payload.cacheInterval, {
895
+ exchangeName,
896
+ frameName,
897
+ symbol,
898
+ });
899
+ }
900
+ },
901
+ };
902
+ BacktestKit.addWalkerSchema({
903
+ walkerName: WALKER_NAME,
904
+ exchangeName,
905
+ frameName,
906
+ strategies: strategyNames,
907
+ callbacks,
908
+ });
909
+ if (payload.verbose) {
910
+ BacktestKit.overrideExchangeSchema({
911
+ exchangeName,
912
+ callbacks: {
913
+ onCandleData(symbol, interval, since) {
914
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
915
+ },
916
+ },
917
+ });
918
+ notifyVerbose();
919
+ }
920
+ if (payload.verbose) {
921
+ BacktestKit.overrideWalkerSchema({
922
+ walkerName: WALKER_NAME,
923
+ callbacks: {
924
+ async onStrategyStart(strategyName, symbol) {
925
+ console.log(`Strategy started: ${strategyName} for symbol: ${symbol}`);
926
+ await callbacks.onStrategyStart(strategyName);
927
+ },
928
+ onStrategyError(strategyName, symbol, error) {
929
+ console.error(`Strategy error: ${strategyName} for symbol: ${symbol}`, error);
930
+ },
931
+ onStrategyComplete(strategyName, symbol) {
932
+ console.log(`Strategy completed: ${strategyName} for symbol: ${symbol}`);
933
+ },
934
+ onComplete(results) {
935
+ console.log(`Walker completed for symbol: ${results.symbol}`, results);
936
+ }
937
+ }
938
+ });
939
+ }
940
+ BacktestKit.Walker.background(symbol, { walkerName: WALKER_NAME });
941
+ const [awaiter, { resolve: res }] = functoolsKit.createAwaiter();
942
+ const unWalker = BacktestKit.listenDoneWalker(() => {
943
+ console.log("Walker comparison finished");
944
+ unWalker();
945
+ res();
946
+ });
947
+ {
948
+ payload.verbose && console.time("Walker");
949
+ await awaiter;
950
+ payload.verbose && console.timeEnd("Walker");
951
+ }
952
+ process.chdir(cwd);
953
+ const dumpName = payload.output || `walker_${symbol}_${Date.now()}`;
954
+ const dumpDir = path.join(process.cwd(), "dump");
955
+ if (payload.json) {
956
+ const filePath = path.resolve(dumpDir, `${dumpName}.json`);
957
+ const data = await BacktestKit.Walker.getData(symbol, { walkerName: WALKER_NAME });
958
+ await fs$1.mkdir(dumpDir, { recursive: true });
959
+ await fs$1.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
960
+ console.log(`Saved: ${filePath}`);
961
+ process.exit(0);
962
+ return;
963
+ }
964
+ if (payload.markdown) {
965
+ const filePath = path.resolve(dumpDir, `${dumpName}.md`);
966
+ const report = await BacktestKit.Walker.getReport(symbol, { walkerName: WALKER_NAME });
967
+ await fs$1.mkdir(dumpDir, { recursive: true });
968
+ await fs$1.writeFile(filePath, report, "utf-8");
969
+ console.log(`Saved: ${filePath}`);
970
+ process.exit(0);
971
+ return;
972
+ }
973
+ const report = await BacktestKit.Walker.getReport(symbol, { walkerName: WALKER_NAME });
974
+ console.log(report);
975
+ process.exit(0);
976
+ });
977
+ this.connect = functoolsKit.singleshot(async () => {
978
+ this.loggerService.log("walkerMainService connect");
979
+ 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)))) {
980
+ return;
981
+ }
982
+ const { values } = getArgs();
983
+ if (!values.walker) {
984
+ return;
985
+ }
986
+ const entryPoints = getPositionals();
987
+ if (!entryPoints.length) {
988
+ throw new Error("At least one entry point is required");
989
+ }
990
+ const cacheInterval = GET_CACHE_INTERVAL_LIST_FN();
991
+ return await this.run({
992
+ entryPoints,
993
+ json: values.json,
994
+ markdown: values.markdown,
995
+ symbol: values.symbol,
996
+ output: values.output,
997
+ cacheInterval,
998
+ verbose: values.verbose,
999
+ noCache: values.noCache,
1000
+ });
1001
+ });
1002
+ }
1003
+ }
1004
+
792
1005
  class LiveMainService {
793
1006
  constructor() {
794
1007
  this.loggerService = inject(TYPES.loggerService);
@@ -806,7 +1019,10 @@ class LiveMainService {
806
1019
  this.frontendProviderService.connect();
807
1020
  this.telegramProviderService.connect();
808
1021
  }
809
- await this.resolveService.attachJavascript(payload.entryPoint);
1022
+ {
1023
+ await this.resolveService.attachJavascript(payload.entryPoint);
1024
+ await this.moduleConnectionService.loadModule("./live.module");
1025
+ }
810
1026
  {
811
1027
  this.exchangeSchemaService.addSchema();
812
1028
  this.symbolSchemaService.addSchema();
@@ -833,7 +1049,6 @@ class LiveMainService {
833
1049
  });
834
1050
  notifyVerbose();
835
1051
  }
836
- await this.moduleConnectionService.loadModule("./live.module");
837
1052
  BacktestKit.Live.background(symbol, {
838
1053
  strategyName,
839
1054
  exchangeName,
@@ -849,7 +1064,7 @@ class LiveMainService {
849
1064
  if (!values.live) {
850
1065
  return;
851
1066
  }
852
- const entryPoint = getPositional();
1067
+ const [entryPoint = null] = getPositionals();
853
1068
  if (!entryPoint) {
854
1069
  throw new Error("Entry point is required");
855
1070
  }
@@ -879,7 +1094,10 @@ class PaperMainService {
879
1094
  this.frontendProviderService.connect();
880
1095
  this.telegramProviderService.connect();
881
1096
  }
882
- await this.resolveService.attachJavascript(payload.entryPoint);
1097
+ {
1098
+ await this.resolveService.attachJavascript(payload.entryPoint);
1099
+ await this.moduleConnectionService.loadModule("./paper.module");
1100
+ }
883
1101
  {
884
1102
  this.exchangeSchemaService.addSchema();
885
1103
  this.symbolSchemaService.addSchema();
@@ -906,7 +1124,6 @@ class PaperMainService {
906
1124
  });
907
1125
  notifyVerbose();
908
1126
  }
909
- await this.moduleConnectionService.loadModule("./paper.module");
910
1127
  BacktestKit.Live.background(symbol, {
911
1128
  strategyName,
912
1129
  exchangeName,
@@ -922,7 +1139,7 @@ class PaperMainService {
922
1139
  if (!values.paper) {
923
1140
  return;
924
1141
  }
925
- const entryPoint = getPositional();
1142
+ const [entryPoint = null] = getPositionals();
926
1143
  if (!entryPoint) {
927
1144
  throw new Error("Entry point is required");
928
1145
  }
@@ -1923,8 +2140,7 @@ class BabelService {
1923
2140
  }
1924
2141
  }
1925
2142
 
1926
- const TRANSPILE_FN = (code, self) => {
1927
- const require = self.getBaseRequire();
2143
+ const TRANSPILE_FN = functoolsKit.memoize(([path]) => `${path}`, (path, code, self, require) => {
1928
2144
  const __filename = self.__filename;
1929
2145
  const __dirname = self.__dirname;
1930
2146
  const module = { exports: {} };
@@ -1933,7 +2149,7 @@ const TRANSPILE_FN = (code, self) => {
1933
2149
  eval(self.params.babel.transpile(code));
1934
2150
  }
1935
2151
  catch (error) {
1936
- console.log(`Error during transpilation error=\`${functoolsKit.getErrorMessage(error)}\` __filename=\`${__filename}\` __dirname=\`${__dirname}\``);
2152
+ console.log(`Error during transpilation error=\`${functoolsKit.getErrorMessage(error)}\` path=\`${path}\` __filename=\`${__filename}\` __dirname=\`${__dirname}\``);
1937
2153
  process.exit(-1);
1938
2154
  }
1939
2155
  return {
@@ -1943,9 +2159,9 @@ const TRANSPILE_FN = (code, self) => {
1943
2159
  exports,
1944
2160
  module,
1945
2161
  };
1946
- };
1947
- const REQUIRE_ENTRY_FACTORY = (filePath, self) => {
1948
- const baseRequire = self.getBaseRequire();
2162
+ });
2163
+ const REQUIRE_ENTRY_FACTORY = (filePath, self, seen) => {
2164
+ const baseRequire = CREATE_BASE_REQUIRE_FN(self, seen);
1949
2165
  try {
1950
2166
  return baseRequire(filePath);
1951
2167
  }
@@ -1953,12 +2169,12 @@ const REQUIRE_ENTRY_FACTORY = (filePath, self) => {
1953
2169
  return null;
1954
2170
  }
1955
2171
  };
1956
- const BABEL_ENTRY_FACTORY = (filePath, self) => {
2172
+ const BABEL_ENTRY_FACTORY = (filePath, self, seen) => {
1957
2173
  try {
1958
2174
  const resolvedPath = path.resolve(self.__dirname, filePath);
1959
2175
  const code = fs.readFileSync(resolvedPath, "utf-8");
1960
2176
  const child = self.fork(path.dirname(resolvedPath));
1961
- const { module } = TRANSPILE_FN(code, child);
2177
+ const { module } = TRANSPILE_FN(resolvedPath, code, child, CREATE_BASE_REQUIRE_FN(child, seen));
1962
2178
  return "default" in module.exports
1963
2179
  ? module.exports.default
1964
2180
  : module.exports;
@@ -1999,21 +2215,20 @@ const GET_RESOLVED_EXT_FN = (filePath) => {
1999
2215
  }
2000
2216
  return filePath;
2001
2217
  };
2002
- const ENTRY_FACTORY = (filePath, self) => {
2003
- filePath = GET_RESOLVED_EXT_FN(filePath);
2218
+ const ENTRY_FACTORY = (filePath, self, seen) => {
2004
2219
  {
2005
2220
  let result = null;
2006
- if ((result = REQUIRE_ENTRY_FACTORY(filePath, self))) {
2221
+ if ((result = REQUIRE_ENTRY_FACTORY(filePath, self, seen))) {
2007
2222
  return result;
2008
2223
  }
2009
- if ((result = BABEL_ENTRY_FACTORY(filePath, self))) {
2224
+ if ((result = BABEL_ENTRY_FACTORY(filePath, self, seen))) {
2010
2225
  return result;
2011
2226
  }
2012
2227
  }
2013
2228
  throw new Error(`Failed to load module at ${filePath} (basepath: ${self.params.path})`);
2014
2229
  };
2015
- const CREATE_BASE_REQUIRE_FN = (self) => {
2016
- const baseRequire = module$1.createRequire(self.__filename);
2230
+ const CREATE_BASE_REQUIRE_FN = (self, seen) => {
2231
+ const baseRequire = self.baseRequire();
2017
2232
  return new Proxy(baseRequire, {
2018
2233
  apply(_target, _this, args) {
2019
2234
  const id = args[0];
@@ -2034,7 +2249,7 @@ const CREATE_BASE_REQUIRE_FN = (self) => {
2034
2249
  if (id.startsWith("./") || id.startsWith("../")) {
2035
2250
  const resolved = path.resolve(self.__dirname, id);
2036
2251
  const child = self.fork(path.dirname(resolved));
2037
- return child.import(resolved);
2252
+ return child.import(resolved, seen);
2038
2253
  }
2039
2254
  return baseRequire(id);
2040
2255
  },
@@ -2048,11 +2263,11 @@ const BacktestKitCli = new Proxy({}, {
2048
2263
  class ClientLoader {
2049
2264
  constructor(params) {
2050
2265
  this.params = params;
2051
- this.getBaseRequire = functoolsKit.singleshot(() => {
2052
- this.params.logger.log("ClientLoader getBaseRequire", {
2266
+ this.baseRequire = functoolsKit.singleshot(() => {
2267
+ this.params.logger.log("ClientLoader baseRequire", {
2053
2268
  basePath: this.params.path,
2054
2269
  });
2055
- return CREATE_BASE_REQUIRE_FN(this);
2270
+ return module$1.createRequire(this.__filename);
2056
2271
  });
2057
2272
  this.__filename = path.join(params.path, "index.cjs");
2058
2273
  this.__dirname = path.dirname(this.__filename);
@@ -2068,12 +2283,21 @@ class ClientLoader {
2068
2283
  logger: this.params.logger,
2069
2284
  });
2070
2285
  }
2071
- import(filePath) {
2286
+ import(filePath, seen = new Set()) {
2072
2287
  this.params.logger.log("ClientLoader import", {
2073
2288
  filePath,
2074
2289
  basePath: this.params.path,
2075
2290
  });
2076
- return ENTRY_FACTORY(filePath, this);
2291
+ const resolved = GET_RESOLVED_EXT_FN(filePath);
2292
+ if (seen.has(resolved)) {
2293
+ throw new Error(`Circular dependency detected: ${resolved} (seen: ${[...seen].join("->")}->${resolved})`);
2294
+ }
2295
+ const currentSeen = new Set(seen);
2296
+ if (!seen.size) {
2297
+ currentSeen.add(path.resolve(this.__dirname, filePath));
2298
+ }
2299
+ currentSeen.add(resolved);
2300
+ return ENTRY_FACTORY(resolved, this, currentSeen);
2077
2301
  }
2078
2302
  check(filePath) {
2079
2303
  this.params.logger.log("ClientLoader check", {
@@ -2141,6 +2365,7 @@ class LoaderService {
2141
2365
  }
2142
2366
  {
2143
2367
  provide(TYPES.backtestMainService, () => new BacktestMainService());
2368
+ provide(TYPES.walkerMainService, () => new WalkerMainService());
2144
2369
  provide(TYPES.paperMainService, () => new PaperMainService());
2145
2370
  provide(TYPES.liveMainService, () => new LiveMainService());
2146
2371
  }
@@ -2180,6 +2405,7 @@ const connectionServices = {
2180
2405
  };
2181
2406
  const mainServices = {
2182
2407
  backtestMainService: inject(TYPES.backtestMainService),
2408
+ walkerMainService: inject(TYPES.walkerMainService),
2183
2409
  paperMainService: inject(TYPES.paperMainService),
2184
2410
  liveMainService: inject(TYPES.liveMainService),
2185
2411
  };
@@ -2215,14 +2441,14 @@ const cli = {
2215
2441
  };
2216
2442
  init();
2217
2443
 
2218
- const MODES = ["backtest", "paper", "live", "pine", "dump", "init", "help", "version"];
2444
+ const MODES = ["backtest", "walker", "paper", "live", "pine", "dump", "init", "help", "version"];
2219
2445
  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
2446
+ const HELP_TEXT$1 = `
2447
+ Example:
2448
+
2449
+ node ${ENTRY_PATH$1} --help
2224
2450
  `.trimStart();
2225
- const main$a = async () => {
2451
+ const main$b = async () => {
2226
2452
  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
2453
  return;
2228
2454
  }
@@ -2230,21 +2456,21 @@ const main$a = async () => {
2230
2456
  if (MODES.some((mode) => values[mode])) {
2231
2457
  return;
2232
2458
  }
2233
- process.stdout.write(`@backtest-kit/cli ${"6.1.1"}\n`);
2459
+ process.stdout.write(`@backtest-kit/cli ${"6.2.1"}\n`);
2234
2460
  process.stdout.write("\n");
2235
2461
  process.stdout.write(`Run with --help to see available commands.\n`);
2236
2462
  process.stdout.write("\n");
2237
2463
  process.stdout.write(HELP_TEXT$1);
2238
2464
  process.exit(0);
2239
2465
  };
2240
- main$a();
2466
+ main$b();
2241
2467
 
2242
2468
  const notifyShutdown = functoolsKit.singleshot(async () => {
2243
2469
  console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
2244
2470
  });
2245
2471
 
2246
- const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
2247
- process.off("SIGINT", BEFORE_EXIT_FN$4);
2472
+ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
2473
+ process.off("SIGINT", BEFORE_EXIT_FN$5);
2248
2474
  const [running = null] = await BacktestKit.Backtest.list();
2249
2475
  if (!running) {
2250
2476
  return;
@@ -2260,6 +2486,35 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
2260
2486
  frameName,
2261
2487
  });
2262
2488
  });
2489
+ const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
2490
+ process.on("SIGINT", BEFORE_EXIT_FN$5);
2491
+ });
2492
+ const main$a = async () => {
2493
+ 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)))) {
2494
+ return;
2495
+ }
2496
+ const { values } = getArgs();
2497
+ if (!values.backtest) {
2498
+ return;
2499
+ }
2500
+ await cli.backtestMainService.connect();
2501
+ listenGracefulShutdown$5();
2502
+ };
2503
+ main$a();
2504
+
2505
+ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
2506
+ process.off("SIGINT", BEFORE_EXIT_FN$4);
2507
+ const [running = null] = await BacktestKit.Walker.list();
2508
+ if (!running) {
2509
+ return;
2510
+ }
2511
+ notifyShutdown();
2512
+ const { walkerName, symbol, status } = running;
2513
+ if (status === "fulfilled") {
2514
+ return;
2515
+ }
2516
+ BacktestKit.Walker.stop(symbol, { walkerName });
2517
+ });
2263
2518
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
2264
2519
  process.on("SIGINT", BEFORE_EXIT_FN$4);
2265
2520
  });
@@ -2268,11 +2523,11 @@ const main$9 = async () => {
2268
2523
  return;
2269
2524
  }
2270
2525
  const { values } = getArgs();
2271
- if (!values.backtest) {
2526
+ if (!values.walker) {
2272
2527
  return;
2273
2528
  }
2274
- await cli.backtestMainService.connect();
2275
2529
  listenGracefulShutdown$4();
2530
+ await cli.walkerMainService.connect();
2276
2531
  };
2277
2532
  main$9();
2278
2533
 
@@ -2408,7 +2663,7 @@ const main$4 = async () => {
2408
2663
  if (!values.pine) {
2409
2664
  return;
2410
2665
  }
2411
- const entryPoint = getPositional();
2666
+ const [entryPoint = null] = getPositionals();
2412
2667
  if (!entryPoint) {
2413
2668
  return;
2414
2669
  }
@@ -2602,6 +2857,20 @@ function runScript(scriptPath, cwd) {
2602
2857
  child.on("error", reject);
2603
2858
  });
2604
2859
  }
2860
+ function runNpmInstall(cwd) {
2861
+ return new Promise((resolve, reject) => {
2862
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
2863
+ const child = child_process.spawn(npm, ["install"], { cwd, stdio: "inherit", shell: true });
2864
+ child.on("close", (code) => {
2865
+ if (code !== 0) {
2866
+ reject(new Error(`npm install exited with code ${code}`));
2867
+ return;
2868
+ }
2869
+ resolve();
2870
+ });
2871
+ child.on("error", reject);
2872
+ });
2873
+ }
2605
2874
  const main$2 = async () => {
2606
2875
  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)))) {
2607
2876
  return;
@@ -2622,109 +2891,130 @@ const main$2 = async () => {
2622
2891
  await copyDir(templatePath, projectPath, { PROJECT_NAME: projectName });
2623
2892
  console.log(`Fetching docs...`);
2624
2893
  await runScript(path.join(projectPath, "scripts/fetch_docs.mjs"), projectPath);
2894
+ console.log(`Installing dependencies...`);
2895
+ await runNpmInstall(projectPath);
2625
2896
  console.log(`Done! Project created at ${projectPath}`);
2626
2897
  process.exit(0);
2627
2898
  };
2628
2899
  main$2();
2629
2900
 
2630
2901
  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
2902
+ const HELP_TEXT = `
2903
+ Usage:
2904
+ node index.mjs --<mode> [flags] [entry-point]
2905
+
2906
+ Modes:
2907
+
2908
+ --backtest <entry> Run strategy against historical candle data
2909
+ --walker <entry...> Run Walker A/B strategy comparison across multiple strategies
2910
+ --paper <entry> Paper trading (live prices, no real orders)
2911
+ --live <entry> Live trading with real orders
2912
+ --pine <entry> Execute a local .pine indicator file
2913
+ --dump Fetch and save raw OHLCV candles
2914
+ --init Scaffold a new project in the current directory
2915
+ --help Print this help message
2916
+
2917
+ Backtest flags:
2918
+
2919
+ --symbol <string> Trading pair (default: BTCUSDT)
2920
+ --strategy <string> Strategy name from addStrategySchema (default: first registered)
2921
+ --exchange <string> Exchange name from addExchangeSchema (default: first registered)
2922
+ --frame <string> Frame name from addFrameSchema (default: first registered)
2923
+ --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
2924
+ --noCache Skip candle cache warming before the run
2925
+ --verbose Log every candle fetch to stdout
2926
+ --ui Start web dashboard at http://localhost:60050
2927
+ --telegram Send trade notifications to Telegram
2928
+
2929
+ Walker flags (--walker):
2930
+
2931
+ --symbol <string> Trading pair (default: BTCUSDT)
2932
+ --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
2933
+ --noCache Skip candle cache warming before the run
2934
+ --verbose Log every candle fetch to stdout
2935
+ --output <string> Output file base name (default: walker_{SYMBOL}_{TIMESTAMP})
2936
+ --json Save results as JSON to ./dump/<output>.json
2937
+ --markdown Save report as Markdown to ./dump/<output>.md
2938
+
2939
+ Each positional argument is a strategy entry point. All strategy files are loaded without
2940
+ changing process.cwd() — .env is read from the working directory only.
2941
+ addWalkerSchema is called automatically using the registered exchange and frame.
2942
+ After comparison completes the report is printed to stdout (or saved if --json/--markdown).
2943
+
2944
+ Module file ./modules/walker.module is loaded automatically if it exists.
2945
+
2946
+ Paper / Live flags:
2947
+
2948
+ --symbol <string> Trading pair (default: BTCUSDT)
2949
+ --strategy <string> Strategy name (default: first registered)
2950
+ --exchange <string> Exchange name (default: first registered)
2951
+ --verbose Log every candle fetch to stdout
2952
+ --ui Start web dashboard
2953
+ --telegram Send Telegram notifications
2954
+
2955
+ PineScript flags (--pine):
2956
+
2957
+ --symbol <string> Trading pair (default: BTCUSDT)
2958
+ --timeframe <string> Candle interval (default: 15m)
2959
+ --limit <string> Number of candles to fetch (default: 250)
2960
+ --when <string> End date — ISO 8601 or Unix ms (default: now)
2961
+ --exchange <string> Exchange name (default: first registered)
2962
+ --output <string> Output file base name without extension
2963
+ --json Save output as JSON array to <pine-dir>/dump/<output>.json
2964
+ --jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
2965
+ --markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
2966
+
2967
+ Only plot() calls with display=display.data_window produce output columns.
2968
+ Module file ./modules/pine.module is loaded automatically if it exists.
2969
+
2970
+ Candle dump flags (--dump):
2971
+
2972
+ --symbol <string> Trading pair (default: BTCUSDT)
2973
+ --timeframe <string> Candle interval (default: 15m)
2974
+ --limit <string> Number of candles (default: 250)
2975
+ --when <string> End date ISO 8601 or Unix ms (default: now)
2976
+ --exchange <string> Exchange name (default: first registered)
2977
+ --output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
2978
+ --json Save as JSON array to ./dump/<output>.json
2979
+ --jsonl Save as JSONL to ./dump/<output>.jsonl
2980
+
2981
+ Module file ./modules/dump.module is loaded automatically if it exists.
2982
+
2983
+ Init flags (--init):
2984
+
2985
+ --output <string> Target directory name (default: backtest-kit-project)
2986
+
2987
+ Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
2988
+
2989
+ Module hooks (loaded automatically by each mode):
2990
+
2991
+ modules/backtest.module --backtest Broker adapter for backtest
2992
+ modules/walker.module --walker Broker adapter for walker comparison
2993
+ modules/paper.module --paper Broker adapter for paper trading
2994
+ modules/live.module --live Broker adapter for live trading
2995
+ modules/pine.module --pine Exchange schema for PineScript runs
2996
+ modules/dump.module --dump Exchange schema for candle dumps
2997
+
2998
+ Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
2999
+
3000
+ Environment variables:
3001
+
3002
+ CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
3003
+ CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
3004
+ CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
3005
+ CC_WWWROOT_PORT UI server port (default: 60050)
3006
+
3007
+ Examples:
3008
+
3009
+ node ${ENTRY_PATH} --backtest ./content/feb_2026.strategy.ts
3010
+ node ${ENTRY_PATH} --backtest --symbol BTCUSDT --noCache --ui ./content/feb_2026.strategy.ts
3011
+ node ${ENTRY_PATH} --walker ./content/feb_2026_v1.strategy.ts ./content/feb_2026_v2.strategy.ts ./content/feb_2026_v3.strategy.ts
3012
+ node ${ENTRY_PATH} --walker --symbol BTCUSDT --noCache --markdown ./content/feb_2026_v1.ts ./content/feb_2026_v2.ts
3013
+ node ${ENTRY_PATH} --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
3014
+ node ${ENTRY_PATH} --live --ui --telegram ./content/feb_2026.strategy.ts
3015
+ node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
3016
+ node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
3017
+ node ${ENTRY_PATH} --init --output my-trading-bot
2728
3018
  `.trimStart();
2729
3019
  const main$1 = async () => {
2730
3020
  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 +3024,7 @@ const main$1 = async () => {
2734
3024
  if (!values.help) {
2735
3025
  return;
2736
3026
  }
3027
+ process.stdout.write(`@backtest-kit/cli ${"6.2.1"}\n\n`);
2737
3028
  process.stdout.write(HELP_TEXT);
2738
3029
  process.exit(0);
2739
3030
  };
@@ -2747,7 +3038,7 @@ const main = async () => {
2747
3038
  if (!values.version) {
2748
3039
  return;
2749
3040
  }
2750
- process.stdout.write(`@backtest-kit/cli ${"6.1.1"}\n`);
3041
+ process.stdout.write(`@backtest-kit/cli ${"6.2.1"}\n`);
2751
3042
  process.exit(0);
2752
3043
  };
2753
3044
  main();
@@ -2769,7 +3060,7 @@ async function run(mode, args) {
2769
3060
  }
2770
3061
  if (mode === "backtest") {
2771
3062
  await cli.backtestMainService.run(args);
2772
- listenGracefulShutdown$4();
3063
+ listenGracefulShutdown$5();
2773
3064
  return;
2774
3065
  }
2775
3066
  if (mode === "paper") {