@backtest-kit/cli 14.1.0 → 15.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.mjs CHANGED
@@ -85,17 +85,28 @@ const main$i = () => {
85
85
  main$i();
86
86
 
87
87
  const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
88
- function dumpStackTrace() {
89
- const trace = stackTrace.get();
90
- const result = [];
91
- trace.forEach((callSite) => {
92
- result.push(`File: ${callSite.getFileName()}`);
93
- result.push(`Line: ${callSite.getLineNumber()}`);
94
- result.push(`Function: ${callSite.getFunctionName() || "anonymous"}`);
95
- result.push(`Method: ${callSite.getMethodName() || "none"}`);
96
- result.push("---");
97
- });
98
- return str.newline(result);
88
+ function dumpStackTrace(error) {
89
+ // Parse the ERROR's own stack — stackTrace.get() captured the handler's
90
+ // stack instead, so error.txt carried a useless trace of ErrorService itself.
91
+ if (!(error instanceof Error) || !error.stack) {
92
+ return "";
93
+ }
94
+ try {
95
+ const trace = stackTrace.parse(error);
96
+ const result = [];
97
+ trace.forEach((callSite) => {
98
+ result.push(`File: ${callSite.getFileName()}`);
99
+ result.push(`Line: ${callSite.getLineNumber()}`);
100
+ result.push(`Function: ${callSite.getFunctionName() || "anonymous"}`);
101
+ result.push(`Method: ${callSite.getMethodName() || "none"}`);
102
+ result.push("---");
103
+ });
104
+ return str.newline(result);
105
+ }
106
+ catch {
107
+ // Unparseable/exotic stack format — dump the raw stack rather than nothing
108
+ return error.stack;
109
+ }
99
110
  }
100
111
  const timeNow = () => {
101
112
  const d = new Date();
@@ -115,7 +126,7 @@ class ErrorService {
115
126
  message: getErrorMessage(error),
116
127
  data: errorData(error),
117
128
  }, null, 2);
118
- const trace = dumpStackTrace();
129
+ const trace = dumpStackTrace(error);
119
130
  fs.appendFileSync("./error.txt", `${date}\n${msg}\n${trace}\n\n`);
120
131
  };
121
132
  this._listenForError = () => {
@@ -240,10 +251,6 @@ const SETUP_ADAPTER_FN = () => {
240
251
  StorageLive.usePersist();
241
252
  StorageBacktest.useMemory();
242
253
  }
243
- {
244
- RecentLive.usePersist();
245
- RecentBacktest.useMemory();
246
- }
247
254
  {
248
255
  NotificationLive.usePersist();
249
256
  NotificationBacktest.useMemory();
@@ -309,6 +316,7 @@ class SetupUtils {
309
316
  Markdown.disable();
310
317
  Report.disable();
311
318
  Dump.disable();
319
+ State.disable();
312
320
  Memory.disable();
313
321
  }
314
322
  {
@@ -497,7 +505,11 @@ const ADD_EXCHANGE_FN = (self) => {
497
505
  formatPrice: async (symbol, price) => {
498
506
  const exchange = await getExchange();
499
507
  const market = exchange.market(symbol);
500
- const tickSize = market.limits?.price?.min || market.precision?.price;
508
+ // ccxt binance uses TICK_SIZE precision mode, so precision.price IS the
509
+ // tick size. limits.price.min (PRICE_FILTER minPrice) is only a fallback:
510
+ // it merely coincides with the tick on most Binance markets and is NOT
511
+ // the tick size by contract.
512
+ const tickSize = [market.precision?.price, market.limits?.price?.min].find((value) => typeof value === "number" && isFinite(value) && value > 0);
501
513
  if (tickSize !== undefined) {
502
514
  return roundTicks(price, tickSize);
503
515
  }
@@ -506,7 +518,10 @@ const ADD_EXCHANGE_FN = (self) => {
506
518
  formatQuantity: async (symbol, quantity) => {
507
519
  const exchange = await getExchange();
508
520
  const market = exchange.market(symbol);
509
- const stepSize = market.limits?.amount?.min || market.precision?.amount;
521
+ // Same rule as formatPrice: precision.amount is the LOT_SIZE step;
522
+ // limits.amount.min is minQty, which can exceed the step on some markets
523
+ // and would over-truncate quantities if preferred.
524
+ const stepSize = [market.precision?.amount, market.limits?.amount?.min].find((value) => typeof value === "number" && isFinite(value) && value > 0);
510
525
  if (stepSize !== undefined) {
511
526
  return roundTicks(quantity, stepSize);
512
527
  }
@@ -651,7 +666,7 @@ const getArgs = singleshot(() => {
651
666
  },
652
667
  cacheInterval: {
653
668
  type: "string",
654
- default: "1m, 15m, 30m, 4h",
669
+ default: "1m, 15m, 30m, 1h, 4h",
655
670
  },
656
671
  // pinescript entry
657
672
  editor: {
@@ -983,6 +998,8 @@ const notifyKill = singleshot(() => {
983
998
  process.on("SIGINT", () => kill());
984
999
  });
985
1000
 
1001
+ // Must match the getArgs --cacheInterval default ("1m, 15m, 30m, 1h, 4h"):
1002
+ // this fallback is only reachable with an explicit --cacheInterval ""
986
1003
  const DEFAULT_CACHE_LIST$1 = ["1m", "15m", "30m", "1h", "4h"];
987
1004
  const GET_CACHE_INTERVAL_LIST_FN$1 = () => {
988
1005
  const { values } = getArgs();
@@ -1125,6 +1142,8 @@ var WalkerName;
1125
1142
  })(WalkerName || (WalkerName = {}));
1126
1143
  var WalkerName$1 = WalkerName;
1127
1144
 
1145
+ // Must match the getArgs --cacheInterval default ("1m, 15m, 30m, 1h, 4h"):
1146
+ // this fallback is only reachable with an explicit --cacheInterval ""
1128
1147
  const DEFAULT_CACHE_LIST = ["1m", "15m", "30m", "1h", "4h"];
1129
1148
  const GET_CACHE_INTERVAL_LIST_FN = () => {
1130
1149
  const { values } = getArgs();
@@ -1635,7 +1654,7 @@ const MAP_SYMBOL_CONFIG_FN = (config) => {
1635
1654
  symbol,
1636
1655
  icon,
1637
1656
  logo: logo ?? icon,
1638
- priority: priority ?? idx,
1657
+ priority: priority ?? -idx,
1639
1658
  displayName: displayName ?? symbol,
1640
1659
  index: idx,
1641
1660
  ...other,
@@ -1799,20 +1818,33 @@ class QuickchartApiService {
1799
1818
  const HEALTH_CHECK_DELAY = 60000;
1800
1819
  let _is_stopped = false;
1801
1820
  const handleHealthCheck = singleshot(async (bot) => {
1802
- const fn = async () => {
1803
- if (_is_stopped) {
1804
- return;
1805
- }
1806
- try {
1807
- await bot.telegram.getMe();
1808
- setTimeout(fn, HEALTH_CHECK_DELAY);
1809
- }
1810
- catch (error) {
1811
- console.log("Bot is offline");
1812
- throw new Error("Telegram goes offline");
1813
- }
1821
+ // Первый чек выполняется синхронно с запуском: ошибка уходит вызывающему
1822
+ // getTelegram (бот не стартует, если Telegram недоступен сразу).
1823
+ try {
1824
+ await bot.telegram.getMe();
1825
+ }
1826
+ catch {
1827
+ console.log("Bot is offline");
1828
+ throw new Error("Telegram goes offline");
1829
+ }
1830
+ // Периодические чеки: раньше throw внутри setTimeout-цикла становился
1831
+ // unhandledRejection (никем не await-ился) и ронял процесс неконтролируемо,
1832
+ // оставляя дочерние процессы (UI-сервер) живыми. Теперь — управляемый kill.
1833
+ const schedule = () => {
1834
+ setTimeout(() => {
1835
+ if (_is_stopped) {
1836
+ return;
1837
+ }
1838
+ bot.telegram
1839
+ .getMe()
1840
+ .then(schedule)
1841
+ .catch(() => {
1842
+ console.log("Bot is offline");
1843
+ kill(-1);
1844
+ });
1845
+ }, HEALTH_CHECK_DELAY);
1814
1846
  };
1815
- await fn();
1847
+ schedule();
1816
1848
  });
1817
1849
  const getTelegram = singleshot(async () => {
1818
1850
  if (_is_stopped) {
@@ -1911,8 +1943,11 @@ const publishInternal = queued(async ({ channel, msg, images = [], onScheduled,
1911
1943
  onScheduled();
1912
1944
  let isOk = true;
1913
1945
  try {
1946
+ // Must persist across flood retries: declared inside `execute` it was reset
1947
+ // on every recursive retry, re-sending the media group (duplicate channel
1948
+ // posts) when the flood error hit the follow-up sendMessage.
1949
+ let isImagesPublished = false;
1914
1950
  const execute = async (retry = 0) => {
1915
- let isImagesPublished = false;
1916
1951
  try {
1917
1952
  if (images?.length) {
1918
1953
  console.log("Bot fetching images");
@@ -2013,21 +2048,67 @@ class TelegramApiService {
2013
2048
  task.finally(() => {
2014
2049
  TIMEOUT_COUNTER -= 1;
2015
2050
  });
2051
+ // The watchdog must fire HERE: when every publish is stuck, this branch is
2052
+ // the only one executing — checking after the early return made the kill
2053
+ // reachable only once a publish suddenly succeeded (problem already gone).
2054
+ if (TIMEOUT_COUNTER > MAX_TIMEOUT_COUNT) {
2055
+ setTimeout(() => kill(-1), 5000);
2056
+ }
2016
2057
  return "Message scheduled for publication";
2017
2058
  }
2018
- if (TIMEOUT_COUNTER > MAX_TIMEOUT_COUNT) {
2019
- setTimeout(() => kill(-1), 5000);
2020
- }
2021
2059
  return "Message published successfully";
2022
2060
  };
2023
2061
  }
2024
2062
  }
2025
2063
 
2026
- const TELEGAM_MAX_SYMBOLS = 4090;
2064
+ const TELEGRAM_MAX_SYMBOLS = 4090;
2027
2065
  const TELEGRAM_SYMBOL_RESERVED = 96;
2066
+ /** Void tags never get a closing counterpart when balancing truncated HTML. */
2067
+ const VOID_TAGS = new Set(["br"]);
2068
+ /**
2069
+ * Truncates Telegram HTML without breaking it: a blunt substring could cut a
2070
+ * tag or entity in half and leave unbalanced tags — Telegram then rejects the
2071
+ * WHOLE message ("can't parse entities"), so the notification would be lost
2072
+ * instead of shortened. Closing tags appended here fit into
2073
+ * TELEGRAM_SYMBOL_RESERVED headroom.
2074
+ */
2075
+ const truncateTelegramHtml = (html, maxChars) => {
2076
+ let result = html.substring(0, maxChars);
2077
+ // Don't cut a tag in half
2078
+ const lastOpen = result.lastIndexOf("<");
2079
+ if (lastOpen > result.lastIndexOf(">")) {
2080
+ result = result.substring(0, lastOpen);
2081
+ }
2082
+ // Don't cut an HTML entity in half
2083
+ result = result.replace(/&[a-zA-Z0-9#]*$/, "");
2084
+ // Close tags left open by the cut (input is sanitize-html output — well-formed)
2085
+ const stack = [];
2086
+ const tagRe = /<(\/?)([a-zA-Z][a-zA-Z0-9-]*)(?:\s[^>]*)?>/g;
2087
+ let match;
2088
+ while ((match = tagRe.exec(result))) {
2089
+ const [, slash, rawName] = match;
2090
+ const name = rawName.toLowerCase();
2091
+ if (VOID_TAGS.has(name)) {
2092
+ continue;
2093
+ }
2094
+ if (slash) {
2095
+ const index = stack.lastIndexOf(name);
2096
+ if (index !== -1) {
2097
+ stack.splice(index, 1);
2098
+ }
2099
+ }
2100
+ else {
2101
+ stack.push(name);
2102
+ }
2103
+ }
2104
+ for (let i = stack.length - 1; i >= 0; i--) {
2105
+ result += `</${stack[i]}>`;
2106
+ }
2107
+ return result;
2108
+ };
2028
2109
  // Function to convert Markdown to Telegram-compatible HTML
2029
2110
  function toTelegramHtml(markdown) {
2030
- const maxChars = TELEGAM_MAX_SYMBOLS - TELEGRAM_SYMBOL_RESERVED;
2111
+ const maxChars = TELEGRAM_MAX_SYMBOLS - TELEGRAM_SYMBOL_RESERVED;
2031
2112
  // Initialize markdown-it with options
2032
2113
  const md = new MarkdownIt({
2033
2114
  html: false,
@@ -2040,6 +2121,15 @@ function toTelegramHtml(markdown) {
2040
2121
  markdown = markdown.replaceAll(typo.bullet, "&bull;");
2041
2122
  // Render Markdown to HTML
2042
2123
  let telegramHtml = md.render(markdown);
2124
+ // List markers must be injected as TEXT inside <li> before sanitizing:
2125
+ // sanitize-html transformTags cannot insert text (a transform returning a
2126
+ // string like "- " is silently ignored), so list items were losing their
2127
+ // bullets/numbers entirely. Ordered lists get 1./2./..., unordered get "-".
2128
+ telegramHtml = telegramHtml.replace(/<ol>([\s\S]*?)<\/ol>/g, (_, inner) => {
2129
+ let counter = 0;
2130
+ return `<ol>${inner.replace(/<li>/g, () => `<li>${++counter}. `)}</ol>`;
2131
+ });
2132
+ telegramHtml = telegramHtml.replace(/<li>(?!\d+\. )/g, "<li>- ");
2043
2133
  // Post-process with sanitize-html to ensure Telegram compatibility
2044
2134
  telegramHtml = sanitizeHtml(telegramHtml, {
2045
2135
  allowedTags: [
@@ -2071,10 +2161,9 @@ function toTelegramHtml(markdown) {
2071
2161
  em: "i",
2072
2162
  // Remove p tags, replace with newlines
2073
2163
  p: () => "",
2074
- // Emulate unordered lists with bullets
2164
+ // ul/ol/li tags are dropped by allowedTags; their text markers are injected
2165
+ // BEFORE sanitizing (see above) — a string-returning transform can't do it
2075
2166
  ul: () => "",
2076
- li: () => "- ",
2077
- // Emulate ordered lists with numbers
2078
2167
  ol: () => "",
2079
2168
  // Remove hr, replace with text-based separator
2080
2169
  hr: () => "\n",
@@ -2095,7 +2184,7 @@ function toTelegramHtml(markdown) {
2095
2184
  // Check Telegram message length limit (4096 characters)
2096
2185
  if (telegramHtml.length > maxChars) {
2097
2186
  console.warn("HTML exceeds Telegram's 4096-character limit. Truncating...");
2098
- telegramHtml = telegramHtml.substring(0, maxChars);
2187
+ telegramHtml = truncateTelegramHtml(telegramHtml, maxChars);
2099
2188
  }
2100
2189
  const telegramDom = new JSDOM(telegramHtml, {
2101
2190
  contentType: "text/html",
@@ -2336,7 +2425,7 @@ class TelegramLogicService {
2336
2425
  });
2337
2426
  };
2338
2427
  this.notifyRisk = async (event) => {
2339
- this.loggerService.log("telegramLogicService notifyClosed", {
2428
+ this.loggerService.log("telegramLogicService notifyRisk", {
2340
2429
  event,
2341
2430
  });
2342
2431
  const markdown = await this.telegramTemplateService.getRiskMarkdown(event);
@@ -2485,6 +2574,11 @@ class TelegramLogicService {
2485
2574
  });
2486
2575
  const unSync = listenSync(async (event) => {
2487
2576
  if (event.action === "signal-open") {
2577
+ // type "schedule" is a resting-order PLACEMENT, not a position open:
2578
+ // the user already got the "scheduled" notification from listenSignal
2579
+ if (event.type !== "active") {
2580
+ return;
2581
+ }
2488
2582
  await this.notifySignalOpen(event);
2489
2583
  return;
2490
2584
  }
@@ -2727,8 +2821,8 @@ const LOAD_MODULE_MODULE_FN = async (fileName, self) => {
2727
2821
  return true;
2728
2822
  }
2729
2823
  }
2730
- catch {
2731
- console.warn(`Module module import failed filePath=${filePath} baseDir=${baseDir}`);
2824
+ catch (error) {
2825
+ console.warn(`Module import failed filePath=${filePath} baseDir=${baseDir}`, error);
2732
2826
  kill(-1);
2733
2827
  return false;
2734
2828
  }
@@ -2763,12 +2857,12 @@ registerPlugin("plugin-transform-modules-umd", pluginUMD);
2763
2857
  class BabelService {
2764
2858
  constructor() {
2765
2859
  this.loggerService = inject(TYPES.loggerService);
2766
- this.transpile = (code) => {
2767
- this.loggerService.log("babelService transpile", { codeLen: code.length });
2860
+ this.transpile = (code, filename) => {
2861
+ this.loggerService.log("babelService transpile", { codeLen: code.length, filename });
2768
2862
  const { values } = getArgs();
2769
2863
  const result = transform(code, {
2770
- filename: "index.ts",
2771
- presets: ["env", "typescript"],
2864
+ filename,
2865
+ presets: filename.endsWith(".tsx") ? ["env", "react", "typescript"] : ["env", "typescript"],
2772
2866
  plugins: [
2773
2867
  [
2774
2868
  "plugin-transform-modules-umd",
@@ -2846,7 +2940,7 @@ const TRANSPILE_FN = memoize(([path]) => `${path}`, (path, code, self, require)
2846
2940
  const module = { exports: {} };
2847
2941
  const exports = module.exports;
2848
2942
  try {
2849
- eval(self.params.babel.transpile(code));
2943
+ eval(self.params.babel.transpile(code, path));
2850
2944
  }
2851
2945
  catch (error) {
2852
2946
  console.log(`Error during transpilation error=\`${getErrorMessage(error)}\` path=\`${path}\` __filename=\`${__filename}\` __dirname=\`${__dirname}\``);
@@ -2861,25 +2955,35 @@ const TRANSPILE_FN = memoize(([path]) => `${path}`, (path, code, self, require)
2861
2955
  module,
2862
2956
  };
2863
2957
  });
2864
- const REQUIRE_ENTRY_FACTORY = (filePath, self, seen) => {
2958
+ const REQUIRE_ENTRY_FACTORY = (filePath, self, seen, errors) => {
2865
2959
  {
2866
2960
  return null;
2867
2961
  }
2868
2962
  };
2869
- const BABEL_ENTRY_FACTORY = (filePath, self, seen) => {
2963
+ const BABEL_ENTRY_FACTORY = (filePath, self, seen, errors) => {
2870
2964
  try {
2871
2965
  const resolvedPath = path.resolve(self.__dirname, filePath);
2872
2966
  const code = fs.readFileSync(resolvedPath, "utf-8");
2873
2967
  const child = self.fork(path.dirname(resolvedPath));
2874
- const { module } = TRANSPILE_FN(resolvedPath, code, child, CREATE_BASE_REQUIRE_FN(child, seen));
2968
+ const transpiled = TRANSPILE_FN(resolvedPath, code, child, CREATE_BASE_REQUIRE_FN(child, seen));
2969
+ if (!transpiled) {
2970
+ // TRANSPILE_FN already reported the error and scheduled kill(-1)
2971
+ return null;
2972
+ }
2973
+ const { module } = transpiled;
2875
2974
  if (!USE_ESMODULE_DEFAULT) {
2876
- return module.exports;
2975
+ return { exports: module.exports };
2877
2976
  }
2878
- return "default" in module.exports
2879
- ? module.exports.default
2880
- : module.exports;
2977
+ return {
2978
+ exports: "default" in module.exports
2979
+ ? module.exports.default
2980
+ : module.exports,
2981
+ };
2881
2982
  }
2882
- catch {
2983
+ catch (error) {
2984
+ // Never swallow the cause: a nested "Circular dependency detected" (or any
2985
+ // real load error) must surface instead of a generic "Failed to load module".
2986
+ errors.push(error);
2883
2987
  return null;
2884
2988
  }
2885
2989
  };
@@ -2924,16 +3028,18 @@ const GET_RESOLVED_EXT_FN = (filePath) => {
2924
3028
  return filePath;
2925
3029
  };
2926
3030
  const ENTRY_FACTORY = (filePath, self, seen) => {
3031
+ const errors = [];
2927
3032
  {
2928
3033
  let result = null;
2929
3034
  if ((result = REQUIRE_ENTRY_FACTORY())) {
2930
- return result;
3035
+ return result.exports;
2931
3036
  }
2932
- if ((result = BABEL_ENTRY_FACTORY(filePath, self, seen))) {
2933
- return result;
3037
+ if ((result = BABEL_ENTRY_FACTORY(filePath, self, seen, errors))) {
3038
+ return result.exports;
2934
3039
  }
2935
3040
  }
2936
- throw new Error(`Failed to load module at ${filePath} (basepath: ${self.params.path})`);
3041
+ const causes = errors.map(getErrorMessage).filter(Boolean).join("; ");
3042
+ throw new Error(`Failed to load module at ${filePath} (basepath: ${self.params.path})${causes ? ` caused by: ${causes}` : ""}`);
2937
3043
  };
2938
3044
  const READ_IMPORT_PATHS_MAP_FN = singleshot((importPathsDir) => {
2939
3045
  const entries = fs.readdirSync(importPathsDir, { withFileTypes: true });
@@ -3213,8 +3319,8 @@ const LOAD_CONFIG_CONFIG_FN = async (fileName, self) => {
3213
3319
  return await self.loaderService.import(filePath, baseDir);
3214
3320
  }
3215
3321
  }
3216
- catch {
3217
- console.warn(`Module module import failed filePath=${filePath} baseDir=${baseDir}`);
3322
+ catch (error) {
3323
+ console.warn(`Config import failed filePath=${filePath} baseDir=${baseDir}`, error);
3218
3324
  kill(-1);
3219
3325
  return;
3220
3326
  }
@@ -3388,10 +3494,10 @@ init();
3388
3494
 
3389
3495
  const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "flush", "init", "docker", "help", "version"];
3390
3496
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
3391
- const HELP_TEXT$1 = `
3392
- Example:
3393
-
3394
- node ${ENTRY_PATH$1} --help
3497
+ const HELP_TEXT$1 = `
3498
+ Example:
3499
+
3500
+ node ${ENTRY_PATH$1} --help
3395
3501
  `.trimStart();
3396
3502
  const main$h = async () => {
3397
3503
  if (!getEntry(import.meta.url)) {
@@ -3401,7 +3507,7 @@ const main$h = async () => {
3401
3507
  if (MODES.some((mode) => values[mode])) {
3402
3508
  return;
3403
3509
  }
3404
- process.stdout.write(`@backtest-kit/cli ${"14.1.0"}\n`);
3510
+ process.stdout.write(`@backtest-kit/cli ${"15.1.0"}\n`);
3405
3511
  process.stdout.write("\n");
3406
3512
  process.stdout.write(`Run with --help to see available commands.\n`);
3407
3513
  process.stdout.write("\n");
@@ -3553,7 +3659,7 @@ const main$d = async () => {
3553
3659
  if (values.entry) {
3554
3660
  return;
3555
3661
  }
3556
- cli.paperMainService.connect();
3662
+ await cli.paperMainService.connect();
3557
3663
  listenGracefulShutdown$4();
3558
3664
  };
3559
3665
  main$d();
@@ -4107,6 +4213,7 @@ const main$6 = async () => {
4107
4213
  isOk = true;
4108
4214
  }
4109
4215
  catch {
4216
+ isOk = false;
4110
4217
  }
4111
4218
  finally {
4112
4219
  isOk && console.log(`Editor launched: http://localhost:${CC_WWWROOT_PORT}?pine=1`);
@@ -4500,204 +4607,207 @@ const main$2 = async () => {
4500
4607
  main$2();
4501
4608
 
4502
4609
  const ENTRY_PATH = "./node_modules/@backtest-kit/cli/build/index.mjs";
4503
- const HELP_TEXT = `
4504
- Usage:
4505
- node index.mjs --<mode> [flags] [entry-point]
4506
-
4507
- Modes:
4508
-
4509
- --backtest <entry> Run strategy against historical candle data
4510
- --walker <entry...> Run Walker A/B strategy comparison across multiple strategies
4511
- --paper <entry> Paper trading (live prices, no real orders)
4512
- --live <entry> Live trading with real orders
4513
- --main <entry> Run an entry point with prepared environment, no trading harness
4514
- --pine <entry> Execute a local .pine indicator file
4515
- --editor Open the Pine Script visual editor in the browser
4516
- --dump Fetch and save raw OHLCV candles
4517
- --pnldebug Simulate PnL per minute for a given entry price and direction
4518
- --brokerdebug Fire a single broker commit against the live broker adapter
4519
- --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
4520
- --init Scaffold a new project in the current directory
4521
- --docker Scaffold a Docker workspace for running strategies in a container
4522
- --help Print this help message
4523
-
4524
- Backtest flags:
4525
-
4526
- --symbol <string> Trading pair (default: BTCUSDT)
4527
- --strategy <string> Strategy name from addStrategySchema (default: first registered)
4528
- --exchange <string> Exchange name from addExchangeSchema (default: first registered)
4529
- --frame <string> Frame name from addFrameSchema (default: first registered)
4530
- --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
4531
- --noCache Skip candle cache warming before the run
4532
- --noFlush Skip removing report/log/markdown/agent folders before backtest run
4533
- --verbose Log every candle fetch to stdout
4534
- --ui Start web dashboard at http://localhost:60050
4535
- --telegram Send trade notifications to Telegram
4536
-
4537
- Walker flags (--walker):
4538
-
4539
- --symbol <string> Trading pair (default: BTCUSDT)
4540
- --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 4h")
4541
- --noCache Skip candle cache warming before the run
4542
- --noFlush Skip removing report/log/markdown/agent folders before walker run
4543
- --verbose Log every candle fetch to stdout
4544
- --output <string> Output file base name (default: walker_{SYMBOL}_{TIMESTAMP})
4545
- --json Save results as JSON to ./dump/<output>.json
4546
- --markdown Save report as Markdown to ./dump/<output>.md
4547
-
4548
- Each positional argument is a strategy entry point. All strategy files are loaded without
4549
- changing process.cwd() — .env is read from the working directory only.
4550
- addWalkerSchema is called automatically using the registered exchange and frame.
4551
- After comparison completes the report is printed to stdout (or saved if --json/--markdown).
4552
-
4553
- Module file ./modules/walker.module is loaded automatically if it exists.
4554
-
4555
- Paper / Live flags:
4556
-
4557
- --symbol <string> Trading pair (default: BTCUSDT)
4558
- --strategy <string> Strategy name (default: first registered)
4559
- --exchange <string> Exchange name (default: first registered)
4560
- --verbose Log every candle fetch to stdout
4561
- --ui Start web dashboard
4562
- --telegram Send Telegram notifications
4563
-
4564
- Main flags (--main):
4565
-
4566
- --noFlush Skip removing report/log/markdown/agent folders before the run
4567
-
4568
- Prepares the runtime environment (loads .env, setup.config, loader.config and
4569
- modules/main.module) and runs the single positional entry point — but does NOT
4570
- start any trading harness. Unlike --backtest/--live/--walker, the CLI never calls
4571
- Backtest/Live/Walker.background; the entry point decides what to run.
4572
-
4573
- Exactly one positional entry point is required. process.cwd() is changed to the
4574
- entry point directory and its local .env is loaded.
4575
-
4576
- Backtest, Live and Walker runs started from userspace are still tracked: the run
4577
- finishes automatically when one of them completes, Ctrl+C stops any active run,
4578
- and a second Ctrl+C force-quits.
4579
-
4580
- Module file ./modules/main.module is loaded automatically if it exists.
4581
-
4582
- PineScript flags (--pine):
4583
-
4584
- --symbol <string> Trading pair (default: BTCUSDT)
4585
- --timeframe <string> Candle interval (default: 15m)
4586
- --limit <string> Number of candles to fetch (default: 250)
4587
- --when <string> End date — ISO 8601 or Unix ms (default: now)
4588
- --exchange <string> Exchange name (default: first registered)
4589
- --output <string> Output file base name without extension
4590
- --json Save output as JSON array to <pine-dir>/dump/<output>.json
4591
- --jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
4592
- --markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
4593
-
4594
- Only plot() calls with display=display.data_window produce output columns.
4595
- Module file ./modules/pine.module is loaded automatically if it exists.
4596
-
4597
- Candle dump flags (--dump):
4598
-
4599
- --symbol <string> Trading pair (default: BTCUSDT)
4600
- --timeframe <string> Candle interval (default: 15m)
4601
- --limit <string> Number of candles (default: 250)
4602
- --when <string> End date — ISO 8601 or Unix ms (default: now)
4603
- --exchange <string> Exchange name (default: first registered)
4604
- --output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
4605
- --json Save as JSON array to ./dump/<output>.json
4606
- --jsonl Save as JSONL to ./dump/<output>.jsonl
4607
-
4608
- Module file ./modules/dump.module is loaded automatically if it exists.
4609
-
4610
- PnL debug flags (--pnldebug):
4611
-
4612
- --symbol <string> Trading pair (default: BTCUSDT)
4613
- --priceopen <number> Entry price (required)
4614
- --direction <string> Position direction: long or short (default: long)
4615
- --when <string> Start timestamp — ISO 8601 or Unix ms (default: now)
4616
- --minutes <string> Number of 1m candles to simulate (default: 60)
4617
- --exchange <string> Exchange name (default: first registered)
4618
- --output <string> Output file base name (default: {SYMBOL}_{DIRECTION}_{PRICEOPEN}_{TIMESTAMP})
4619
- --json Save as JSON array to ./dump/<output>.json
4620
- --jsonl Save as JSONL to ./dump/<output>.jsonl
4621
- --markdown Save as Markdown table to ./dump/<output>.md
4622
-
4623
- Module file ./modules/pnldebug.module is loaded automatically if it exists.
4624
-
4625
- Broker debug flags (--brokerdebug):
4626
-
4627
- --symbol <string> Trading pair (default: BTCUSDT)
4628
- --exchange <string> Exchange name (default: first registered)
4629
- --commit <string> Commit type to fire: signal-open, signal-close, partial-profit,
4630
- partial-loss, average-buy, trailing-stop, trailing-take, breakeven
4631
- (default: signal-open)
4632
-
4633
- Loads ./live.module, fetches the last candle for --symbol/--timeframe, and calls
4634
- the selected broker commit with synthetic payload values derived from current price.
4635
-
4636
- Flush flags (--flush):
4637
-
4638
- One or more positional entry points. For each entry point the following
4639
- subdirectories are removed from <entry-dir>/dump/:
4640
-
4641
- report log markdown agent
4642
-
4643
- Init flags (--init):
4644
-
4645
- --output <string> Target directory name (default: backtest-kit-project)
4646
-
4647
- Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
4648
-
4649
- Docker flags (--docker):
4650
-
4651
- --output <string> Target directory name (default: backtest-kit-docker)
4652
-
4653
- Scaffolds a Docker workspace: docker-compose.yaml, .env.example, package.json,
4654
- tsconfig.json, and a sample strategy under content/. Run npm install then
4655
- docker compose up to start the container.
4656
-
4657
- Module hooks (loaded automatically by each mode):
4658
-
4659
- modules/backtest.module --backtest Broker adapter for backtest
4660
- modules/walker.module --walker Broker adapter for walker comparison
4661
- modules/paper.module --paper Broker adapter for paper trading
4662
- modules/live.module --live Broker adapter for live trading
4663
- modules/main.module --main Environment setup for a custom entry point
4664
- modules/pine.module --pine Exchange schema for PineScript runs
4665
- modules/editor.module --editor Exchange schema for the visual Pine editor
4666
- modules/dump.module --dump Exchange schema for candle dumps
4667
- modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
4668
- modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
4669
-
4670
- --flush has no associated module. It only removes dump subdirectories.
4671
-
4672
- Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
4673
-
4674
- Environment variables:
4675
-
4676
- CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
4677
- CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
4678
- CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
4679
- CC_WWWROOT_PORT UI server port (default: 60050)
4680
-
4681
- Examples:
4682
-
4683
- node ${ENTRY_PATH} --backtest ./content/feb_2026.strategy.ts
4684
- node ${ENTRY_PATH} --backtest --symbol BTCUSDT --noCache --noFlush --ui ./content/feb_2026.strategy.ts
4685
- node ${ENTRY_PATH} --walker ./content/feb_2026_v1.strategy.ts ./content/feb_2026_v2.strategy.ts ./content/feb_2026_v3.strategy.ts
4686
- node ${ENTRY_PATH} --walker --symbol BTCUSDT --noCache --noFlush --markdown ./content/feb_2026_v1.ts ./content/feb_2026_v2.ts
4687
- node ${ENTRY_PATH} --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
4688
- node ${ENTRY_PATH} --live --ui --telegram ./content/feb_2026.strategy.ts
4689
- node ${ENTRY_PATH} --main ./tools/fetch_fear_and_greed.ts
4690
- node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
4691
- node ${ENTRY_PATH} --editor
4692
- node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
4693
- node ${ENTRY_PATH} --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120
4694
- node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
4695
- node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
4696
- node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
4697
- node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
4698
- node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
4699
- node ${ENTRY_PATH} --init --output my-trading-bot
4700
- node ${ENTRY_PATH} --docker --output my-docker-workspace
4610
+ const HELP_TEXT = `
4611
+ Usage:
4612
+ node index.mjs --<mode> [flags] [entry-point]
4613
+
4614
+ Modes:
4615
+
4616
+ --backtest <entry> Run strategy against historical candle data
4617
+ --walker <entry...> Run Walker A/B strategy comparison across multiple strategies
4618
+ --paper <entry> Paper trading (live prices, no real orders)
4619
+ --live <entry> Live trading with real orders
4620
+ --main <entry> Run an entry point with prepared environment, no trading harness
4621
+ --pine <entry> Execute a local .pine indicator file
4622
+ --editor Open the Pine Script visual editor in the browser
4623
+ --dump Fetch and save raw OHLCV candles
4624
+ --pnldebug Simulate PnL per minute for a given entry price and direction
4625
+ --brokerdebug Fire a single broker commit against the live broker adapter
4626
+ --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
4627
+ --init Scaffold a new project in the current directory
4628
+ --docker Scaffold a Docker workspace for running strategies in a container
4629
+ --help Print this help message
4630
+
4631
+ Backtest flags:
4632
+
4633
+ --symbol <string> Trading pair (default: BTCUSDT)
4634
+ --strategy <string> Strategy name from addStrategySchema (default: first registered)
4635
+ --exchange <string> Exchange name from addExchangeSchema (default: first registered)
4636
+ --frame <string> Frame name from addFrameSchema (default: first registered)
4637
+ --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 1h, 4h")
4638
+ --noCache Skip candle cache warming before the run
4639
+ --noFlush Skip removing report/log/markdown/agent folders before backtest run
4640
+ --verbose Log every candle fetch to stdout
4641
+ --ui Start web dashboard at http://localhost:60050
4642
+ --telegram Send trade notifications to Telegram
4643
+
4644
+ Walker flags (--walker):
4645
+
4646
+ --symbol <string> Trading pair (default: BTCUSDT)
4647
+ --cacheInterval <string> Comma-separated intervals to pre-cache (default: "1m, 15m, 30m, 1h, 4h")
4648
+ --noCache Skip candle cache warming before the run
4649
+ --noFlush Skip removing report/log/markdown/agent folders before walker run
4650
+ --verbose Log every candle fetch to stdout
4651
+ --output <string> Output file base name (default: walker_{SYMBOL}_{TIMESTAMP})
4652
+ --json Save results as JSON to ./dump/<output>.json
4653
+ --markdown Save report as Markdown to ./dump/<output>.md
4654
+
4655
+ Each positional argument is a strategy entry point. While an entry point is loaded
4656
+ (and again before its strategy runs) process.cwd() is switched to its directory and
4657
+ restored afterwards; .env is read from the launch directory first, then from the
4658
+ entry point directory (the latter wins).
4659
+ addWalkerSchema is called automatically using the registered exchange and frame.
4660
+ After comparison completes the report is printed to stdout (or saved if --json/--markdown).
4661
+
4662
+ Module file ./modules/walker.module is loaded automatically if it exists.
4663
+
4664
+ Paper / Live flags:
4665
+
4666
+ --symbol <string> Trading pair (default: BTCUSDT)
4667
+ --strategy <string> Strategy name (default: first registered)
4668
+ --exchange <string> Exchange name (default: first registered)
4669
+ --verbose Log every candle fetch to stdout
4670
+ --ui Start web dashboard
4671
+ --telegram Send Telegram notifications
4672
+
4673
+ Main flags (--main):
4674
+
4675
+ --noFlush Skip removing report/log/markdown/agent folders before the run
4676
+
4677
+ Prepares the runtime environment (loads .env, setup.config, loader.config and
4678
+ modules/main.module) and runs the single positional entry point but does NOT
4679
+ start any trading harness. Unlike --backtest/--live/--walker, the CLI never calls
4680
+ Backtest/Live/Walker.background; the entry point decides what to run.
4681
+
4682
+ Exactly one positional entry point is required. process.cwd() is changed to the
4683
+ entry point directory and its local .env is loaded.
4684
+
4685
+ Backtest, Live and Walker runs started from userspace are still tracked: the run
4686
+ finishes automatically when one of them completes, Ctrl+C stops any active run,
4687
+ and a second Ctrl+C force-quits.
4688
+
4689
+ Module file ./modules/main.module is loaded automatically if it exists.
4690
+
4691
+ PineScript flags (--pine):
4692
+
4693
+ --symbol <string> Trading pair (default: BTCUSDT)
4694
+ --timeframe <string> Candle interval (default: 15m)
4695
+ --limit <string> Number of candles to fetch (default: 250)
4696
+ --when <string> End date ISO 8601 or Unix ms (default: now)
4697
+ --exchange <string> Exchange name (default: first registered)
4698
+ --output <string> Output file base name without extension
4699
+ --json Save output as JSON array to <pine-dir>/dump/<output>.json
4700
+ --jsonl Save output as JSONL to <pine-dir>/dump/<output>.jsonl
4701
+ --markdown Save output as Markdown table to <pine-dir>/dump/<output>.md
4702
+
4703
+ Only plot() calls with display=display.data_window produce output columns.
4704
+ Module file ./modules/pine.module is loaded automatically if it exists.
4705
+
4706
+ Candle dump flags (--dump):
4707
+
4708
+ --symbol <string> Trading pair (default: BTCUSDT)
4709
+ --timeframe <string> Candle interval (default: 15m)
4710
+ --limit <string> Number of candles (default: 250)
4711
+ --when <string> End date ISO 8601 or Unix ms (default: now)
4712
+ --exchange <string> Exchange name (default: first registered)
4713
+ --output <string> Output file base name (default: {SYMBOL}_{LIMIT}_{TIMEFRAME}_{TIMESTAMP})
4714
+ --json Save as JSON array to ./dump/<output>.json
4715
+ --jsonl Save as JSONL to ./dump/<output>.jsonl
4716
+ --markdown Save as Markdown table to ./dump/<output>.md
4717
+
4718
+ Module file ./modules/dump.module is loaded automatically if it exists.
4719
+
4720
+ PnL debug flags (--pnldebug):
4721
+
4722
+ --symbol <string> Trading pair (default: BTCUSDT)
4723
+ --priceopen <number> Entry price (required)
4724
+ --direction <string> Position direction: long or short (default: long)
4725
+ --when <string> Start timestamp ISO 8601 or Unix ms (default: now)
4726
+ --minutes <string> Number of 1m candles to simulate (default: 60)
4727
+ --exchange <string> Exchange name (default: first registered)
4728
+ --output <string> Output file base name (default: {SYMBOL}_{DIRECTION}_{PRICEOPEN}_{TIMESTAMP})
4729
+ --json Save as JSON array to ./dump/<output>.json
4730
+ --jsonl Save as JSONL to ./dump/<output>.jsonl
4731
+ --markdown Save as Markdown table to ./dump/<output>.md
4732
+
4733
+ Module file ./modules/pnldebug.module is loaded automatically if it exists.
4734
+
4735
+ Broker debug flags (--brokerdebug):
4736
+
4737
+ --symbol <string> Trading pair (default: BTCUSDT)
4738
+ --exchange <string> Exchange name (default: first registered)
4739
+ --commit <string> Commit type to fire: signal-open, signal-close, partial-profit,
4740
+ partial-loss, average-buy, trailing-stop, trailing-take, breakeven
4741
+ (default: signal-open)
4742
+
4743
+ Loads ./live.module, fetches the last candle for --symbol/--timeframe, and calls
4744
+ the selected broker commit with synthetic payload values derived from current price.
4745
+
4746
+ Flush flags (--flush):
4747
+
4748
+ One or more positional entry points. For each entry point the following
4749
+ subdirectories are removed from <entry-dir>/dump/:
4750
+
4751
+ report log markdown agent
4752
+
4753
+ Init flags (--init):
4754
+
4755
+ --output <string> Target directory name (default: backtest-kit-project)
4756
+
4757
+ Scaffolds a project and runs scripts/fetch_docs.mjs to download library docs.
4758
+
4759
+ Docker flags (--docker):
4760
+
4761
+ --output <string> Target directory name (default: backtest-kit-docker)
4762
+
4763
+ Scaffolds a Docker workspace: docker-compose.yaml, .env.example, package.json,
4764
+ tsconfig.json, and a sample strategy under content/. Run npm install then
4765
+ docker compose up to start the container.
4766
+
4767
+ Module hooks (loaded automatically by each mode):
4768
+
4769
+ modules/backtest.module --backtest Broker adapter for backtest
4770
+ modules/walker.module --walker Broker adapter for walker comparison
4771
+ modules/paper.module --paper Broker adapter for paper trading
4772
+ modules/live.module --live Broker adapter for live trading
4773
+ modules/main.module --main Environment setup for a custom entry point
4774
+ modules/pine.module --pine Exchange schema for PineScript runs
4775
+ modules/editor.module --editor Exchange schema for the visual Pine editor
4776
+ modules/dump.module --dump Exchange schema for candle dumps
4777
+ modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
4778
+ modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
4779
+
4780
+ --flush has no associated module. It only removes dump subdirectories.
4781
+
4782
+ Extensions .ts, .mjs, .cjs are tried automatically. Missing module = soft warning.
4783
+
4784
+ Environment variables:
4785
+
4786
+ CC_TELEGRAM_TOKEN Telegram bot token (required for --telegram)
4787
+ CC_TELEGRAM_CHANNEL Telegram channel or chat ID (required for --telegram)
4788
+ CC_WWWROOT_HOST UI server bind address (default: 0.0.0.0)
4789
+ CC_WWWROOT_PORT UI server port (default: 60050)
4790
+
4791
+ Examples:
4792
+
4793
+ node ${ENTRY_PATH} --backtest ./content/feb_2026.strategy.ts
4794
+ node ${ENTRY_PATH} --backtest --symbol BTCUSDT --noCache --noFlush --ui ./content/feb_2026.strategy.ts
4795
+ node ${ENTRY_PATH} --walker ./content/feb_2026_v1.strategy.ts ./content/feb_2026_v2.strategy.ts ./content/feb_2026_v3.strategy.ts
4796
+ node ${ENTRY_PATH} --walker --symbol BTCUSDT --noCache --noFlush --markdown ./content/feb_2026_v1.ts ./content/feb_2026_v2.ts
4797
+ node ${ENTRY_PATH} --paper --symbol ETHUSDT ./content/feb_2026.strategy.ts
4798
+ node ${ENTRY_PATH} --live --ui --telegram ./content/feb_2026.strategy.ts
4799
+ node ${ENTRY_PATH} --main ./tools/fetch_fear_and_greed.ts
4800
+ node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
4801
+ node ${ENTRY_PATH} --editor
4802
+ node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
4803
+ node ${ENTRY_PATH} --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120
4804
+ node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
4805
+ node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
4806
+ node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
4807
+ node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
4808
+ node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
4809
+ node ${ENTRY_PATH} --init --output my-trading-bot
4810
+ node ${ENTRY_PATH} --docker --output my-docker-workspace
4701
4811
  `.trimStart();
4702
4812
  const main$1 = async () => {
4703
4813
  if (!getEntry(import.meta.url)) {
@@ -4707,7 +4817,7 @@ const main$1 = async () => {
4707
4817
  if (!values.help) {
4708
4818
  return;
4709
4819
  }
4710
- process.stdout.write(`@backtest-kit/cli ${"14.1.0"}\n\n`);
4820
+ process.stdout.write(`@backtest-kit/cli ${"15.1.0"}\n\n`);
4711
4821
  process.stdout.write(HELP_TEXT);
4712
4822
  process.exit(0);
4713
4823
  };
@@ -4721,7 +4831,7 @@ const main = async () => {
4721
4831
  if (!values.version) {
4722
4832
  return;
4723
4833
  }
4724
- process.stdout.write(`@backtest-kit/cli ${"14.1.0"}\n`);
4834
+ process.stdout.write(`@backtest-kit/cli ${"15.1.0"}\n`);
4725
4835
  process.exit(0);
4726
4836
  };
4727
4837
  main();