@backtest-kit/cli 7.4.0 โ†’ 7.5.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,11 @@ 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();
724
740
  }
725
741
  {
726
742
  BacktestKit.StorageLive.usePersist();
@@ -734,6 +750,18 @@ class SetupUtils {
734
750
  BacktestKit.NotificationLive.usePersist();
735
751
  BacktestKit.NotificationBacktest.useMemory();
736
752
  }
753
+ {
754
+ BacktestKit.RecentLive.usePersist();
755
+ BacktestKit.RecentBacktest.useMemory();
756
+ }
757
+ {
758
+ BacktestKit.MemoryLive.usePersist();
759
+ BacktestKit.MemoryBacktest.useLocal();
760
+ }
761
+ {
762
+ BacktestKit.StateLive.usePersist();
763
+ BacktestKit.StateBacktest.useLocal();
764
+ }
737
765
  {
738
766
  BacktestKit.Markdown.useDummy();
739
767
  BacktestKit.Log.useJsonl();
@@ -776,6 +804,7 @@ class SetupUtils {
776
804
  BacktestKit.PersistIntervalAdapter.clear();
777
805
  BacktestKit.PersistMemoryAdapter.clear();
778
806
  BacktestKit.PersistRecentAdapter.clear();
807
+ BacktestKit.PersistStateAdapter.clear();
779
808
  }
780
809
  {
781
810
  BacktestKit.Dump.clear();
@@ -2965,14 +2994,14 @@ const cli = {
2965
2994
  };
2966
2995
  init();
2967
2996
 
2968
- const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "flush", "init", "help", "version"];
2997
+ const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "pnldebug", "flush", "init", "help", "version"];
2969
2998
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
2970
2999
  const HELP_TEXT$1 = `
2971
3000
  Example:
2972
3001
 
2973
3002
  node ${ENTRY_PATH$1} --help
2974
3003
  `.trimStart();
2975
- const main$d = async () => {
3004
+ const main$e = async () => {
2976
3005
  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
3006
  return;
2978
3007
  }
@@ -2980,14 +3009,14 @@ const main$d = async () => {
2980
3009
  if (MODES.some((mode) => values[mode])) {
2981
3010
  return;
2982
3011
  }
2983
- process.stdout.write(`@backtest-kit/cli ${"7.4.0"}\n`);
3012
+ process.stdout.write(`@backtest-kit/cli ${"7.5.0"}\n`);
2984
3013
  process.stdout.write("\n");
2985
3014
  process.stdout.write(`Run with --help to see available commands.\n`);
2986
3015
  process.stdout.write("\n");
2987
3016
  process.stdout.write(HELP_TEXT$1);
2988
3017
  process.exit(0);
2989
3018
  };
2990
- main$d();
3019
+ main$e();
2991
3020
 
2992
3021
  const notifyShutdown = functoolsKit.singleshot(async () => {
2993
3022
  console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
@@ -3003,7 +3032,7 @@ const flush = async (entryPoint) => {
3003
3032
  console.log(`Removed: ${target}`);
3004
3033
  }
3005
3034
  };
3006
- const main$c = async () => {
3035
+ const main$d = async () => {
3007
3036
  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
3037
  return;
3009
3038
  }
@@ -3020,7 +3049,7 @@ const main$c = async () => {
3020
3049
  }
3021
3050
  process.exit(0);
3022
3051
  };
3023
- main$c();
3052
+ main$d();
3024
3053
 
3025
3054
  const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3026
3055
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3042,7 +3071,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3042
3071
  const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
3043
3072
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3044
3073
  });
3045
- const main$b = async () => {
3074
+ const main$c = async () => {
3046
3075
  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
3076
  return;
3048
3077
  }
@@ -3057,7 +3086,7 @@ const main$b = async () => {
3057
3086
  await cli.backtestMainService.connect();
3058
3087
  listenGracefulShutdown$5();
3059
3088
  };
3060
- main$b();
3089
+ main$c();
3061
3090
 
3062
3091
  const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3063
3092
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3075,7 +3104,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3075
3104
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
3076
3105
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3077
3106
  });
3078
- const main$a = async () => {
3107
+ const main$b = async () => {
3079
3108
  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
3109
  return;
3081
3110
  }
@@ -3091,7 +3120,7 @@ const main$a = async () => {
3091
3120
  listenGracefulShutdown$4();
3092
3121
  await cli.walkerMainService.connect();
3093
3122
  };
3094
- main$a();
3123
+ main$b();
3095
3124
 
3096
3125
  const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3097
3126
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3112,7 +3141,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3112
3141
  const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
3113
3142
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3114
3143
  });
