@backtest-kit/cli 7.4.0 โ†’ 7.6.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/README.md CHANGED
@@ -44,6 +44,7 @@ Point the CLI at your strategy file, choose a mode, and it handles exchange conn
44
44
  | **PineScript** | `--pine` | Run a local `.pine` indicator against exchange data |
45
45
  | **Pine Editor** | `--editor` | Open the visual Pine Script editor in the browser |
46
46
  | **Candle Dump** | `--dump` | Fetch and save raw OHLCV candles to a file |
47
+ | **PnL Debug** | `--pnldebug` | Simulate per-minute PnL for a given entry price and direction |
47
48
  | **Flush** | `--flush` | Delete report/log/markdown/agent folders from strategy dump dir |
48
49
  | **Init Project** | `--init` | Scaffold a new backtest-kit project |
49
50
 
@@ -1065,6 +1066,110 @@ Or add it to `package.json`:
1065
1066
  npx @backtest-kit/cli --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
1066
1067
  ```
1067
1068
 
1069
+ ## ๐Ÿž PnL Debug (`--pnldebug`)
1070
+
1071
+ `@backtest-kit/cli` can simulate a hypothetical position minute by minute and print running PnL, peak profit, and maximum drawdown for each candle โ€” without placing any trades or loading a strategy file.
1072
+
1073
+ ### CLI Flags
1074
+
1075
+ | Flag | Type | Description |
1076
+ |------|------|-------------|
1077
+ | `--pnldebug` | boolean | Enable PnL debug mode |
1078
+ | `--priceopen` | number | Entry price (required) |
1079
+ | `--direction` | string | `long` or `short` (default: `long`) |
1080
+ | `--when` | string | Start timestamp โ€” ISO 8601 or Unix ms (default: now) |
1081
+ | `--minutes` | string | Number of 1m candles to simulate (default: `60`) |
1082
+ | `--symbol` | string | Trading pair (default: `"BTCUSDT"`) |
1083
+ | `--exchange` | string | Exchange name (default: first registered, falls back to CCXT Binance) |
1084
+ | `--output` | string | Output file base name (default: `{SYMBOL}_{DIRECTION}_{PRICEOPEN}_{TIMESTAMP}`) |
1085
+ | `--json` | boolean | Save results as JSON array to `./dump/<output>.json` |
1086
+ | `--jsonl` | boolean | Save results as JSONL to `./dump/<output>.jsonl` |
1087
+ | `--markdown` | boolean | Save results as Markdown table to `./dump/<output>.md` |
1088
+
1089
+ ### Output columns
1090
+
1091
+ | Column | Description |
1092
+ |--------|-------------|
1093
+ | `min` | Minute offset from start (1-based) |
1094
+ | `timestamp` | Candle timestamp (ISO 8601) |
1095
+ | `close` | Candle close price |
1096
+ | `pnl%` | Running PnL vs entry price (signed %) |
1097
+ | `peak%` | Highest PnL reached so far (always โ‰ฅ 0) |
1098
+ | `drawdown%` | Lowest PnL reached so far (always โ‰ค 0) |
1099
+
1100
+ ### Exchange via `pnldebug.module`
1101
+
1102
+ By default the CLI registers CCXT Binance automatically. To use a different exchange, create a `modules/pnldebug.module.ts` file in the current working directory โ€” the CLI loads it automatically before fetching candles.
1103
+
1104
+ ```typescript
1105
+ // modules/pnldebug.module.ts
1106
+ import { addExchangeSchema } from "backtest-kit";
1107
+ import ccxt from "ccxt";
1108
+
1109
+ addExchangeSchema({
1110
+ exchangeName: "my-exchange",
1111
+ getCandles: async (symbol, interval, since, limit) => {
1112
+ const exchange = new ccxt.bybit({ enableRateLimit: true });
1113
+ const ohlcv = await exchange.fetchOHLCV(symbol, interval, since.getTime(), limit);
1114
+ return ohlcv.map(([timestamp, open, high, low, close, volume]) => ({
1115
+ timestamp, open, high, low, close, volume,
1116
+ }));
1117
+ },
1118
+ formatPrice: (symbol, price) => price.toFixed(2),
1119
+ formatQuantity: (symbol, quantity) => quantity.toFixed(8),
1120
+ });
1121
+ ```
1122
+
1123
+ ### Usage
1124
+
1125
+ Print to stdout (default table format):
1126
+
1127
+ ```bash
1128
+ npx @backtest-kit/cli --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120
1129
+ ```
1130
+
1131
+ Save as Markdown:
1132
+
1133
+ ```bash
1134
+ npx @backtest-kit/cli --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
1135
+ # โ†’ ./dump/BTCUSDT_long_67956.73_{timestamp}.md
1136
+ ```
1137
+
1138
+ Override the output file name with `--output`:
1139
+
1140
+ ```bash
1141
+ npx @backtest-kit/cli --pnldebug --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120 \
1142
+ --jsonl --output feb25_short_debug
1143
+ # โ†’ ./dump/feb25_short_debug.jsonl
1144
+ ```
1145
+
1146
+ Or add it to `package.json`:
1147
+
1148
+ ```json
1149
+ {
1150
+ "scripts": {
1151
+ "pnldebug": "npx @backtest-kit/cli --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when \"2025-02-25\" --minutes 120"
1152
+ }
1153
+ }
1154
+ ```
1155
+
1156
+ ```bash
1157
+ npm run pnldebug
1158
+ ```
1159
+
1160
+ ### Example stdout output
1161
+
1162
+ ```
1163
+ Symbol: BTCUSDT | Direction: short | PriceOpen: 64069.50 | From: 2025-02-25T00:00:00.000Z | Minutes: 120
1164
+
1165
+ min | timestamp | close | pnl% | peak% | drawdown%
1166
+ -----------------------------------------------------------------------------------
1167
+ 1 | 2025-02-25T00:01:00.000Z | 64020.10 | +0.08% | +0.08% | 0.00%
1168
+ 2 | 2025-02-25T00:02:00.000Z | 64105.30 | -0.06% | +0.08% | -0.06%
1169
+ ...
1170
+ 120 | 2025-02-25T02:00:00.000Z | 63200.00 | +1.36% | +1.36% | -0.06%
1171
+ ```
1172
+
1068
1173
  ## ๐Ÿ—‘๏ธ Flushing Strategy Output (`--flush`)
1069
1174
 
1070
1175
  `@backtest-kit/cli` can delete generated output folders from one or more strategy dump directories without touching cached candle data.
package/build/index.cjs CHANGED
@@ -570,6 +570,22 @@ const getArgs = functoolsKit.singleshot(() => {
570
570
  type: "boolean",
571
571
  default: false,
572
572
  },
573
+ pnldebug: {
574
+ type: "boolean",
575
+ default: false,
576
+ },
577
+ priceopen: {
578
+ type: "string",
579
+ default: "",
580
+ },
581
+ direction: {
582
+ type: "string",
583
+ default: "",
584
+ },
585
+ minutes: {
586
+ type: "string",
587
+ default: "",
588
+ },
573
589
  init: {
574
590
  type: "boolean",
575
591
  default: false,
@@ -716,11 +732,15 @@ class SetupUtils {
716
732
  BacktestKit.Markdown.enable();
717
733
  BacktestKit.Report.enable();
718
734
  BacktestKit.Dump.enable();
735
+ BacktestKit.State.enable();
719
736
  BacktestKit.Memory.enable();
720
737
  }
721
738
  {
722
739
  BacktestKit.Dump.useMarkdown();
723
- BacktestKit.Memory.usePersist();
740
+ }
741
+ {
742
+ BacktestKit.SessionLive.usePersist();
743
+ BacktestKit.SessionBacktest.useLocal();
724
744
  }
725
745
  {
726
746
  BacktestKit.StorageLive.usePersist();
@@ -734,6 +754,18 @@ class SetupUtils {
734
754
  BacktestKit.NotificationLive.usePersist();
735
755
  BacktestKit.NotificationBacktest.useMemory();
736
756
  }
757
+ {
758
+ BacktestKit.RecentLive.usePersist();
759
+ BacktestKit.RecentBacktest.useMemory();
760
+ }
761
+ {
762
+ BacktestKit.MemoryLive.usePersist();
763
+ BacktestKit.MemoryBacktest.useLocal();
764
+ }
765
+ {
766
+ BacktestKit.StateLive.usePersist();
767
+ BacktestKit.StateBacktest.useLocal();
768
+ }
737
769
  {
738
770
  BacktestKit.Markdown.useDummy();
739
771
  BacktestKit.Log.useJsonl();
@@ -776,6 +808,8 @@ class SetupUtils {
776
808
  BacktestKit.PersistIntervalAdapter.clear();
777
809
  BacktestKit.PersistMemoryAdapter.clear();
778
810
  BacktestKit.PersistRecentAdapter.clear();
811
+ BacktestKit.PersistStateAdapter.clear();
812
+ BacktestKit.PersistSessionAdapter.clear();
779
813
  }
780
814
  {
781
815
  BacktestKit.Dump.clear();
@@ -967,7 +1001,7 @@ class WalkerMainService {
967
1001
  }
968
1002
  strategyMap.set(strategyName, entryPoint);
969
1003
  }
970
- sessionMap.set(entryPoint, BacktestKit.Session.createSnapshot());
1004
+ sessionMap.set(entryPoint, BacktestKit.System.createSnapshot());
971
1005
  BacktestKit.Cache.resetCounter();
972
1006
  BacktestKit.Interval.resetCounter();
973
1007
  }
@@ -2965,14 +2999,14 @@ const cli = {
2965
2999
  };
2966
3000
  init();
2967
3001
 
2968
- const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "flush", "init", "help", "version"];
3002
+ const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "pnldebug", "flush", "init", "help", "version"];
2969
3003
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
2970
3004
  const HELP_TEXT$1 = `
