@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.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,200 @@ 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
+ const strategyMap = new Map();
808
+ for (const entryPoint of payload.entryPoints) {
809
+ await this.resolveService.attachStrategy(entryPoint);
810
+ for (const { strategyName } of await listStrategySchema()) {
811
+ if (strategyMap.has(strategyName)) {
812
+ continue;
813
+ }
814
+ strategyMap.set(strategyName, entryPoint);
815
+ }
816
+ }
817
+ await this.moduleConnectionService.loadModule("./walker.module");
818
+ {
819
+ this.exchangeSchemaService.addSchema();
820
+ this.symbolSchemaService.addSchema();
821
+ }
822
+ {
823
+ const { length } = await listFrameSchema();
824
+ if (!length) {
825
+ const endDate = alignToInterval(new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), "1m");
826
+ const startDate = alignToInterval(new Date(Date.now() - 31 * 24 * 60 * 60 * 1000), "1m");
827
+ 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.`);
828
+ addFrameSchema({
829
+ frameName: FrameName$1.DefaultFrame,
830
+ interval: "1m",
831
+ startDate,
832
+ endDate,
833
+ });
834
+ }
835
+ }
836
+ const symbol = payload.symbol || "BTCUSDT";
837
+ const strategyList = await listStrategySchema();
838
+ const strategyNames = strategyList.map((s) => s.strategyName);
839
+ if (!strategyNames.length) {
840
+ throw new Error("No strategies found in provided entry points");
841
+ }
842
+ const [defaultExchangeName = null] = await listExchangeSchema();
843
+ const [defaultFrameName = null] = await listFrameSchema();
844
+ const exchangeName = defaultExchangeName?.exchangeName;
845
+ const frameName = defaultFrameName?.frameName;
846
+ if (!exchangeName) {
847
+ throw new Error("Exchange name is required");
848
+ }
849
+ if (!frameName) {
850
+ throw new Error("Frame name is required");
851
+ }
852
+ const cwd = process.cwd();
853
+ const self = this;
854
+ const callbacks = {
855
+ async onStrategyStart(strategyName) {
856
+ const entryPoint = strategyMap.get(strategyName);
857
+ if (!entryPoint) {
858
+ return;
859
+ }
860
+ const absolutePath = path.resolve(entryPoint);
861
+ const moduleRoot = path.dirname(absolutePath);
862
+ {
863
+ process.chdir(moduleRoot);
864
+ cwd !== moduleRoot && Log.useJsonl();
865
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
866
+ dotenv.config({ path: path.join(moduleRoot, '.env'), override: true, quiet: true });
867
+ }
868
+ if (!payload.noCache) {
869
+ await self.cacheLogicService.execute(payload.cacheInterval, {
870
+ exchangeName,
871
+ frameName,
872
+ symbol,
873
+ });
874
+ }
875
+ },
876
+ };
877
+ addWalkerSchema({
878
+ walkerName: WALKER_NAME,
879
+ exchangeName,
880
+ frameName,
881
+ strategies: strategyNames,
882
+ callbacks,
883
+ });
884
+ if (payload.verbose) {
885
+ overrideExchangeSchema({
886
+ exchangeName,
887
+ callbacks: {
888
+ onCandleData(symbol, interval, since) {
889
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
890
+ },
891
+ },
892
+ });
893
+ notifyVerbose();
894
+ }
895
+ if (payload.verbose) {
896
+ overrideWalkerSchema({
897
+ walkerName: WALKER_NAME,
898
+ callbacks: {
899
+ async onStrategyStart(strategyName, symbol) {
900
+ console.log(`Strategy started: ${strategyName} for symbol: ${symbol}`);
901
+ await callbacks.onStrategyStart(strategyName);
902
+ },
903
+ onStrategyError(strategyName, symbol, error) {
904
+ console.error(`Strategy error: ${strategyName} for symbol: ${symbol}`, error);
905
+ },
906
+ onStrategyComplete(strategyName, symbol) {
907
+ console.log(`Strategy completed: ${strategyName} for symbol: ${symbol}`);
908
+ },
909
+ onComplete(results) {
910
+ console.log(`Walker completed for symbol: ${results.symbol}`, results);
911
+ }
912
+ }
913
+ });
914
+ }
915
+ Walker.background(symbol, { walkerName: WALKER_NAME });
916
+ const [awaiter, { resolve: res }] = createAwaiter();
917
+ const unWalker = listenDoneWalker(() => {
918
+ console.log("Walker comparison finished");
919
+ unWalker();
920
+ res();
921
+ });
922
+ {
923
+ payload.verbose && console.time("Walker");
924
+ await awaiter;
925
+ payload.verbose && console.timeEnd("Walker");
926
+ }
927
+ process.chdir(cwd);
928
+ const dumpName = payload.output || `walker_${symbol}_${Date.now()}`;
929
+ const dumpDir = join(process.cwd(), "dump");
930
+ if (payload.json) {
931
+ const filePath = resolve(dumpDir, `${dumpName}.json`);
932
+ const data = await Walker.getData(symbol, { walkerName: WALKER_NAME });
933
+ await mkdir(dumpDir, { recursive: true });
934
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
935
+ console.log(`Saved: ${filePath}`);
936
+ process.exit(0);
937
+ return;
938
+ }
939
+ if (payload.markdown) {
940
+ const filePath = resolve(dumpDir, `${dumpName}.md`);
941
+ const report = await Walker.getReport(symbol, { walkerName: WALKER_NAME });
942
+ await mkdir(dumpDir, { recursive: true });
943
+ await writeFile(filePath, report, "utf-8");
944
+ console.log(`Saved: ${filePath}`);
945
+ process.exit(0);
946
+ return;
947
+ }
948
+ const report = await Walker.getReport(symbol, { walkerName: WALKER_NAME });
949
+ console.log(report);
950
+ process.exit(0);
951
+ });
952
+ this.connect = singleshot(async () => {
953
+ this.loggerService.log("walkerMainService connect");
954
+ if (!getEntry(import.meta.url)) {
955
+ return;
956
+ }
957
+ const { values } = getArgs();
958
+ if (!values.walker) {
959
+ return;
960
+ }
961
+ const entryPoints = getPositionals();
962
+ if (!entryPoints.length) {
963
+ throw new Error("At least one entry point is required");
964
+ }
965
+ const cacheInterval = GET_CACHE_INTERVAL_LIST_FN();
966
+ return await this.run({
967
+ entryPoints,
968
+ json: values.json,
969
+ markdown: values.markdown,
970
+ symbol: values.symbol,
971
+ output: values.output,
972
+ cacheInterval,
973
+ verbose: values.verbose,
974
+ noCache: values.noCache,
975
+ });
976
+ });
977
+ }
978
+ }
979
+
767
980
  class LiveMainService {
768
981
  constructor() {
769
982
  this.loggerService = inject(TYPES.loggerService);
@@ -781,7 +994,10 @@ class LiveMainService {
781
994
  this.frontendProviderService.connect();
782
995
  this.telegramProviderService.connect();
783
996
  }
784
- await this.resolveService.attachJavascript(payload.entryPoint);
997
+ {
998
+ await this.resolveService.attachJavascript(payload.entryPoint);
999
+ await this.moduleConnectionService.loadModule("./live.module");
1000
+ }
785
1001
  {
786
1002
  this.exchangeSchemaService.addSchema();
787
1003
  this.symbolSchemaService.addSchema();
@@ -808,7 +1024,6 @@ class LiveMainService {
808
1024
  });
809
1025
  notifyVerbose();
810
1026
  }
811
- await this.moduleConnectionService.loadModule("./live.module");
812
1027
  Live.background(symbol, {
813
1028
  strategyName,
814
1029
  exchangeName,
@@ -824,7 +1039,7 @@ class LiveMainService {
824
1039
  if (!values.live) {
825
1040
  return;
826
1041
  }
827
- const entryPoint = getPositional();
1042
+ const [entryPoint = null] = getPositionals();
828
1043
  if (!entryPoint) {
829
1044
  throw new Error("Entry point is required");
830
1045
  }
@@ -854,7 +1069,10 @@ class PaperMainService {
854
1069
  this.frontendProviderService.connect();
855
1070
  this.telegramProviderService.connect();
856
1071
  }
857
- await this.resolveService.attachJavascript(payload.entryPoint);
1072
+ {
1073
+ await this.resolveService.attachJavascript(payload.entryPoint);
1074
+ await this.moduleConnectionService.loadModule("./paper.module");
1075
+ }
858
1076
  {
859
1077
  this.exchangeSchemaService.addSchema();
860
1078
  this.symbolSchemaService.addSchema();
@@ -881,7 +1099,6 @@ class PaperMainService {
881
1099
  });
882
1100
  notifyVerbose();
883
1101
  }
884
- await this.moduleConnectionService.loadModule("./paper.module");
885
1102
  Live.background(symbol, {
886
1103
  strategyName,
887
1104
  exchangeName,
@@ -897,7 +1114,7 @@ class PaperMainService {
897
1114
  if (!values.paper) {
898
1115
  return;
899
1116
  }
900
- const entryPoint = getPositional();
1117
+ const [entryPoint = null] = getPositionals();
901
1118
  if (!entryPoint) {
902
1119
  throw new Error("Entry point is required");
903
1120
  }
@@ -1898,8 +2115,7 @@ class BabelService {
1898
2115
  }
1899
2116
  }
1900
2117
 
1901
- const TRANSPILE_FN = (code, self) => {
1902
- const require = self.getBaseRequire();
2118
+ const TRANSPILE_FN = memoize(([path]) => `${path}`, (path, code, self, require) => {
1903
2119
  const __filename = self.__filename;
1904
2120
  const __dirname = self.__dirname;
1905
2121
  const module = { exports: {} };
@@ -1908,7 +2124,7 @@ const TRANSPILE_FN = (code, self) => {
1908
2124
  eval(self.params.babel.transpile(code));
1909
2125
  }
1910
2126
  catch (error) {
1911
- console.log(`Error during transpilation error=\`${getErrorMessage(error)}\` __filename=\`${__filename}\` __dirname=\`${__dirname}\``);
2127
+ console.log(`Error during transpilation error=\`${getErrorMessage(error)}\` path=\`${path}\` __filename=\`${__filename}\` __dirname=\`${__dirname}\``);
1912
2128
  process.exit(-1);