3115
- const main$9 = async () => {
3144
+ const main$a = async () => {
3116
3145
  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
3146
  return;
3118
3147
  }
@@ -3123,7 +3152,7 @@ const main$9 = async () => {
3123
3152
  cli.paperMainService.connect();
3124
3153
  listenGracefulShutdown$3();
3125
3154
  };
3126
- main$9();
3155
+ main$a();
3127
3156
 
3128
3157
  const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
3129
3158
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -3144,7 +3173,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
3144
3173
  const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
3145
3174
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3146
3175
  });
3147
- const main$8 = async () => {
3176
+ const main$9 = async () => {
3148
3177
  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
3178
  return;
3150
3179
  }
@@ -3155,7 +3184,7 @@ const main$8 = async () => {
3155
3184
  await cli.liveMainService.connect();
3156
3185
  listenGracefulShutdown$2();
3157
3186
  };
3158
- main$8();
3187
+ main$9();
3159
3188
 
3160
3189
  const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
3161
3190
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -3165,7 +3194,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
3165
3194
  const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
3166
3195
  process.on("SIGINT", BEFORE_EXIT_FN$1);
3167
3196
  });
3168
- const main$7 = async () => {
3197
+ const main$8 = async () => {
3169
3198
  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
3199
  return;
3171
3200
  }
@@ -3175,7 +3204,7 @@ const main$7 = async () => {
3175
3204
  }
3176
3205
  listenGracefulShutdown$1();
3177
3206
  };
3178
- main$7();
3207
+ main$8();
3179
3208
 
3180
3209
  const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
3181
3210
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -3185,7 +3214,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
3185
3214
  const listenGracefulShutdown = functoolsKit.singleshot(() => {
3186
3215
  process.on("SIGINT", BEFORE_EXIT_FN);
3187
3216
  });
3188
- const main$6 = async () => {
3217
+ const main$7 = async () => {
3189
3218
  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
3219
  return;
3191
3220
  }
@@ -3195,7 +3224,7 @@ const main$6 = async () => {
3195
3224
  }
3196
3225
  listenGracefulShutdown();
3197
3226
  };
3198
- main$6();
3227
+ main$7();
3199
3228
 
3200
3229
  const EXTRACT_ROWS_FN = (plots, schema) => {
3201
3230
  const keys = Object.keys(schema);
@@ -3217,7 +3246,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
3217
3246
  }
3218
3247
  return rows;
3219
3248
  };