2971
3005
  Example:
2972
3006
 
2973
3007
  node ${ENTRY_PATH$1} --help
2974
3008
  `.trimStart();
2975
- const main$d = async () => {
3009
+ const main$e = async () => {
2976
3010
  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)))) {
2977
3011
  return;
2978
3012
  }
@@ -2980,14 +3014,14 @@ const main$d = async () => {
2980
3014
  if (MODES.some((mode) => values[mode])) {
2981
3015
  return;
2982
3016
  }
2983
- process.stdout.write(`@backtest-kit/cli ${"7.4.0"}\n`);
3017
+ process.stdout.write(`@backtest-kit/cli ${"7.6.0"}\n`);
2984
3018
  process.stdout.write("\n");
2985
3019
  process.stdout.write(`Run with --help to see available commands.\n`);
2986
3020
  process.stdout.write("\n");
2987
3021
  process.stdout.write(HELP_TEXT$1);
2988
3022
  process.exit(0);
2989
3023
  };
2990
- main$d();
3024
+ main$e();
2991
3025
 
2992
3026
  const notifyShutdown = functoolsKit.singleshot(async () => {
2993
3027
  console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
@@ -3003,7 +3037,7 @@ const flush = async (entryPoint) => {
3003
3037
  console.log(`Removed: ${target}`);
3004
3038
  }
3005
3039
  };
3006
- const main$c = async () => {
3040
+ const main$d = async () => {
3007
3041
  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)))) {
3008
3042
  return;
3009
3043
  }
@@ -3020,7 +3054,7 @@ const main$c = async () => {
3020
3054
  }
3021
3055
  process.exit(0);
3022
3056
  };
3023
- main$c();
3057
+ main$d();
3024
3058
 
3025
3059
  const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3026
3060
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3042,7 +3076,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3042
3076
  const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
3043
3077
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3044
3078
  });
3045
- const main$b = async () => {
3079
+ const main$c = async () => {
3046
3080
  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)))) {
3047
3081
  return;
3048
3082
  }
@@ -3057,7 +3091,7 @@ const main$b = async () => {
3057
3091
  await cli.backtestMainService.connect();
3058
3092
  listenGracefulShutdown$5();
3059
3093
  };
3060
- main$b();
3094
+ main$c();
3061
3095
 
3062
3096
  const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3063
3097
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3075,7 +3109,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3075
3109
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
3076
3110
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3077
3111
  });
3078
- const main$a = async () => {
3112
+ const main$b = async () => {
3079
3113
  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)))) {
3080
3114
  return;
3081
3115
  }
@@ -3091,7 +3125,7 @@ const main$a = async () => {
3091
3125
  listenGracefulShutdown$4();
3092
3126
  await cli.walkerMainService.connect();
3093
3127
  };
3094
- main$a();
3128
+ main$b();
3095
3129
 
3096
3130
  const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3097
3131
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3112,7 +3146,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3112
3146
  const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
3113
3147
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3114
3148
  });
3115
- const main$9 = async () => {
3149
+ const main$a = async () => {
3116
3150
  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)))) {
3117
3151
  return;
3118
3152
  }
@@ -3123,7 +3157,7 @@ const main$9 = async () => {
3123
3157
  cli.paperMainService.connect();
3124
3158
  listenGracefulShutdown$3();
3125
3159
  };
3126
- main$9();
3160
+ main$a();
3127
3161
 
3128
3162
  const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
3129
3163
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -3144,7 +3178,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
3144
3178
  const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
3145
3179
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3146
3180
  });
3147
- const main$8 = async () => {
3181
+ const main$9 = async () => {
3148
3182
  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)))) {
3149
3183
  return;
3150
3184
  }
@@ -3155,7 +3189,7 @@ const main$8 = async () => {
3155
3189
  await cli.liveMainService.connect();
3156
3190
  listenGracefulShutdown$2();
3157
3191
  };
3158
- main$8();
3192
+ main$9();
3159
3193
 
3160
3194
  const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
3161
3195
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -3165,7 +3199,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
3165
3199
  const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
3166
3200
  process.on("SIGINT", BEFORE_EXIT_FN$1);
3167
3201
  });
3168
- const main$7 = async () => {
3202
+ const main$8 = async () => {
3169
3203
  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)))) {
3170
3204
  return;
3171
3205
  }
@@ -3175,7 +3209,7 @@ const main$7 = async () => {
3175
3209
  }
3176
3210
  listenGracefulShutdown$1();
3177
3211
  };
3178
- main$7();
3212
+ main$8();
3179
3213
 
3180
3214
  const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
3181
3215
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -3185,7 +3219,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
3185
3219
  const listenGracefulShutdown = functoolsKit.singleshot(() => {
3186
3220
  process.on("SIGINT", BEFORE_EXIT_FN);
3187
3221
  });
3188
- const main$6 = async () => {
3222
+ const main$7 = async () => {
3189
3223
  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)))) {
3190
3224
  return;
3191
3225
  }
@@ -3195,7 +3229,7 @@ const main$6 = async () => {
3195
3229
  }
3196
3230
  listenGracefulShutdown();
3197
3231
  };
3198
- main$6();
3232
+ main$7();
3199
3233
 
3200
3234
  const EXTRACT_ROWS_FN = (plots, schema) => {
3201
3235
  const keys = Object.keys(schema);
@@ -3217,7 +3251,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
3217
3251
  }
3218
3252
  return rows;
3219
3253
  };
3220
- const main$5 = async () => {
3254
+ const main$6 = async () => {
3221
3255
  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)))) {
3222
3256
  return;
3223
3257
  }
@@ -3297,9 +3331,9 @@ const main$5 = async () => {
3297
3331
  console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
3298
3332
  process.exit(0);
3299
3333
  };
3300
- main$5();
3334
+ main$6();
3301
3335
 
3302
- const main$4 = async () => {
3336
+ const main$5 = async () => {
3303
3337
  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)))) {
3304
3338
  return;
3305
3339
  }
@@ -3334,9 +3368,9 @@ const main$4 = async () => {
3334
3368
  };
3335
3369
  process.on("SIGINT", beforeExit);
3336
3370
  };
3337
- main$4();
3371
+ main$5();
3338
3372
 
3339
- const main$3 = async () => {
3373
+ const main$4 = async () => {
3340
3374
  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)))) {
3341
3375
  return;
3342
3376
  }
@@ -3397,6 +3431,102 @@ const main$3 = async () => {
3397
3431
  console.log(JSON.stringify(candles, null, 2));
3398
3432
  process.exit(0);
3399
3433
  };
3434
+ main$4();
3435
+
3436
+ const main$3 = async () => {
3437
+ 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)))) {
3438
+ return;
3439
+ }
3440
+ const { values } = getArgs();
3441
+ if (!values.pnldebug) {
3442
+ return;
3443
+ }
3444
+ await cli.moduleConnectionService.loadModule("./pnldebug.module");
3445
+ {
3446
+ await cli.exchangeSchemaService.addSchema();
3447
+ await cli.symbolSchemaService.addSchema();
3448
+ }
3449
+ const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
3450
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
3451
+ const symbol = values.symbol || "BTCUSDT";
3452
+ const priceOpenStr = values.priceopen;
3453
+ if (!priceOpenStr) {
3454
+ console.error("Error: --priceopen is required");
3455
+ process.exit(1);
3456
+ }
3457
+ const priceOpen = parseFloat(priceOpenStr);
3458
+ if (isNaN(priceOpen)) {
3459
+ console.error(`Error: --priceopen must be a number, got: ${priceOpenStr}`);
3460
+ process.exit(1);
3461
+ }
3462
+ const direction = (values.direction || "long").toLowerCase();
3463
+ if (direction !== "long" && direction !== "short") {
3464
+ console.error(`Error: --direction must be 'long' or 'short', got: ${direction}`);
3465
+ process.exit(1);
3466
+ }
3467
+ const whenStr = values.when || Date.now().toString();
3468
+ const whenStamp = Date.parse(whenStr);
3469
+ const when = isNaN(whenStamp) ? new Date() : new Date(whenStamp);
3470
+ const timestamp = BacktestKit.alignToInterval(when, "1m").getTime();
3471
+ const minutesStr = values.minutes || "60";
3472
+ const minutesNum = parseInt(minutesStr);
3473
+ const minutes = isNaN(minutesNum) ? 60 : minutesNum;
3474
+ const candles = await BacktestKit.Exchange.getRawCandles(symbol, "1m", { exchangeName }, minutes, undefined, timestamp);
3475
+ if (candles.length === 0) {
3476
+ console.error("Error: no candles returned for the given parameters");
3477
+ process.exit(1);
3478
+ }
3479
+ let peak = 0;
3480
+ let drawdown = 0;
3481
+ const rows = candles.map((c, i) => {
3482
+ const pnl = direction === "short"
3483
+ ? (priceOpen - c.close) / priceOpen * 100
3484
+ : (c.close - priceOpen) / priceOpen * 100;
3485
+ if (pnl > peak)
3486
+ peak = pnl;
3487
+ if (pnl < drawdown)
3488
+ drawdown = pnl;
3489
+ return { min: i + 1, timestamp: c.timestamp, close: c.close, pnl, peak, drawdown };
3490
+ });
3491
+ const dumpName = values.output || `${symbol}_${direction}_${priceOpen}_${timestamp}`;
3492
+ const dumpDir = path.join(process.cwd(), "dump");
3493
+ if (values.json) {
3494
+ const filePath = path.resolve(dumpDir, `${dumpName}.json`);
3495
+ await fs$1.mkdir(dumpDir, { recursive: true });
3496
+ await fs$1.writeFile(filePath, JSON.stringify(rows, null, 2), "utf-8");
3497
+ console.log(`Saved: ${filePath}`);
3498
+ process.exit(0);
3499
+ }
3500
+ if (values.jsonl) {
3501
+ const filePath = path.resolve(dumpDir, `${dumpName}.jsonl`);
3502
+ await fs$1.mkdir(dumpDir, { recursive: true });
3503
+ await fs$1.writeFile(filePath, rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
3504
+ console.log(`Saved: ${filePath}`);
3505
+ process.exit(0);
3506
+ }
3507
+ if (values.markdown) {
3508
+ const header = `| min | timestamp | close | pnl% | peak% | drawdown% |\n| --- | --- | --- | --- | --- | --- |`;
3509
+ const mdRows = rows.map((r) => `| ${r.min} | ${new Date(r.timestamp).toISOString()} | ${r.close.toFixed(2)} | ${(r.pnl >= 0 ? "+" : "") + r.pnl.toFixed(2)}% | +${r.peak.toFixed(2)}% | ${r.drawdown.toFixed(2)}% |`);
3510
+ const filePath = path.resolve(dumpDir, `${dumpName}.md`);
3511
+ await fs$1.mkdir(dumpDir, { recursive: true });
3512
+ await fs$1.writeFile(filePath, [header, ...mdRows].join("\n"), "utf-8");
3513
+ console.log(`Saved: ${filePath}`);
3514
+ process.exit(0);
3515
+ }
3516
+ console.log(`Symbol: ${symbol} | Direction: ${direction} | PriceOpen: ${priceOpen} | From: ${new Date(timestamp).toISOString()} | Minutes: ${minutes}\n`);
3517
+ console.log(`${"min".padStart(5)} | ${"timestamp".padEnd(24)} | ${"close".padStart(12)} | ${"pnl%".padStart(8)} | ${"peak%".padStart(8)} | ${"drawdown%".padStart(10)}`);
3518
+ console.log("-".repeat(83));
3519
+ for (const r of rows) {
3520
+ const min = String(r.min).padStart(5);
3521
+ const ts = new Date(r.timestamp).toISOString().padEnd(24);
3522
+ const close = r.close.toFixed(2).padStart(12);
3523
+ const pnlStr = (r.pnl >= 0 ? "+" : "") + r.pnl.toFixed(2) + "%";
3524
+ const peakStr = "+" + r.peak.toFixed(2) + "%";
3525
+ const drawdownStr = r.drawdown.toFixed(2) + "%";
3526
+ console.log(`${min} | ${ts} | ${close} | ${pnlStr.padStart(8)} | ${peakStr.padStart(8)} | ${drawdownStr.padStart(10)}`);
3527
+ }
3528
+ process.exit(0);
3529
+ };
3400
3530
  main$3();
3401
3531
 
3402
3532
  const __filename$1 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
@@ -3515,6 +3645,7 @@ Modes:
3515
3645
  --pine <entry> Execute a local .pine indicator file
3516
3646
  --editor Open the Pine Script visual editor in the browser
3517
3647
  --dump Fetch and save raw OHLCV candles
3648
+ --pnldebug Simulate PnL per minute for a given entry price and direction
3518
3649
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
3519
3650
  --init Scaffold a new project in the current directory
3520
3651
  --help Print this help message
@@ -3587,6 +3718,21 @@ Candle dump flags (--dump):
3587
3718
 
3588
3719
  Module file ./modules/dump.module is loaded automatically if it exists.
3589
3720
 
3721
+ PnL debug flags (--pnldebug):
3722
+
3723
+ --symbol <string> Trading pair (default: BTCUSDT)
3724
+ --priceopen <number> Entry price (required)
3725
+ --direction <string> Position direction: long or short (default: long)
3726
+ --when <string> Start timestamp โ€” ISO 8601 or Unix ms (default: now)
3727
+ --minutes <string> Number of 1m candles to simulate (default: 60)
3728
+ --exchange <string> Exchange name (default: first registered)
3729
+ --output <string> Output file base name (default: {SYMBOL}_{DIRECTION}_{PRICEOPEN}_{TIMESTAMP})
3730
+ --json Save as JSON array to ./dump/<output>.json
3731
+ --jsonl Save as JSONL to ./dump/<output>.jsonl
3732
+ --markdown Save as Markdown table to ./dump/<output>.md
3733
+
3734
+ Module file ./modules/pnldebug.module is loaded automatically if it exists.
3735
+
3590
3736
  Flush flags (--flush):
3591
3737
 
3592
3738
  One or more positional entry points. For each entry point the following
@@ -3609,6 +3755,7 @@ Module hooks (loaded automatically by each mode):
3609
3755
  modules/pine.module --pine Exchange schema for PineScript runs
3610
3756
  modules/editor.module --editor Exchange schema for the visual Pine editor
3611
3757
  modules/dump.module --dump Exchange schema for candle dumps
3758
+ modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
3612
3759
 
3613
3760
  --flush has no associated module. It only removes dump subdirectories.
3614
3761
 
@@ -3632,6 +3779,8 @@ Examples:
3632
3779
  node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
3633
3780
  node ${ENTRY_PATH} --editor
3634
3781
  node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
3782
+ node ${ENTRY_PATH} --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120
3783
+ node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
3635
3784
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
3636
3785
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
3637
3786
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -3644,7 +3793,7 @@ const main$1 = async () => {
3644
3793
  if (!values.help) {
3645
3794
  return;
3646
3795
  }
3647
- process.stdout.write(`@backtest-kit/cli ${"7.4.0"}\n\n`);
3796
+ process.stdout.write(`@backtest-kit/cli ${"7.6.0"}\n\n`);
3648
3797
  process.stdout.write(HELP_TEXT);
3649
3798
  process.exit(0);
3650
3799
  };
@@ -3658,7 +3807,7 @@ const main = async () => {
3658
3807
  if (!values.version) {
3659
3808
  return;
3660
3809
  }
3661
- process.stdout.write(`@backtest-kit/cli ${"7.4.0"}\n`);
3810
+ process.stdout.write(`@backtest-kit/cli ${"7.6.0"}\n`);
3662
3811
  process.exit(0);
3663
3812
  };
3664
3813
  main();
package/build/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import * as BacktestKit from 'backtest-kit';
3
- import { setConfig, Log, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, Notification, Recent, Storage, Markdown, Report, Dump, Memory, StorageLive, StorageBacktest, RecentLive, RecentBacktest, NotificationLive, NotificationBacktest, MarkdownWriter, ReportWriter, PersistSignalAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistPartialAdapter, PersistBreakevenAdapter, PersistCandleAdapter, PersistStorageAdapter, PersistNotificationAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistIntervalAdapter, PersistMemoryAdapter, PersistRecentAdapter, listStrategySchema, overrideExchangeSchema, Backtest, Session, Cache, Interval, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenSync, listenSignalNotify, Exchange } from 'backtest-kit';
3
+ import { setConfig, Log, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, Notification, Recent, Storage, Markdown, Report, Dump, State, Memory, SessionLive, SessionBacktest, StorageLive, StorageBacktest, RecentLive, RecentBacktest, NotificationLive, NotificationBacktest, MemoryLive, MemoryBacktest, StateLive, StateBacktest, MarkdownWriter, ReportWriter, PersistSignalAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistPartialAdapter, PersistBreakevenAdapter, PersistCandleAdapter, PersistStorageAdapter, PersistNotificationAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistIntervalAdapter, PersistMemoryAdapter, PersistRecentAdapter, PersistStateAdapter, PersistSessionAdapter, listStrategySchema, overrideExchangeSchema, Backtest, System, Cache, Interval, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenSync, listenSignalNotify, Exchange } from 'backtest-kit';
4
4
  import { getErrorMessage, errorData, singleshot, str, BehaviorSubject, compose, createAwaiter, execpool, queued, sleep, randomString, TIMEOUT_SYMBOL, typo, retry, trycatch, memoize, isObject } from 'functools-kit';
5
5
  import fs, { constants } from 'fs';
6
6
  import * as stackTrace from 'stack-trace';
@@ -545,6 +545,22 @@ const getArgs = singleshot(() => {
545
545
  type: "boolean",
546
546
  default: false,
547
547
  },
548
+ pnldebug: {
549
+ type: "boolean",
550
+ default: false,
551
+ },
552
+ priceopen: {
553
+ type: "string",
554
+ default: "",
555
+ },
556
+ direction: {
557
+ type: "string",
558
+ default: "",
559
+ },
560
+ minutes: {
561
+ type: "string",
562
+ default: "",
563
+ },
548
564
  init: {
549
565
  type: "boolean",
550
566
  default: false,
@@ -691,11 +707,15 @@ class SetupUtils {
691
707
  Markdown.enable();
692
708
  Report.enable();
693
709
  Dump.enable();
710
+ State.enable();
694
711
  Memory.enable();
695
712
  }
696
713
  {
697
714
  Dump.useMarkdown();
698
- Memory.usePersist();
715
+ }
716
+ {
717
+ SessionLive.usePersist();
718
+ SessionBacktest.useLocal();
699
719
  }
700
720
  {
701
721
  StorageLive.usePersist();
@@ -709,6 +729,18 @@ class SetupUtils {
709
729
  NotificationLive.usePersist();
710
730
  NotificationBacktest.useMemory();
711
731
  }
732
+ {
733
+ RecentLive.usePersist();
734
+ RecentBacktest.useMemory();
735
+ }
736
+ {
737
+ MemoryLive.usePersist();
738
+ MemoryBacktest.useLocal();
739
+ }
740
+ {
741
+ StateLive.usePersist();
742
+ StateBacktest.useLocal();
743
+ }
712
744
  {
713
745
  Markdown.useDummy();
714
746
  Log.useJsonl();
@@ -751,6 +783,8 @@ class SetupUtils {
751
783
  PersistIntervalAdapter.clear();
752
784
  PersistMemoryAdapter.clear();
753
785
  PersistRecentAdapter.clear();
786
+ PersistStateAdapter.clear();
787
+ PersistSessionAdapter.clear();
754
788
  }
755
789
  {
756
790
  Dump.clear();
@@ -942,7 +976,7 @@ class WalkerMainService {
942
976
  }
943
977
  strategyMap.set(strategyName, entryPoint);
944
978
  }
945
- sessionMap.set(entryPoint, Session.createSnapshot());
979
+ sessionMap.set(entryPoint, System.createSnapshot());
946
980
  Cache.resetCounter();
947
981
  Interval.resetCounter();
948
982
  }
@@ -2936,14 +2970,14 @@ const cli = {
2936
2970
  };
2937
2971
  init();
2938
2972
 
2939
- const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "flush", "init", "help", "version"];
2973
+ const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "pnldebug", "flush", "init", "help", "version"];
2940
2974
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
2941
2975
  const HELP_TEXT$1 = `