1913
2129
  }
1914
2130
  return {
@@ -1918,18 +2134,18 @@ const TRANSPILE_FN = (code, self) => {
1918
2134
  exports,
1919
2135
  module,
1920
2136
  };
1921
- };
1922
- const REQUIRE_ENTRY_FACTORY = (filePath, self) => {
2137
+ });
2138
+ const REQUIRE_ENTRY_FACTORY = (filePath, self, seen) => {
1923
2139
  {
1924
2140
  return null;
1925
2141
  }
1926
2142
  };
1927
- const BABEL_ENTRY_FACTORY = (filePath, self) => {
2143
+ const BABEL_ENTRY_FACTORY = (filePath, self, seen) => {
1928
2144
  try {
1929
2145
  const resolvedPath = path.resolve(self.__dirname, filePath);
1930
2146
  const code = fs.readFileSync(resolvedPath, "utf-8");
1931
2147
  const child = self.fork(path.dirname(resolvedPath));
1932
- const { module } = TRANSPILE_FN(code, child);
2148
+ const { module } = TRANSPILE_FN(resolvedPath, code, child, CREATE_BASE_REQUIRE_FN(child, seen));
1933
2149
  return "default" in module.exports
1934
2150
  ? module.exports.default
1935
2151
  : module.exports;
@@ -1970,21 +2186,20 @@ const GET_RESOLVED_EXT_FN = (filePath) => {
1970
2186
  }
1971
2187
  return filePath;
1972
2188
  };
1973
- const ENTRY_FACTORY = (filePath, self) => {
1974
- filePath = GET_RESOLVED_EXT_FN(filePath);
2189
+ const ENTRY_FACTORY = (filePath, self, seen) => {
1975
2190
  {
1976
2191
  let result = null;
1977
2192
  if ((result = REQUIRE_ENTRY_FACTORY())) {
1978
2193
  return result;
1979
2194
  }
1980
- if ((result = BABEL_ENTRY_FACTORY(filePath, self))) {
2195
+ if ((result = BABEL_ENTRY_FACTORY(filePath, self, seen))) {
1981
2196
  return result;
1982
2197
  }
1983
2198
  }
1984
2199
  throw new Error(`Failed to load module at ${filePath} (basepath: ${self.params.path})`);
1985
2200
  };
1986
- const CREATE_BASE_REQUIRE_FN = (self) => {
1987
- const baseRequire = createRequire(self.__filename);
2201
+ const CREATE_BASE_REQUIRE_FN = (self, seen) => {
2202
+ const baseRequire = self.baseRequire();
1988
2203
  return new Proxy(baseRequire, {
1989
2204
  apply(_target, _this, args) {
1990
2205
  const id = args[0];
@@ -2005,7 +2220,7 @@ const CREATE_BASE_REQUIRE_FN = (self) => {
2005
2220
  if (id.startsWith("./") || id.startsWith("../")) {
2006
2221
  const resolved = path.resolve(self.__dirname, id);
2007
2222
  const child = self.fork(path.dirname(resolved));
2008
- return child.import(resolved);
2223
+ return child.import(resolved, seen);
2009
2224
  }
2010
2225
  return baseRequire(id);
2011
2226
  },
@@ -2019,11 +2234,11 @@ const BacktestKitCli = new Proxy({}, {
2019
2234
  class ClientLoader {
2020
2235
  constructor(params) {
2021
2236
  this.params = params;
2022
- this.getBaseRequire = singleshot(() => {
2023
- this.params.logger.log("ClientLoader getBaseRequire", {
2237
+ this.baseRequire = singleshot(() => {
2238
+ this.params.logger.log("ClientLoader baseRequire", {
2024
2239
  basePath: this.params.path,
2025
2240
  });
2026
- return CREATE_BASE_REQUIRE_FN(this);
2241
+ return createRequire(this.__filename);
2027
2242
  });
2028
2243
  this.__filename = path.join(params.path, "index.cjs");
2029
2244
  this.__dirname = path.dirname(this.__filename);
@@ -2039,12 +2254,21 @@ class ClientLoader {
2039
2254
  logger: this.params.logger,
2040
2255
  });
2041
2256
  }
2042
- import(filePath) {
2257
+ import(filePath, seen = new Set()) {
2043
2258
  this.params.logger.log("ClientLoader import", {
2044
2259
  filePath,
2045
2260
  basePath: this.params.path,
2046
2261
  });
2047
- return ENTRY_FACTORY(filePath, this);
2262
+ const resolved = GET_RESOLVED_EXT_FN(filePath);
2263
+ if (seen.has(resolved)) {
2264
+ throw new Error(`Circular dependency detected: ${resolved} (seen: ${[...seen].join("->")}->${resolved})`);
2265
+ }
2266
+ const currentSeen = new Set(seen);
2267
+ if (!seen.size) {
2268
+ currentSeen.add(path.resolve(this.__dirname, filePath));
2269
+ }
2270
+ currentSeen.add(resolved);
2271
+ return ENTRY_FACTORY(resolved, this, currentSeen);
2048
2272
  }
2049
2273
  check(filePath) {
2050
2274
  this.params.logger.log("ClientLoader check", {
@@ -2112,6 +2336,7 @@ class LoaderService {
2112
2336
  }
2113
2337
  {
2114
2338
  provide(TYPES.backtestMainService, () => new BacktestMainService());
2339
+ provide(TYPES.walkerMainService, () => new WalkerMainService());
2115
2340
  provide(TYPES.paperMainService, () => new PaperMainService());
2116
2341
  provide(TYPES.liveMainService, () => new LiveMainService());
2117
2342
  }
@@ -2151,6 +2376,7 @@ const connectionServices = {
2151
2376
  };
2152
2377
  const mainServices = {
2153
2378
  backtestMainService: inject(TYPES.backtestMainService),
2379
+ walkerMainService: inject(TYPES.walkerMainService),
2154
2380
  paperMainService: inject(TYPES.paperMainService),
2155
2381
  liveMainService: inject(TYPES.liveMainService),
2156
2382
  };
@@ -2186,14 +2412,14 @@ const cli = {
2186
2412
  };
2187
2413
  init();
2188
2414
 
2189
- const MODES = ["backtest", "paper", "live", "pine", "dump", "init", "help", "version"];
2415
+ const MODES = ["backtest", "walker", "paper", "live", "pine", "dump", "init", "help", "version"];
2190
2416
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
2191
- const HELP_TEXT$1 = `
2192
- Example:
2193
-
2194
- node ${ENTRY_PATH$1} --help
2417
+ const HELP_TEXT$1 = `
2418
+ Example:
2419
+
2420
+ node ${ENTRY_PATH$1} --help
2195
2421
  `.trimStart();
2196
- const main$a = async () => {
2422
+ const main$b = async () => {
2197
2423
  if (!getEntry(import.meta.url)) {
2198
2424
  return;
2199
2425
  }
@@ -2201,21 +2427,21 @@ const main$a = async () => {
2201
2427
  if (MODES.some((mode) => values[mode])) {
2202
2428
  return;
2203
2429
  }
2204
- process.stdout.write(`@backtest-kit/cli ${"6.1.1"}\n`);
2430
+ process.stdout.write(`@backtest-kit/cli ${"6.2.1"}\n`);
2205
2431
  process.stdout.write("\n");
2206
2432
  process.stdout.write(`Run with --help to see available commands.\n`);
2207
2433
  process.stdout.write("\n");
2208
2434
  process.stdout.write(HELP_TEXT$1);
2209
2435
  process.exit(0);
2210
2436
  };
2211
- main$a();
2437
+ main$b();
2212
2438
 
2213
2439
  const notifyShutdown = singleshot(async () => {
2214
2440
  console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
2215
2441
  });
2216
2442
 
2217
- const BEFORE_EXIT_FN$4 = singleshot(async () => {
2218
- process.off("SIGINT", BEFORE_EXIT_FN$4);
2443
+ const BEFORE_EXIT_FN$5 = singleshot(async () => {
2444
+ process.off("SIGINT", BEFORE_EXIT_FN$5);
2219
2445
  const [running = null] = await Backtest.list();
2220
2446
  if (!running) {
2221
2447
  return;
@@ -2231,6 +2457,35 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
2231
2457
  frameName,
2232
2458
  });
2233
2459
  });
2460
+ const listenGracefulShutdown$5 = singleshot(() => {
2461
+ process.on("SIGINT", BEFORE_EXIT_FN$5);
2462
+ });
2463
+ const main$a = async () => {
2464
+ if (!getEntry(import.meta.url)) {
2465
+ return;
2466
+ }
2467
+ const { values } = getArgs();
2468
+ if (!values.backtest) {
2469
+ return;
2470
+ }
2471
+ await cli.backtestMainService.connect();
2472
+ listenGracefulShutdown$5();
2473
+ };
2474
+ main$a();
2475
+
2476
+ const BEFORE_EXIT_FN$4 = singleshot(async () => {
2477
+ process.off("SIGINT", BEFORE_EXIT_FN$4);
2478
+ const [running = null] = await Walker.list();
2479
+ if (!running) {
2480
+ return;
2481
+ }
2482
+ notifyShutdown();
2483
+ const { walkerName, symbol, status } = running;
2484
+ if (status === "fulfilled") {
2485
+ return;
2486
+ }
2487
+ Walker.stop(symbol, { walkerName });
2488
+ });
2234
2489
  const listenGracefulShutdown$4 = singleshot(() => {
2235
2490
  process.on("SIGINT", BEFORE_EXIT_FN$4);
2236
2491
  });
@@ -2239,11 +2494,11 @@ const main$9 = async () => {
2239
2494
  return;
2240
2495
  }
2241
2496
  const { values } = getArgs();
2242
- if (!values.backtest) {
2497
+ if (!values.walker) {
2243
2498
  return;
2244
2499
  }
2245
- await cli.backtestMainService.connect();
2246
2500
  listenGracefulShutdown$4();
2501
+ await cli.walkerMainService.connect();
2247
2502
  };
2248
2503
  main$9();
2249
2504
 
@@ -2379,7 +2634,7 @@ const main$4 = async () => {
2379
2634
  if (!values.pine) {
2380
2635
  return;
2381
2636
  }
2382
- const entryPoint = getPositional();
2637
+ const [entryPoint = null] = getPositionals();
2383
2638
  if (!entryPoint) {
2384
2639
  return;
2385
2640
  }
@@ -2573,6 +2828,20 @@ function runScript(scriptPath, cwd) {
2573
2828
  child.on("error", reject);
2574
2829
  });
2575
2830
  }
2831
+ function runNpmInstall(cwd) {
2832
+ return new Promise((resolve, reject) => {
2833
+ const npm = process.platform === "win32" ? "npm.cmd" : "npm";
2834
+ const child = spawn(npm, ["install"], { cwd, stdio: "inherit", shell: true });
2835
+ child.on("close", (code) => {
2836
+ if (code !== 0) {
2837
+ reject(new Error(`npm install exited with code ${code}`));
2838
+ return;
2839
+ }
2840
+ resolve();
2841
+ });
2842
+ child.on("error", reject);
2843
+ });
2844
+ }
2576
2845
  const main$2 = async () => {
2577
2846
  if (!getEntry(import.meta.url)) {
2578
2847
  return;
@@ -2593,109 +2862,130 @@ const main$2 = async () => {
2593
2862
  await copyDir(templatePath, projectPath, { PROJECT_NAME: projectName });
2594
2863
  console.log(`Fetching docs...`);
2595
2864
  await runScript(join(projectPath, "scripts/fetch_docs.mjs"), projectPath);
2865
+ console.log(`Installing dependencies...`);
2866
+ await runNpmInstall(projectPath);
2596
2867
  console.log(`Done! Project created at ${projectPath}`);
2597
2868
  process.exit(0);
2598
2869
  };
2599
2870
  main$2();
2600
2871
 
2601
2872
  const ENTRY_PATH = "./node_modules/@backtest-kit/cli/build/index.mjs";
2602
- const HELP_TEXT = `
2603
- @backtest-kit/cli
2604
-
2605
- Usage:
2606
- node index.mjs --<mode> [flags] [entry-point]
2607
-
2608
- Modes:
2609
-
2610
- --backtest <entry> Run strategy against historical candle data
2611
- --paper <entry> Paper trading (live prices, no real orders)
2612
- --live <entry> Live trading with real orders
2613
- --pine <entry> Execute a local .pine indicator file
2614
- --dump Fetch and save raw OHLCV candles
2615
- --init Scaffold a new project in the current directory
2616
- --help Print this help message
2617
-
2618
- Backtest flags:
2619
-
2620
- --symbol <string> Trading pair (default: BTCUSDT)
2621
- --strategy <string> Strategy name from addStrategySchema (default: first registered)
2622
- --exchange <string> Exchange name from addExchangeSchema (default: first registered)
2623
- --frame <string> Frame name from addFrameSchema (default: first registered)
2624
- --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
2625
- --noCache Skip candle cache warming before the run
2626
- --verbose Log every candle fetch to stdout
2627
- --ui Start web dashboard at http://localhost:60050
2628
- --telegram Send trade notifications to Telegram
2629
-
2630
- Paper / Live flags:
2631
-
2632
- --symbol <string> Trading pair (default: BTCUSDT)
2633
- --strategy <string> Strategy name (default: first registered)
2634
- --exchange <string> Exchange name (default: first registered)
2635
- --verbose Log every candle fetch to stdout
2636
- --ui Start web dashboard
2637
- --telegram Send Telegram notifications
2638
-
2639
- PineScript flags (--pine):
2640
-
2641
- --symbol <string> Trading pair (default: BTCUSDT)
2642
- --timeframe <string> Candle interval (default: 15m)
2643
- --limit <string> Number of candles to fetch (default: 250)
2644
- --when <string> End date ISO 8601 or Unix ms (default: now)
2645
- --exchange <string> Exchange name (default: first registered)
2646
- --output <string> Output file base name without extension
2647
- --json Save output as JSON array to <pine-dir>/dump/<output>.json
2648
- --jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
2649
- --markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
2650
-
2651
- Only plot() calls with display=display.data_window produce output columns.
2652
- Module file ./modules/pine.module is loaded automatically if it exists.
2653
-
2654
- Candle dump flags (--dump):
2655
-
2656
- --symbol <string> Trading pair (default: BTCUSDT)
2657
- --timeframe <string> Candle interval (default: 15m)
2658
- --limit <string> Number of candles (default: 250)
2659
- --when <string> End date ISO 8601 or Unix ms (default: now)
2660
- --exchange <string> Exchange name (default: first registered)
2661
- --output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
2662
- --json Save as JSON array to ./dump/<output>.json
2663
- --jsonl Save as JSONL to ./dump/<output>.jsonl
2664
-
2665
- Module file ./modules/dump.module is loaded automatically if it exists.
2666
-
2667
- Init flags (--init):
2668
-
2669
- --output <string> Target directory name (default: backtest-kit-project)
2670
-
2671
- Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
2672
-
2673
- Module hooks (loaded automatically by each mode):
2674
-
2675
- modules/backtest.module --backtest Broker adapter for backtest
2676
- modules/paper.module --paper Broker adapter for paper trading
2677
- modules/live.module --live Broker adapter for live trading
2678
- modules/pine.module --pine Exchange schema for PineScript runs
2679
- modules/dump.module --dump Exchange schema for candle dumps
2680
-
2681
- Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
2682
-
2683
- Environment variables:
2684
-
2685
- CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
2686
- CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
2687
- CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
2688
- CC_WWWROOT_PORT UI server port (default: 60050)
2689
-
2690
- Examples:
2691
-
2692
- node ${ENTRY_PATH} --backtest ./content/feb_2026.strategy.ts
2693
- node ${ENTRY_PATH} --backtest --symbol BTCUSDT --noCache --ui ./content/feb_2026.strategy.ts
2694
- node ${ENTRY_PATH} --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
2695
- node ${ENTRY_PATH} --live --ui --telegram ./content/feb_2026.strategy.ts
2696
- node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
2697
- node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
2698
- node ${ENTRY_PATH} --init --output my-trading-bot
2873
+ const HELP_TEXT = `
2874
+ Usage:
2875
+ node index.mjs --<mode> [flags] [entry-point]
2876
+
2877
+ Modes:
2878
+
2879
+ --backtest <entry> Run strategy against historical candle data
2880
+ --walker <entry...> Run Walker A/B strategy comparison across multiple strategies
2881
+ --paper <entry> Paper trading (live prices, no real orders)
2882
+ --live <entry> Live trading with real orders
2883
+ --pine <entry> Execute a local .pine indicator file
2884
+ --dump Fetch and save raw OHLCV candles
2885
+ --init Scaffold a new project in the current directory
2886
+ --help Print this help message
2887
+
2888
+ Backtest flags:
2889
+
2890
+ --symbol <string> Trading pair (default: BTCUSDT)
2891
+ --strategy <string> Strategy name from addStrategySchema (default: first registered)
2892
+ --exchange <string> Exchange name from addExchangeSchema (default: first registered)
2893
+ --frame <string> Frame name from addFrameSchema (default: first registered)
2894
+ --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
2895
+ --noCache Skip candle cache warming before the run
2896
+ --verbose Log every candle fetch to stdout
2897
+ --ui Start web dashboard at http://localhost:60050
2898
+ --telegram Send trade notifications to Telegram
2899
+
2900
+ Walker flags (--walker):
2901
+
2902
+ --symbol <string> Trading pair (default: BTCUSDT)
2903
+ --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
2904
+ --noCache Skip candle cache warming before the run
2905
+ --verbose Log every candle fetch to stdout
2906
+ --output <string> Output file base name (default: walker_{SYMBOL}_{TIMESTAMP})
2907
+ --json Save results as JSON to ./dump/<output>.json
2908
+ --markdown Save report as Markdown to ./dump/<output>.md
2909
+
2910
+ Each positional argument is a strategy entry point. All strategy files are loaded without
2911
+ changing process.cwd() — .env is read from the working directory only.
2912
+ addWalkerSchema is called automatically using the registered exchange and frame.
2913
+ After comparison completes the report is printed to stdout (or saved if --json/--markdown).
2914
+
2915
+ Module file ./modules/walker.module is loaded automatically if it exists.
2916
+
2917
+ Paper / Live flags:
2918
+
2919
+ --symbol <string> Trading pair (default: BTCUSDT)
2920
+ --strategy <string> Strategy name (default: first registered)
2921
+ --exchange <string> Exchange name (default: first registered)
2922
+ --verbose Log every candle fetch to stdout
2923
+ --ui Start web dashboard
2924
+ --telegram Send Telegram notifications
2925
+
2926
+ PineScript flags (--pine):
2927
+
2928
+ --symbol <string> Trading pair (default: BTCUSDT)
2929
+ --timeframe <string> Candle interval (default: 15m)
2930
+ --limit <string> Number of candles to fetch (default: 250)
2931
+ --when <string> End date — ISO 8601 or Unix ms (default: now)
2932
+ --exchange <string> Exchange name (default: first registered)
2933
+ --output <string> Output file base name without extension
2934
+ --json Save output as JSON array to <pine-dir>/dump/<output>.json
2935
+ --jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
2936
+ --markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
2937
+
2938
+ Only plot() calls with display=display.data_window produce output columns.
2939
+ Module file ./modules/pine.module is loaded automatically if it exists.
2940
+
2941
+ Candle dump flags (--dump):
2942
+
2943
+ --symbol <string> Trading pair (default: BTCUSDT)
2944
+ --timeframe <string> Candle interval (default: 15m)
2945
+ --limit <string> Number of candles (default: 250)
2946
+ --when <string> End date ISO 8601 or Unix ms (default: now)
2947
+ --exchange <string> Exchange name (default: first registered)
2948
+ --output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
2949
+ --json Save as JSON array to ./dump/<output>.json
2950
+ --jsonl Save as JSONL to ./dump/<output>.jsonl
2951
+
2952
+ Module file ./modules/dump.module is loaded automatically if it exists.
2953
+
2954
+ Init flags (--init):
2955
+
2956
+ --output <string> Target directory name (default: backtest-kit-project)
2957
+
2958
+ Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
2959
+
2960
+ Module hooks (loaded automatically by each mode):
2961
+
2962
+ modules/backtest.module --backtest Broker adapter for backtest
2963
+ modules/walker.module --walker Broker adapter for walker comparison
2964
+ modules/paper.module --paper Broker adapter for paper trading
2965
+ modules/live.module --live Broker adapter for live trading
2966
+ modules/pine.module --pine Exchange schema for PineScript runs
2967
+ modules/dump.module --dump Exchange schema for candle dumps
2968
+
2969
+ Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
2970
+
2971
+ Environment variables:
2972
+
2973
+ CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
2974
+ CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
2975
+ CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
2976
+ CC_WWWROOT_PORT UI server port (default: 60050)
2977
+
2978
+ Examples:
2979
+
2980
+ node ${ENTRY_PATH} --backtest ./content/feb_2026.strategy.ts
2981
+ node ${ENTRY_PATH} --backtest --symbol BTCUSDT --noCache --ui ./content/feb_2026.strategy.ts
2982
+ node ${ENTRY_PATH} --walker ./content/feb_2026_v1.strategy.ts ./content/feb_2026_v2.strategy.ts ./content/feb_2026_v3.strategy.ts
2983
+ node ${ENTRY_PATH} --walker --symbol BTCUSDT --noCache --markdown ./content/feb_2026_v1.ts ./content/feb_2026_v2.ts
2984
+ node ${ENTRY_PATH} --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
2985
+ node ${ENTRY_PATH} --live --ui --telegram ./content/feb_2026.strategy.ts
2986
+ node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
2987
+ node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
2988
+ node ${ENTRY_PATH} --init --output my-trading-bot
2699
2989
  `.trimStart();
2700
2990
  const main$1 = async () => {
2701
2991
  if (!getEntry(import.meta.url)) {
@@ -2705,6 +2995,7 @@ const main$1 = async () => {
2705
2995
  if (!values.help) {
2706
2996
  return;
2707
2997
  }
2998
+ process.stdout.write(`@backtest-kit/cli ${"6.2.1"}\n\n`);
2708
2999
  process.stdout.write(HELP_TEXT);
2709
3000
  process.exit(0);
2710
3001
  };
@@ -2718,7 +3009,7 @@ const main = async () => {
2718
3009
  if (!values.version) {
2719
3010
  return;
2720
3011
  }
2721
- process.stdout.write(`@backtest-kit/cli ${"6.1.1"}\n`);
3012
+ process.stdout.write(`@backtest-kit/cli ${"6.2.1"}\n`);
2722
3013
  process.exit(0);
2723
3014
  };
2724
3015
  main();
@@ -2740,7 +3031,7 @@ async function run(mode, args) {
2740
3031
  }
2741
3032
  if (mode === "backtest") {
2742
3033
  await cli.backtestMainService.run(args);
2743
- listenGracefulShutdown$4();
3034
+ listenGracefulShutdown$5();
2744
3035
  return;
2745
3036
  }
2746
3037
  if (mode === "paper") {