3220
- const main$5 = async () => {
3249
+ const main$6 = async () => {
3221
3250
  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
3251
  return;
3223
3252
  }
@@ -3297,9 +3326,9 @@ const main$5 = async () => {
3297
3326
  console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
3298
3327
  process.exit(0);
3299
3328
  };
3300
- main$5();
3329
+ main$6();
3301
3330
 
3302
- const main$4 = async () => {
3331
+ const main$5 = async () => {
3303
3332
  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
3333
  return;
3305
3334
  }
@@ -3334,9 +3363,9 @@ const main$4 = async () => {
3334
3363
  };
3335
3364
  process.on("SIGINT", beforeExit);
3336
3365
  };
3337
- main$4();
3366
+ main$5();
3338
3367
 
3339
- const main$3 = async () => {
3368
+ const main$4 = async () => {
3340
3369
  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
3370
  return;
3342
3371
  }
@@ -3397,6 +3426,102 @@ const main$3 = async () => {
3397
3426
  console.log(JSON.stringify(candles, null, 2));
3398
3427
  process.exit(0);
3399
3428
  };
3429
+ main$4();
3430
+
3431
+ const main$3 = async () => {
3432
+ 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)))) {
3433
+ return;
3434
+ }
3435
+ const { values } = getArgs();
3436
+ if (!values.pnldebug) {
3437
+ return;
3438
+ }
3439
+ await cli.moduleConnectionService.loadModule("./pnldebug.module");
3440
+ {
3441
+ await cli.exchangeSchemaService.addSchema();
3442
+ await cli.symbolSchemaService.addSchema();
3443
+ }
3444
+ const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
3445
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
3446
+ const symbol = values.symbol || "BTCUSDT";
3447
+ const priceOpenStr = values.priceopen;
3448
+ if (!priceOpenStr) {
3449
+ console.error("Error: --priceopen is required");
3450
+ process.exit(1);
3451
+ }
3452
+ const priceOpen = parseFloat(priceOpenStr);
3453
+ if (isNaN(priceOpen)) {
3454
+ console.error(`Error: --priceopen must be a number, got: ${priceOpenStr}`);
3455
+ process.exit(1);
3456
+ }
3457
+ const direction = (values.direction || "long").toLowerCase();
3458
+ if (direction !== "long" && direction !== "short") {
3459
+ console.error(`Error: --direction must be 'long' or 'short', got: ${direction}`);
3460
+ process.exit(1);
3461
+ }
3462
+ const whenStr = values.when || Date.now().toString();
3463
+ const whenStamp = Date.parse(whenStr);
3464
+ const when = isNaN(whenStamp) ? new Date() : new Date(whenStamp);
3465
+ const timestamp = BacktestKit.alignToInterval(when, "1m").getTime();
3466
+ const minutesStr = values.minutes || "60";
3467
+ const minutesNum = parseInt(minutesStr);
3468
+ const minutes = isNaN(minutesNum) ? 60 : minutesNum;
3469
+ const candles = await BacktestKit.Exchange.getRawCandles(symbol, "1m", { exchangeName }, minutes, undefined, timestamp);
3470
+ if (candles.length === 0) {
3471
+ console.error("Error: no candles returned for the given parameters");
3472
+ process.exit(1);
3473
+ }
3474
+ let peak = 0;
3475
+ let drawdown = 0;
3476
+ const rows = candles.map((c, i) => {
3477
+ const pnl = direction === "short"
3478
+ ? (priceOpen - c.close) / priceOpen * 100
3479
+ : (c.close - priceOpen) / priceOpen * 100;
3480
+ if (pnl > peak)
3481
+ peak = pnl;
3482
+ if (pnl < drawdown)
3483
+ drawdown = pnl;
3484
+ return { min: i + 1, timestamp: c.timestamp, close: c.close, pnl, peak, drawdown };
3485
+ });
3486
+ const dumpName = values.output || `${symbol}_${direction}_${priceOpen}_${timestamp}`;
3487
+ const dumpDir = path.join(process.cwd(), "dump");
3488
+ if (values.json) {
3489
+ const filePath = path.resolve(dumpDir, `${dumpName}.json`);
3490
+ await fs$1.mkdir(dumpDir, { recursive: true });
3491
+ await fs$1.writeFile(filePath, JSON.stringify(rows, null, 2), "utf-8");
3492
+ console.log(`Saved: ${filePath}`);
3493
+ process.exit(0);
3494
+ }
3495
+ if (values.jsonl) {
3496
+ const filePath = path.resolve(dumpDir, `${dumpName}.jsonl`);
3497
+ await fs$1.mkdir(dumpDir, { recursive: true });
3498
+ await fs$1.writeFile(filePath, rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
3499
+ console.log(`Saved: ${filePath}`);
3500
+ process.exit(0);
3501
+ }
3502
+ if (values.markdown) {
3503
+ const header = `| min | timestamp | close | pnl% | peak% | drawdown% |\n| --- | --- | --- | --- | --- | --- |`;
3504
+ 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)}% |`);
3505
+ const filePath = path.resolve(dumpDir, `${dumpName}.md`);
3506
+ await fs$1.mkdir(dumpDir, { recursive: true });
3507
+ await fs$1.writeFile(filePath, [header, ...mdRows].join("\n"), "utf-8");
3508
+ console.log(`Saved: ${filePath}`);
3509
+ process.exit(0);
3510
+ }
3511
+ console.log(`Symbol: ${symbol} | Direction: ${direction} | PriceOpen: ${priceOpen} | From: ${new Date(timestamp).toISOString()} | Minutes: ${minutes}\n`);
3512
+ console.log(`${"min".padStart(5)} | ${"timestamp".padEnd(24)} | ${"close".padStart(12)} | ${"pnl%".padStart(8)} | ${"peak%".padStart(8)} | ${"drawdown%".padStart(10)}`);
3513
+ console.log("-".repeat(83));
3514
+ for (const r of rows) {
3515
+ const min = String(r.min).padStart(5);
3516
+ const ts = new Date(r.timestamp).toISOString().padEnd(24);
3517
+ const close = r.close.toFixed(2).padStart(12);
3518
+ const pnlStr = (r.pnl >= 0 ? "+" : "") + r.pnl.toFixed(2) + "%";
3519
+ const peakStr = "+" + r.peak.toFixed(2) + "%";
3520
+ const drawdownStr = r.drawdown.toFixed(2) + "%";
3521
+ console.log(`${min} | ${ts} | ${close} | ${pnlStr.padStart(8)} | ${peakStr.padStart(8)} | ${drawdownStr.padStart(10)}`);
3522
+ }
3523
+ process.exit(0);
3524
+ };
3400
3525
  main$3();
3401
3526
 
3402
3527
  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 +3640,7 @@ Modes:
3515
3640
  --pine <entry> Execute a local .pine indicator file
3516
3641
  --editor Open the Pine Script visual editor in the browser
3517
3642
  --dump Fetch and save raw OHLCV candles
3643
+ --pnldebug Simulate PnL per minute for a given entry price and direction
3518
3644
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
3519
3645
  --init Scaffold a new project in the current directory
3520
3646
  --help Print this help message
@@ -3587,6 +3713,21 @@ Candle dump flags (--dump):
3587
3713
 
3588
3714
  Module file ./modules/dump.module is loaded automatically if it exists.
3589
3715
 
3716
+ PnL debug flags (--pnldebug):
3717
+
3718
+ --symbol <string> Trading pair (default: BTCUSDT)
3719
+ --priceopen <number> Entry price (required)
3720
+ --direction <string> Position direction: long or short (default: long)
3721
+ --when <string> Start timestamp โ€” ISO 8601 or Unix ms (default: now)
3722
+ --minutes <string> Number of 1m candles to simulate (default: 60)
3723
+ --exchange <string> Exchange name (default: first registered)
3724
+ --output <string> Output file base name (default: {SYMBOL}_{DIRECTION}_{PRICEOPEN}_{TIMESTAMP})
3725
+ --json Save as JSON array to ./dump/<output>.json
3726
+ --jsonl Save as JSONL to ./dump/<output>.jsonl
3727
+ --markdown Save as Markdown table to ./dump/<output>.md
3728
+
3729
+ Module file ./modules/pnldebug.module is loaded automatically if it exists.
3730
+
3590
3731
  Flush flags (--flush):
3591
3732
 
3592
3733
  One or more positional entry points. For each entry point the following
@@ -3609,6 +3750,7 @@ Module hooks (loaded automatically by each mode):
3609
3750
  modules/pine.module --pine Exchange schema for PineScript runs
3610
3751
  modules/editor.module --editor Exchange schema for the visual Pine editor
3611
3752
  modules/dump.module --dump Exchange schema for candle dumps
3753
+ modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
3612
3754
 
3613
3755
  --flush has no associated module. It only removes dump subdirectories.
3614
3756
 
@@ -3632,6 +3774,8 @@ Examples:
3632
3774
  node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
3633
3775
  node ${ENTRY_PATH} --editor
3634
3776
  node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
3777
+ node ${ENTRY_PATH} --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120
3778
+ node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
3635
3779
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
3636
3780
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
3637
3781
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -3644,7 +3788,7 @@ const main$1 = async () => {
3644
3788
  if (!values.help) {
3645
3789
  return;
3646
3790
  }
3647
- process.stdout.write(`@backtest-kit/cli ${"7.4.0"}\n\n`);
3791
+ process.stdout.write(`@backtest-kit/cli ${"7.5.0"}\n\n`);
3648
3792
  process.stdout.write(HELP_TEXT);
3649
3793
  process.exit(0);
3650
3794
  };
@@ -3658,7 +3802,7 @@ const main = async () => {
3658
3802
  if (!values.version) {
3659
3803
  return;
3660
3804
  }
3661
- process.stdout.write(`@backtest-kit/cli ${"7.4.0"}\n`);
3805
+ process.stdout.write(`@backtest-kit/cli ${"7.5.0"}\n`);
3662
3806
  process.exit(0);
3663
3807
  };
3664
3808
  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, 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, listStrategySchema, overrideExchangeSchema, Backtest, Session, 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,11 @@ 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();
699
715
  }
700
716
  {
701
717
  StorageLive.usePersist();
@@ -709,6 +725,18 @@ class SetupUtils {
709
725
  NotificationLive.usePersist();
710
726
  NotificationBacktest.useMemory();
711
727
  }
728
+ {
729
+ RecentLive.usePersist();
730
+ RecentBacktest.useMemory();
731
+ }
732
+ {
733
+ MemoryLive.usePersist();
734
+ MemoryBacktest.useLocal();
735
+ }
736
+ {
737
+ StateLive.usePersist();
738
+ StateBacktest.useLocal();
739
+ }
712
740
  {
713
741
  Markdown.useDummy();
714
742
  Log.useJsonl();
@@ -751,6 +779,7 @@ class SetupUtils {
751
779
  PersistIntervalAdapter.clear();
752
780
  PersistMemoryAdapter.clear();
753
781
  PersistRecentAdapter.clear();
782
+ PersistStateAdapter.clear();
754
783
  }
755
784
  {
756
785
  Dump.clear();
@@ -2936,14 +2965,14 @@ const cli = {
2936
2965
  };
2937
2966
  init();
2938
2967
 
2939
- const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "flush", "init", "help", "version"];
2968
+ const MODES = ["backtest", "walker", "paper", "live", "pine", "editor", "dump", "pnldebug", "flush", "init", "help", "version"];
2940
2969
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
2941
2970
  const HELP_TEXT$1 = `