2942
2976
  Example:
2943
2977
 
2944
2978
  node ${ENTRY_PATH$1} --help
2945
2979
  `.trimStart();
2946
- const main$d = async () => {
2980
+ const main$e = async () => {
2947
2981
  if (!getEntry(import.meta.url)) {
2948
2982
  return;
2949
2983
  }
@@ -2951,14 +2985,14 @@ const main$d = async () => {
2951
2985
  if (MODES.some((mode) => values[mode])) {
2952
2986
  return;
2953
2987
  }
2954
- process.stdout.write(`@backtest-kit/cli ${"7.4.0"}\n`);
2988
+ process.stdout.write(`@backtest-kit/cli ${"7.6.0"}\n`);
2955
2989
  process.stdout.write("\n");
2956
2990
  process.stdout.write(`Run with --help to see available commands.\n`);
2957
2991
  process.stdout.write("\n");
2958
2992
  process.stdout.write(HELP_TEXT$1);
2959
2993
  process.exit(0);
2960
2994
  };
2961
- main$d();
2995
+ main$e();
2962
2996
 
2963
2997
  const notifyShutdown = singleshot(async () => {
2964
2998
  console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
@@ -2974,7 +3008,7 @@ const flush = async (entryPoint) => {
2974
3008
  console.log(`Removed: ${target}`);
2975
3009
  }
2976
3010
  };
2977
- const main$c = async () => {
3011
+ const main$d = async () => {
2978
3012
  if (!getEntry(import.meta.url)) {
2979
3013
  return;
2980
3014
  }
@@ -2991,7 +3025,7 @@ const main$c = async () => {
2991
3025
  }
2992
3026
  process.exit(0);
2993
3027
  };
2994
- main$c();
3028
+ main$d();
2995
3029
 
2996
3030
  const BEFORE_EXIT_FN$5 = singleshot(async () => {
2997
3031
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3013,7 +3047,7 @@ const BEFORE_EXIT_FN$5 = singleshot(async () => {
3013
3047
  const listenGracefulShutdown$5 = singleshot(() => {
3014
3048
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3015
3049
  });
3016
- const main$b = async () => {
3050
+ const main$c = async () => {
3017
3051
  if (!getEntry(import.meta.url)) {
3018
3052
  return;
3019
3053
  }
@@ -3028,7 +3062,7 @@ const main$b = async () => {
3028
3062
  await cli.backtestMainService.connect();
3029
3063
  listenGracefulShutdown$5();
3030
3064
  };
3031
- main$b();
3065
+ main$c();
3032
3066
 
3033
3067
  const BEFORE_EXIT_FN$4 = singleshot(async () => {
3034
3068
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3046,7 +3080,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
3046
3080
  const listenGracefulShutdown$4 = singleshot(() => {
3047
3081
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3048
3082
  });
3049
- const main$a = async () => {
3083
+ const main$b = async () => {
3050
3084
  if (!getEntry(import.meta.url)) {
3051
3085
  return;
3052
3086
  }
@@ -3062,7 +3096,7 @@ const main$a = async () => {
3062
3096
  listenGracefulShutdown$4();
3063
3097
  await cli.walkerMainService.connect();
3064
3098
  };
3065
- main$a();
3099
+ main$b();
3066
3100
 
3067
3101
  const BEFORE_EXIT_FN$3 = singleshot(async () => {
3068
3102
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3083,7 +3117,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
3083
3117
  const listenGracefulShutdown$3 = singleshot(() => {
3084
3118
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3085
3119
  });
3086
- const main$9 = async () => {
3120
+ const main$a = async () => {
3087
3121
  if (!getEntry(import.meta.url)) {
3088
3122
  return;
3089
3123
  }
@@ -3094,7 +3128,7 @@ const main$9 = async () => {
3094
3128
  cli.paperMainService.connect();
3095
3129
  listenGracefulShutdown$3();
3096
3130
  };
3097
- main$9();
3131
+ main$a();
3098
3132
 
3099
3133
  const BEFORE_EXIT_FN$2 = singleshot(async () => {
3100
3134
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -3115,7 +3149,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
3115
3149
  const listenGracefulShutdown$2 = singleshot(() => {
3116
3150
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3117
3151
  });
3118
- const main$8 = async () => {
3152
+ const main$9 = async () => {
3119
3153
  if (!getEntry(import.meta.url)) {
3120
3154
  return;
3121
3155
  }
@@ -3126,7 +3160,7 @@ const main$8 = async () => {
3126
3160
  await cli.liveMainService.connect();
3127
3161
  listenGracefulShutdown$2();
3128
3162
  };
3129
- main$8();
3163
+ main$9();
3130
3164
 
3131
3165
  const BEFORE_EXIT_FN$1 = singleshot(async () => {
3132
3166
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -3136,7 +3170,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
3136
3170
  const listenGracefulShutdown$1 = singleshot(() => {
3137
3171
  process.on("SIGINT", BEFORE_EXIT_FN$1);
3138
3172
  });
3139
- const main$7 = async () => {
3173
+ const main$8 = async () => {
3140
3174
  if (!getEntry(import.meta.url)) {
3141
3175
  return;
3142
3176
  }
@@ -3146,7 +3180,7 @@ const main$7 = async () => {
3146
3180
  }
3147
3181
  listenGracefulShutdown$1();
3148
3182
  };
3149
- main$7();
3183
+ main$8();
3150
3184
 
3151
3185
  const BEFORE_EXIT_FN = singleshot(async () => {
3152
3186
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -3156,7 +3190,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
3156
3190
  const listenGracefulShutdown = singleshot(() => {
3157
3191
  process.on("SIGINT", BEFORE_EXIT_FN);
3158
3192
  });
3159
- const main$6 = async () => {
3193
+ const main$7 = async () => {
3160
3194
  if (!getEntry(import.meta.url)) {
3161
3195
  return;
3162
3196
  }
@@ -3166,7 +3200,7 @@ const main$6 = async () => {
3166
3200
  }
3167
3201
  listenGracefulShutdown();
3168
3202
  };
3169
- main$6();
3203
+ main$7();
3170
3204
 
3171
3205
  const EXTRACT_ROWS_FN = (plots, schema) => {
3172
3206
  const keys = Object.keys(schema);
@@ -3188,7 +3222,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
3188
3222
  }
3189
3223
  return rows;
3190
3224
  };
3191
- const main$5 = async () => {
3225
+ const main$6 = async () => {
3192
3226
  if (!getEntry(import.meta.url)) {
3193
3227
  return;
3194
3228
  }
@@ -3268,9 +3302,9 @@ const main$5 = async () => {
3268
3302
  console.log(await toMarkdown(signalId, plots, signalSchema));
3269
3303
  process.exit(0);
3270
3304
  };
3271
- main$5();
3305
+ main$6();
3272
3306
 
3273
- const main$4 = async () => {
3307
+ const main$5 = async () => {
3274
3308
  if (!getEntry(import.meta.url)) {
3275
3309
  return;
3276
3310
  }
@@ -3305,9 +3339,9 @@ const main$4 = async () => {
3305
3339
  };
3306
3340
  process.on("SIGINT", beforeExit);
3307
3341
  };
3308
- main$4();
3342
+ main$5();
3309
3343
 
3310
- const main$3 = async () => {
3344
+ const main$4 = async () => {
3311
3345
  if (!getEntry(import.meta.url)) {
3312
3346
  return;
3313
3347
  }
@@ -3368,6 +3402,102 @@ const main$3 = async () => {
3368
3402
  console.log(JSON.stringify(candles, null, 2));
3369
3403
  process.exit(0);
3370
3404
  };
3405
+ main$4();
3406
+
3407
+ const main$3 = async () => {
3408
+ if (!getEntry(import.meta.url)) {
3409
+ return;
3410
+ }
3411
+ const { values } = getArgs();
3412
+ if (!values.pnldebug) {
3413
+ return;
3414
+ }
3415
+ await cli.moduleConnectionService.loadModule("./pnldebug.module");
3416
+ {
3417
+ await cli.exchangeSchemaService.addSchema();
3418
+ await cli.symbolSchemaService.addSchema();
3419
+ }
3420
+ const [defaultExchangeName = null] = await listExchangeSchema();
3421
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
3422
+ const symbol = values.symbol || "BTCUSDT";
3423
+ const priceOpenStr = values.priceopen;
3424
+ if (!priceOpenStr) {
3425
+ console.error("Error: --priceopen is required");
3426
+ process.exit(1);
3427
+ }
3428
+ const priceOpen = parseFloat(priceOpenStr);
3429
+ if (isNaN(priceOpen)) {
3430
+ console.error(`Error: --priceopen must be a number, got: ${priceOpenStr}`);
3431
+ process.exit(1);
3432
+ }
3433
+ const direction = (values.direction || "long").toLowerCase();
3434
+ if (direction !== "long" && direction !== "short") {
3435
+ console.error(`Error: --direction must be 'long' or 'short', got: ${direction}`);
3436
+ process.exit(1);
3437
+ }
3438
+ const whenStr = values.when || Date.now().toString();
3439
+ const whenStamp = Date.parse(whenStr);
3440
+ const when = isNaN(whenStamp) ? new Date() : new Date(whenStamp);
3441
+ const timestamp = alignToInterval(when, "1m").getTime();
3442
+ const minutesStr = values.minutes || "60";
3443
+ const minutesNum = parseInt(minutesStr);
3444
+ const minutes = isNaN(minutesNum) ? 60 : minutesNum;
3445
+ const candles = await Exchange.getRawCandles(symbol, "1m", { exchangeName }, minutes, undefined, timestamp);
3446
+ if (candles.length === 0) {
3447
+ console.error("Error: no candles returned for the given parameters");
3448
+ process.exit(1);
3449
+ }
3450
+ let peak = 0;
3451
+ let drawdown = 0;
3452
+ const rows = candles.map((c, i) => {
3453
+ const pnl = direction === "short"
3454
+ ? (priceOpen - c.close) / priceOpen * 100
3455
+ : (c.close - priceOpen) / priceOpen * 100;
3456
+ if (pnl > peak)
3457
+ peak = pnl;
3458
+ if (pnl < drawdown)
3459
+ drawdown = pnl;
3460
+ return { min: i + 1, timestamp: c.timestamp, close: c.close, pnl, peak, drawdown };
3461
+ });
3462
+ const dumpName = values.output || `${symbol}_${direction}_${priceOpen}_${timestamp}`;
3463
+ const dumpDir = join(process.cwd(), "dump");
3464
+ if (values.json) {
3465
+ const filePath = resolve(dumpDir, `${dumpName}.json`);
3466
+ await mkdir(dumpDir, { recursive: true });
3467
+ await writeFile(filePath, JSON.stringify(rows, null, 2), "utf-8");
3468
+ console.log(`Saved: ${filePath}`);
3469
+ process.exit(0);
3470
+ }
3471
+ if (values.jsonl) {
3472
+ const filePath = resolve(dumpDir, `${dumpName}.jsonl`);
3473
+ await mkdir(dumpDir, { recursive: true });
3474
+ await writeFile(filePath, rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
3475
+ console.log(`Saved: ${filePath}`);
3476
+ process.exit(0);
3477
+ }
3478
+ if (values.markdown) {
3479
+ const header = `| min | timestamp | close | pnl% | peak% | drawdown% |\n| --- | --- | --- | --- | --- | --- |`;
3480
+ const mdRows = rows.map((r) => `| ${r.min} | ${new Date(r.timestamp).toISOString()} | ${r.close.toFixed(2)} | ${(r.pnl >= 0 ? "+" : "") + r.pnl.toFixed(2)}% | +${r.peak.toFixed(2)}% | ${r.drawdown.toFixed(2)}% |`);
3481
+ const filePath = resolve(dumpDir, `${dumpName}.md`);
3482
+ await mkdir(dumpDir, { recursive: true });
3483
+ await writeFile(filePath, [header, ...mdRows].join("\n"), "utf-8");
3484
+ console.log(`Saved: ${filePath}`);
3485
+ process.exit(0);
3486
+ }
3487
+ console.log(`Symbol: ${symbol} | Direction: ${direction} | PriceOpen: ${priceOpen} | From: ${new Date(timestamp).toISOString()} | Minutes: ${minutes}\n`);
3488
+ console.log(`${"min".padStart(5)} | ${"timestamp".padEnd(24)} | ${"close".padStart(12)} | ${"pnl%".padStart(8)} | ${"peak%".padStart(8)} | ${"drawdown%".padStart(10)}`);
3489
+ console.log("-".repeat(83));
3490
+ for (const r of rows) {
3491
+ const min = String(r.min).padStart(5);
3492
+ const ts = new Date(r.timestamp).toISOString().padEnd(24);
3493
+ const close = r.close.toFixed(2).padStart(12);
3494
+ const pnlStr = (r.pnl >= 0 ? "+" : "") + r.pnl.toFixed(2) + "%";
3495
+ const peakStr = "+" + r.peak.toFixed(2) + "%";
3496
+ const drawdownStr = r.drawdown.toFixed(2) + "%";
3497
+ console.log(`${min} | ${ts} | ${close} | ${pnlStr.padStart(8)} | ${peakStr.padStart(8)} | ${drawdownStr.padStart(10)}`);
3498
+ }
3499
+ process.exit(0);
3500
+ };
3371
3501
  main$3();
3372
3502
 
3373
3503
  const __filename = fileURLToPath(import.meta.url);
@@ -3486,6 +3616,7 @@ Modes:
3486
3616
  --pine <entry> Execute a local .pine indicator file
3487
3617
  --editor Open the Pine Script visual editor in the browser
3488
3618
  --dump Fetch and save raw OHLCV candles
3619
+ --pnldebug Simulate PnL per minute for a given entry price and direction
3489
3620
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
3490
3621
  --init Scaffold a new project in the current directory
3491
3622
  --help Print this help message
@@ -3558,6 +3689,21 @@ Candle dump flags (--dump):
3558
3689
 
3559
3690
  Module file ./modules/dump.module is loaded automatically if it exists.
3560
3691
 
3692
+ PnL debug flags (--pnldebug):
3693
+
3694
+ --symbol <string> Trading pair (default: BTCUSDT)
3695
+ --priceopen <number> Entry price (required)
3696
+ --direction <string> Position direction: long or short (default: long)
3697
+ --when <string> Start timestamp โ€” ISO 8601 or Unix ms (default: now)
3698
+ --minutes <string> Number of 1m candles to simulate (default: 60)
3699
+ --exchange <string> Exchange name (default: first registered)
3700
+ --output <string> Output file base name (default: {SYMBOL}_{DIRECTION}_{PRICEOPEN}_{TIMESTAMP})
3701
+ --json Save as JSON array to ./dump/<output>.json
3702
+ --jsonl Save as JSONL to ./dump/<output>.jsonl
3703
+ --markdown Save as Markdown table to ./dump/<output>.md
3704
+
3705
+ Module file ./modules/pnldebug.module is loaded automatically if it exists.
3706
+
3561
3707
  Flush flags (--flush):
3562
3708
 
3563
3709
  One or more positional entry points. For each entry point the following
@@ -3580,6 +3726,7 @@ Module hooks (loaded automatically by each mode):
3580
3726
  modules/pine.module --pine Exchange schema for PineScript runs
3581
3727
  modules/editor.module --editor Exchange schema for the visual Pine editor
3582
3728
  modules/dump.module --dump Exchange schema for candle dumps
3729
+ modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
3583
3730
 
3584
3731
  --flush has no associated module. It only removes dump subdirectories.
3585
3732
 
@@ -3603,6 +3750,8 @@ Examples:
3603
3750
  node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
3604
3751
  node ${ENTRY_PATH} --editor
3605
3752
  node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
3753
+ node ${ENTRY_PATH} --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120
3754
+ node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
3606
3755
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
3607
3756
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
3608
3757
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -3615,7 +3764,7 @@ const main$1 = async () => {
3615
3764
  if (!values.help) {
3616
3765
  return;
3617
3766
  }
3618
- process.stdout.write(`@backtest-kit/cli ${"7.4.0"}\n\n`);
3767
+ process.stdout.write(`@backtest-kit/cli ${"7.6.0"}\n\n`);
3619
3768
  process.stdout.write(HELP_TEXT);
3620
3769
  process.exit(0);
3621
3770
  };
@@ -3629,7 +3778,7 @@ const main = async () => {
3629
3778
  if (!values.version) {
3630
3779
  return;
3631
3780
  }
3632
- process.stdout.write(`@backtest-kit/cli ${"7.4.0"}\n`);
3781
+ process.stdout.write(`@backtest-kit/cli ${"7.6.0"}\n`);
3633
3782
  process.exit(0);
3634
3783
  };
3635
3784
  main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtest-kit/cli",
3
- "version": "7.4.0",
3
+ "version": "7.6.0",
4
4
  "description": "Zero-boilerplate CLI runner for backtest-kit strategies. Run backtests, paper trading, and live bots with candle cache warming, web dashboard, and Telegram notifications โ€” no setup code required.",
5
5
  "author": {
6
6
  "name": "Petr Tripolsky",
@@ -62,11 +62,11 @@
62
62
  "devDependencies": {
63
63
  "@babel/plugin-transform-modules-umd": "7.27.1",
64
64
  "@babel/standalone": "7.29.1",
65
- "@backtest-kit/graph": "7.4.0",
66
- "@backtest-kit/ollama": "7.4.0",
67
- "@backtest-kit/pinets": "7.4.0",
68
- "@backtest-kit/signals": "7.4.0",
69
- "@backtest-kit/ui": "7.4.0",
65
+ "@backtest-kit/graph": "7.6.0",
66
+ "@backtest-kit/ollama": "7.6.0",
67
+ "@backtest-kit/pinets": "7.6.0",
68
+ "@backtest-kit/signals": "7.6.0",
69
+ "@backtest-kit/ui": "7.6.0",
70
70
  "@rollup/plugin-replace": "6.0.3",
71
71
  "@rollup/plugin-typescript": "11.1.6",
72
72
  "@types/image-size": "0.7.0",
@@ -74,7 +74,7 @@
74
74
  "@types/mustache": "4.2.6",
75
75
  "@types/node": "22.9.0",
76
76
  "@types/stack-trace": "0.0.33",
77
- "backtest-kit": "7.4.0",
77
+ "backtest-kit": "7.6.0",
78
78
  "glob": "11.0.1",
79
79
  "markdown-it": "14.1.1",
80
80
  "rimraf": "6.0.1",
@@ -89,12 +89,12 @@
89
89
  "peerDependencies": {
90
90
  "@babel/plugin-transform-modules-umd": "^7.27.1",
91
91
  "@babel/standalone": "^7.29.1",
92
- "@backtest-kit/graph": "^7.4.0",
93
- "@backtest-kit/ollama": "^7.4.0",
94
- "@backtest-kit/pinets": "^7.4.0",
95
- "@backtest-kit/signals": "^7.4.0",
96
- "@backtest-kit/ui": "^7.4.0",
97
- "backtest-kit": "^7.4.0",
92
+ "@backtest-kit/graph": "^7.6.0",
93
+ "@backtest-kit/ollama": "^7.6.0",
94
+ "@backtest-kit/pinets": "^7.6.0",
95
+ "@backtest-kit/signals": "^7.6.0",
96
+ "@backtest-kit/ui": "^7.6.0",
97
+ "backtest-kit": "^7.6.0",
98
98
  "markdown-it": "^14.1.1",
99
99
  "typescript": "^5.0.0"
100
100
  },
@@ -13,12 +13,12 @@
13
13
  "license": "ISC",
14
14
  "type": "commonjs",
15
15
  "dependencies": {
16
- "@backtest-kit/cli": "^7.4.0",
17
- "@backtest-kit/graph": "^7.4.0",
18
- "@backtest-kit/pinets": "^7.4.0",
19
- "@backtest-kit/ui": "^7.4.0",
16
+ "@backtest-kit/cli": "^7.6.0",
17
+ "@backtest-kit/graph": "^7.6.0",
18
+ "@backtest-kit/pinets": "^7.6.0",
19
+ "@backtest-kit/ui": "^7.6.0",
20
20
  "agent-swarm-kit": "^2.6.0",
21
- "backtest-kit": "^7.4.0",
21
+ "backtest-kit": "^7.6.0",
22
22
  "functools-kit": "^2.3.0",
23
23
  "garch": "^1.2.3",
24
24
  "get-moment-stamp": "^1.1.2",
@@ -1,21 +1,21 @@
1
- **โ„น๏ธ Signal Info**
2
-
3
- **Symbol:** `{{symbol}}` ({{data.position}})
4
- **Current:** `{{currentPrice}}`
5
- **Entry:** `{{data.priceOpen}}`
6
- **Orig Entry:** `{{data.originalPriceOpen}}`
7
- **DCA Entries:** `{{data.totalEntries}}`
8
- **Partials Done:** `{{data.totalPartials}}`
9
- **Take Profit:** `{{data.priceTakeProfit}}`
10
- **Stop Loss:** `{{data.priceStopLoss}}`
11
- **Orig TP:** `{{data.originalPriceTakeProfit}}`
12
- **Orig SL:** `{{data.originalPriceStopLoss}}`
13
- **PnL:** {{data.pnl.pnlPercentage}}% ({{data.pnl.pnlCost}} / {{data.pnl.pnlEntries}})
14
- **Peak Profit:** {{data.peakProfit.pnlPercentage}}% ({{data.peakProfit.pnlCost}} / {{data.peakProfit.pnlEntries}})
15
- **Max Drawdown:** {{data.maxDrawdown.pnlPercentage}}% ({{data.maxDrawdown.pnlCost}} / {{data.maxDrawdown.pnlEntries}})
16
- **Signal ID:** `{{data.id}}`
17
- **Time:** `{{timestamp}}`
18
-
19
- {{#backtest}}
20
- _๐Ÿงช Backtest_
1
+ **โ„น๏ธ Signal Info**
2
+
3
+ **Symbol:** `{{symbol}}` ({{data.position}})
4
+ **Current:** `{{currentPrice}}`
5
+ **Entry:** `{{data.priceOpen}}`
6
+ **Orig Entry:** `{{data.originalPriceOpen}}`
7
+ **DCA Entries:** `{{data.totalEntries}}`
8
+ **Partials Done:** `{{data.totalPartials}}`
9
+ **Take Profit:** `{{data.priceTakeProfit}}`
10
+ **Stop Loss:** `{{data.priceStopLoss}}`
11
+ **Orig TP:** `{{data.originalPriceTakeProfit}}`
12
+ **Orig SL:** `{{data.originalPriceStopLoss}}`
13
+ **PnL:** {{data.pnl.pnlPercentage}}% ({{data.pnl.pnlCost}} / {{data.pnl.pnlEntries}})
14
+ **Peak Profit:** {{data.peakProfit.pnlPercentage}}% ({{data.peakProfit.pnlCost}} / {{data.peakProfit.pnlEntries}})
15
+ **Max Drawdown:** {{data.maxDrawdown.pnlPercentage}}% ({{data.maxDrawdown.pnlCost}} / {{data.maxDrawdown.pnlEntries}})
16
+ **Signal ID:** `{{data.id}}`
17
+ **Time:** `{{timestamp}}`
18
+
19
+ {{#backtest}}
20
+ _๐Ÿงช Backtest_
21
21
  {{/backtest}}