2942
2971
  Example:
2943
2972
 
2944
2973
  node ${ENTRY_PATH$1} --help
2945
2974
  `.trimStart();
2946
- const main$d = async () => {
2975
+ const main$e = async () => {
2947
2976
  if (!getEntry(import.meta.url)) {
2948
2977
  return;
2949
2978
  }
@@ -2951,14 +2980,14 @@ const main$d = async () => {
2951
2980
  if (MODES.some((mode) => values[mode])) {
2952
2981
  return;
2953
2982
  }
2954
- process.stdout.write(`@backtest-kit/cli ${"7.4.0"}\n`);
2983
+ process.stdout.write(`@backtest-kit/cli ${"7.5.0"}\n`);
2955
2984
  process.stdout.write("\n");
2956
2985
  process.stdout.write(`Run with --help to see available commands.\n`);
2957
2986
  process.stdout.write("\n");
2958
2987
  process.stdout.write(HELP_TEXT$1);
2959
2988
  process.exit(0);
2960
2989
  };
2961
- main$d();
2990
+ main$e();
2962
2991
 
2963
2992
  const notifyShutdown = singleshot(async () => {
2964
2993
  console.log("Graceful shutdown initiated. Press Ctrl+C again to force quit.");
@@ -2974,7 +3003,7 @@ const flush = async (entryPoint) => {
2974
3003
  console.log(`Removed: ${target}`);
2975
3004
  }
2976
3005
  };
2977
- const main$c = async () => {
3006
+ const main$d = async () => {
2978
3007
  if (!getEntry(import.meta.url)) {
2979
3008
  return;
2980
3009
  }
@@ -2991,7 +3020,7 @@ const main$c = async () => {
2991
3020
  }
2992
3021
  process.exit(0);
2993
3022
  };
2994
- main$c();
3023
+ main$d();
2995
3024
 
2996
3025
  const BEFORE_EXIT_FN$5 = singleshot(async () => {
2997
3026
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3013,7 +3042,7 @@ const BEFORE_EXIT_FN$5 = singleshot(async () => {
3013
3042
  const listenGracefulShutdown$5 = singleshot(() => {
3014
3043
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3015
3044
  });
3016
- const main$b = async () => {
3045
+ const main$c = async () => {
3017
3046
  if (!getEntry(import.meta.url)) {
3018
3047
  return;
3019
3048
  }
@@ -3028,7 +3057,7 @@ const main$b = async () => {
3028
3057
  await cli.backtestMainService.connect();
3029
3058
  listenGracefulShutdown$5();
3030
3059
  };
3031
- main$b();
3060
+ main$c();
3032
3061
 
3033
3062
  const BEFORE_EXIT_FN$4 = singleshot(async () => {
3034
3063
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3046,7 +3075,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
3046
3075
  const listenGracefulShutdown$4 = singleshot(() => {
3047
3076
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3048
3077
  });
3049
- const main$a = async () => {
3078
+ const main$b = async () => {
3050
3079
  if (!getEntry(import.meta.url)) {
3051
3080
  return;
3052
3081
  }
@@ -3062,7 +3091,7 @@ const main$a = async () => {
3062
3091
  listenGracefulShutdown$4();
3063
3092
  await cli.walkerMainService.connect();
3064
3093
  };
3065
- main$a();
3094
+ main$b();
3066
3095
 
3067
3096
  const BEFORE_EXIT_FN$3 = singleshot(async () => {
3068
3097
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3083,7 +3112,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
3083
3112
  const listenGracefulShutdown$3 = singleshot(() => {
3084
3113
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3085
3114
  });
3086
- const main$9 = async () => {
3115
+ const main$a = async () => {
3087
3116
  if (!getEntry(import.meta.url)) {
3088
3117
  return;
3089
3118
  }
@@ -3094,7 +3123,7 @@ const main$9 = async () => {
3094
3123
  cli.paperMainService.connect();
3095
3124
  listenGracefulShutdown$3();
3096
3125
  };
3097
- main$9();
3126
+ main$a();
3098
3127
 
3099
3128
  const BEFORE_EXIT_FN$2 = singleshot(async () => {
3100
3129
  process.off("SIGINT", BEFORE_EXIT_FN$2);
@@ -3115,7 +3144,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
3115
3144
  const listenGracefulShutdown$2 = singleshot(() => {
3116
3145
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3117
3146
  });
3118
- const main$8 = async () => {
3147
+ const main$9 = async () => {
3119
3148
  if (!getEntry(import.meta.url)) {
3120
3149
  return;
3121
3150
  }
@@ -3126,7 +3155,7 @@ const main$8 = async () => {
3126
3155
  await cli.liveMainService.connect();
3127
3156
  listenGracefulShutdown$2();
3128
3157
  };
3129
- main$8();
3158
+ main$9();
3130
3159
 
3131
3160
  const BEFORE_EXIT_FN$1 = singleshot(async () => {
3132
3161
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -3136,7 +3165,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
3136
3165
  const listenGracefulShutdown$1 = singleshot(() => {
3137
3166
  process.on("SIGINT", BEFORE_EXIT_FN$1);
3138
3167
  });
3139
- const main$7 = async () => {
3168
+ const main$8 = async () => {
3140
3169
  if (!getEntry(import.meta.url)) {
3141
3170
  return;
3142
3171
  }
@@ -3146,7 +3175,7 @@ const main$7 = async () => {
3146
3175
  }
3147
3176
  listenGracefulShutdown$1();
3148
3177
  };
3149
- main$7();
3178
+ main$8();
3150
3179
 
3151
3180
  const BEFORE_EXIT_FN = singleshot(async () => {
3152
3181
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -3156,7 +3185,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
3156
3185
  const listenGracefulShutdown = singleshot(() => {
3157
3186
  process.on("SIGINT", BEFORE_EXIT_FN);
3158
3187
  });
3159
- const main$6 = async () => {
3188
+ const main$7 = async () => {
3160
3189
  if (!getEntry(import.meta.url)) {
3161
3190
  return;
3162
3191
  }
@@ -3166,7 +3195,7 @@ const main$6 = async () => {
3166
3195
  }
3167
3196
  listenGracefulShutdown();
3168
3197
  };
3169
- main$6();
3198
+ main$7();
3170
3199
 
3171
3200
  const EXTRACT_ROWS_FN = (plots, schema) => {
3172
3201
  const keys = Object.keys(schema);
@@ -3188,7 +3217,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
3188
3217
  }
3189
3218
  return rows;
3190
3219
  };
3191
- const main$5 = async () => {
3220
+ const main$6 = async () => {
3192
3221
  if (!getEntry(import.meta.url)) {
3193
3222
  return;
3194
3223
  }
@@ -3268,9 +3297,9 @@ const main$5 = async () => {
3268
3297
  console.log(await toMarkdown(signalId, plots, signalSchema));
3269
3298
  process.exit(0);
3270
3299
  };
3271
- main$5();
3300
+ main$6();
3272
3301
 
3273
- const main$4 = async () => {
3302
+ const main$5 = async () => {
3274
3303
  if (!getEntry(import.meta.url)) {
3275
3304
  return;
3276
3305
  }
@@ -3305,9 +3334,9 @@ const main$4 = async () => {
3305
3334
  };
3306
3335
  process.on("SIGINT", beforeExit);
3307
3336
  };
3308
- main$4();
3337
+ main$5();
3309
3338
 
3310
- const main$3 = async () => {
3339
+ const main$4 = async () => {
3311
3340
  if (!getEntry(import.meta.url)) {
3312
3341
  return;
3313
3342
  }
@@ -3368,6 +3397,102 @@ const main$3 = async () => {
3368
3397
  console.log(JSON.stringify(candles, null, 2));
3369
3398
  process.exit(0);
3370
3399
  };
3400
+ main$4();
3401
+
3402
+ const main$3 = async () => {
3403
+ if (!getEntry(import.meta.url)) {
3404
+ return;
3405
+ }
3406
+ const { values } = getArgs();
3407
+ if (!values.pnldebug) {
3408
+ return;
3409
+ }
3410
+ await cli.moduleConnectionService.loadModule("./pnldebug.module");
3411
+ {
3412
+ await cli.exchangeSchemaService.addSchema();
3413
+ await cli.symbolSchemaService.addSchema();
3414
+ }
3415
+ const [defaultExchangeName = null] = await listExchangeSchema();
3416
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
3417
+ const symbol = values.symbol || "BTCUSDT";
3418
+ const priceOpenStr = values.priceopen;
3419
+ if (!priceOpenStr) {
3420
+ console.error("Error: --priceopen is required");
3421
+ process.exit(1);
3422
+ }
3423
+ const priceOpen = parseFloat(priceOpenStr);
3424
+ if (isNaN(priceOpen)) {
3425
+ console.error(`Error: --priceopen must be a number, got: ${priceOpenStr}`);
3426
+ process.exit(1);
3427
+ }
3428
+ const direction = (values.direction || "long").toLowerCase();
3429
+ if (direction !== "long" && direction !== "short") {
3430
+ console.error(`Error: --direction must be 'long' or 'short', got: ${direction}`);
3431
+ process.exit(1);
3432
+ }
3433
+ const whenStr = values.when || Date.now().toString();
3434
+ const whenStamp = Date.parse(whenStr);
3435
+ const when = isNaN(whenStamp) ? new Date() : new Date(whenStamp);
3436
+ const timestamp = alignToInterval(when, "1m").getTime();
3437
+ const minutesStr = values.minutes || "60";
3438
+ const minutesNum = parseInt(minutesStr);
3439
+ const minutes = isNaN(minutesNum) ? 60 : minutesNum;
3440
+ const candles = await Exchange.getRawCandles(symbol, "1m", { exchangeName }, minutes, undefined, timestamp);
3441
+ if (candles.length === 0) {
3442
+ console.error("Error: no candles returned for the given parameters");
3443
+ process.exit(1);
3444
+ }
3445
+ let peak = 0;
3446
+ let drawdown = 0;
3447
+ const rows = candles.map((c, i) => {
3448
+ const pnl = direction === "short"
3449
+ ? (priceOpen - c.close) / priceOpen * 100
3450
+ : (c.close - priceOpen) / priceOpen * 100;
3451
+ if (pnl > peak)
3452
+ peak = pnl;
3453
+ if (pnl < drawdown)
3454
+ drawdown = pnl;
3455
+ return { min: i + 1, timestamp: c.timestamp, close: c.close, pnl, peak, drawdown };
3456
+ });
3457
+ const dumpName = values.output || `${symbol}_${direction}_${priceOpen}_${timestamp}`;
3458
+ const dumpDir = join(process.cwd(), "dump");
3459
+ if (values.json) {
3460
+ const filePath = resolve(dumpDir, `${dumpName}.json`);
3461
+ await mkdir(dumpDir, { recursive: true });
3462
+ await writeFile(filePath, JSON.stringify(rows, null, 2), "utf-8");
3463
+ console.log(`Saved: ${filePath}`);
3464
+ process.exit(0);
3465
+ }
3466
+ if (values.jsonl) {
3467
+ const filePath = resolve(dumpDir, `${dumpName}.jsonl`);
3468
+ await mkdir(dumpDir, { recursive: true });
3469
+ await writeFile(filePath, rows.map((r) => JSON.stringify(r)).join("\n"), "utf-8");
3470
+ console.log(`Saved: ${filePath}`);
3471
+ process.exit(0);
3472
+ }
3473
+ if (values.markdown) {
3474
+ const header = `| min | timestamp | close | pnl% | peak% | drawdown% |\n| --- | --- | --- | --- | --- | --- |`;
3475
+ 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)}% |`);
3476
+ const filePath = resolve(dumpDir, `${dumpName}.md`);
3477
+ await mkdir(dumpDir, { recursive: true });
3478
+ await writeFile(filePath, [header, ...mdRows].join("\n"), "utf-8");
3479
+ console.log(`Saved: ${filePath}`);
3480
+ process.exit(0);
3481
+ }
3482
+ console.log(`Symbol: ${symbol} | Direction: ${direction} | PriceOpen: ${priceOpen} | From: ${new Date(timestamp).toISOString()} | Minutes: ${minutes}\n`);
3483
+ console.log(`${"min".padStart(5)} | ${"timestamp".padEnd(24)} | ${"close".padStart(12)} | ${"pnl%".padStart(8)} | ${"peak%".padStart(8)} | ${"drawdown%".padStart(10)}`);
3484
+ console.log("-".repeat(83));
3485
+ for (const r of rows) {
3486
+ const min = String(r.min).padStart(5);
3487
+ const ts = new Date(r.timestamp).toISOString().padEnd(24);
3488
+ const close = r.close.toFixed(2).padStart(12);
3489
+ const pnlStr = (r.pnl >= 0 ? "+" : "") + r.pnl.toFixed(2) + "%";
3490
+ const peakStr = "+" + r.peak.toFixed(2) + "%";
3491
+ const drawdownStr = r.drawdown.toFixed(2) + "%";
3492
+ console.log(`${min} | ${ts} | ${close} | ${pnlStr.padStart(8)} | ${peakStr.padStart(8)} | ${drawdownStr.padStart(10)}`);
3493
+ }
3494
+ process.exit(0);
3495
+ };
3371
3496
  main$3();
3372
3497
 
3373
3498
  const __filename = fileURLToPath(import.meta.url);
@@ -3486,6 +3611,7 @@ Modes:
3486
3611
  --pine <entry> Execute a local .pine indicator file
3487
3612
  --editor Open the Pine Script visual editor in the browser
3488
3613
  --dump Fetch and save raw OHLCV candles
3614
+ --pnldebug Simulate PnL per minute for a given entry price and direction
3489
3615
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
3490
3616
  --init Scaffold a new project in the current directory
3491
3617
  --help Print this help message
@@ -3558,6 +3684,21 @@ Candle dump flags (--dump):
3558
3684
 
3559
3685
  Module file ./modules/dump.module is loaded automatically if it exists.
3560
3686
 
3687
+ PnL debug flags (--pnldebug):
3688
+
3689
+ --symbol <string> Trading pair (default: BTCUSDT)
3690
+ --priceopen <number> Entry price (required)
3691
+ --direction <string> Position direction: long or short (default: long)
3692
+ --when <string> Start timestamp โ€” ISO 8601 or Unix ms (default: now)
3693
+ --minutes <string> Number of 1m candles to simulate (default: 60)
3694
+ --exchange <string> Exchange name (default: first registered)
3695
+ --output <string> Output file base name (default: {SYMBOL}_{DIRECTION}_{PRICEOPEN}_{TIMESTAMP})
3696
+ --json Save as JSON array to ./dump/<output>.json
3697
+ --jsonl Save as JSONL to ./dump/<output>.jsonl
3698
+ --markdown Save as Markdown table to ./dump/<output>.md
3699
+
3700
+ Module file ./modules/pnldebug.module is loaded automatically if it exists.
3701
+
3561
3702
  Flush flags (--flush):
3562
3703
 
3563
3704
  One or more positional entry points. For each entry point the following
@@ -3580,6 +3721,7 @@ Module hooks (loaded automatically by each mode):
3580
3721
  modules/pine.module --pine Exchange schema for PineScript runs
3581
3722
  modules/editor.module --editor Exchange schema for the visual Pine editor
3582
3723
  modules/dump.module --dump Exchange schema for candle dumps
3724
+ modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
3583
3725
 
3584
3726
  --flush has no associated module. It only removes dump subdirectories.
3585
3727
 
@@ -3603,6 +3745,8 @@ Examples:
3603
3745
  node ${ENTRY_PATH} --pine ./math/feb_2026.pine --timeframe 15m --limit 500 --jsonl
3604
3746
  node ${ENTRY_PATH} --editor
3605
3747
  node ${ENTRY_PATH} --dump --symbol BTCUSDT --timeframe 15m --limit 500 --jsonl
3748
+ node ${ENTRY_PATH} --pnldebug --symbol BTCUSDT --priceopen 64069.50 --direction short --when "2025-02-25" --minutes 120
3749
+ node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
3606
3750
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
3607
3751
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
3608
3752
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -3615,7 +3759,7 @@ const main$1 = async () => {
3615
3759
  if (!values.help) {
3616
3760
  return;
3617
3761
  }
3618
- process.stdout.write(`@backtest-kit/cli ${"7.4.0"}\n\n`);
3762
+ process.stdout.write(`@backtest-kit/cli ${"7.5.0"}\n\n`);
3619
3763
  process.stdout.write(HELP_TEXT);
3620
3764
  process.exit(0);
3621
3765
  };
@@ -3629,7 +3773,7 @@ const main = async () => {
3629
3773
  if (!values.version) {
3630
3774
  return;
3631
3775
  }
3632
- process.stdout.write(`@backtest-kit/cli ${"7.4.0"}\n`);
3776
+ process.stdout.write(`@backtest-kit/cli ${"7.5.0"}\n`);
3633
3777
  process.exit(0);
3634
3778
  };
3635
3779
  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.5.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.5.0",
66
+ "@backtest-kit/ollama": "7.5.0",
67
+ "@backtest-kit/pinets": "7.5.0",
68
+ "@backtest-kit/signals": "7.5.0",
69
+ "@backtest-kit/ui": "7.5.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.5.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.5.0",
93
+ "@backtest-kit/ollama": "^7.5.0",
94
+ "@backtest-kit/pinets": "^7.5.0",
95
+ "@backtest-kit/signals": "^7.5.0",
96
+ "@backtest-kit/ui": "^7.5.0",
97
+ "backtest-kit": "^7.5.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.5.0",
17
+ "@backtest-kit/graph": "^7.5.0",
18
+ "@backtest-kit/pinets": "^7.5.0",
19
+ "@backtest-kit/ui": "^7.5.0",
20
20
  "agent-swarm-kit": "^2.6.0",
21
- "backtest-kit": "^7.4.0",
21
+ "backtest-kit": "^7.5.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}}