@backtest-kit/cli 16.4.0 → 16.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/build/index.cjs CHANGED
@@ -233,14 +233,14 @@ const getReadySubject = functoolsKit.singleshot(() => {
233
233
  return new functoolsKit.Subject();
234
234
  });
235
235
 
236
- const main$j = () => {
236
+ const main$l = () => {
237
237
  const entrySubject = getEntrySubject();
238
238
  entrySubject.subscribe(async (path) => {
239
239
  console.log("Running", path);
240
240
  await getReadySubject().next();
241
241
  });
242
242
  };
243
- main$j();
243
+ main$l();
244
244
 
245
245
  const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
246
246
  function dumpStackTrace(error) {
@@ -895,6 +895,14 @@ const getArgs = functoolsKit.singleshot(() => {
895
895
  type: "boolean",
896
896
  default: false,
897
897
  },
898
+ tune: {
899
+ type: "boolean",
900
+ default: false,
901
+ },
902
+ split: {
903
+ type: "string",
904
+ default: "",
905
+ },
898
906
  commit: {
899
907
  type: "string",
900
908
  default: "",
@@ -2341,6 +2349,13 @@ const STOP_BOT_FN = functoolsKit.singleshot(async () => {
2341
2349
  const { stopBot } = await getTelegram();
2342
2350
  stopBot();
2343
2351
  });
2352
+ /**
2353
+ * Throttle window for the per-tick order_continue decision channel: at most one
2354
+ * Telegram notification per signalId per 15 minutes via functools-kit ttl. The
2355
+ * terminal order_stop is NOT throttled (fires once per signal) and clears the
2356
+ * throttle slot for its signalId.
2357
+ */
2358
+ const ORDER_CHECK_NOTIFY_TTL = 15 * 60 * 1000;
2344
2359
  class TelegramLogicService {
2345
2360
  constructor() {
2346
2361
  this.loggerService = inject(TYPES.loggerService);
@@ -2489,11 +2504,68 @@ class TelegramLogicService {
2489
2504
  markdown,
2490
2505
  });
2491
2506
  };
2492
- this.notifySignalOpen = functoolsKit.trycatch(async (event) => {
2493
- this.loggerService.log("telegramLogicService notifySignalOpen", {
2507
+ this.notifyOrderFillOpen = functoolsKit.trycatch(async (event) => {
2508
+ this.loggerService.log("telegramLogicService notifyOrderFillOpen", {
2509
+ event,
2510
+ });
2511
+ const markdown = await this.telegramTemplateService.getOrderFillOpenMarkdown(event);
2512
+ if (!markdown) {
2513
+ return;
2514
+ }
2515
+ await this.telegramWebService.publishNotify({
2516
+ symbol: event.symbol,
2517
+ markdown,
2518
+ });
2519
+ });
2520
+ this.notifyOrderFillClose = functoolsKit.trycatch(async (event) => {
2521
+ this.loggerService.log("telegramLogicService notifyOrderFillClose", {
2522
+ event,
2523
+ });
2524
+ const markdown = await this.telegramTemplateService.getOrderFillCloseMarkdown(event);
2525
+ if (!markdown) {
2526
+ return;
2527
+ }
2528
+ await this.telegramWebService.publishNotify({
2529
+ symbol: event.symbol,
2530
+ markdown,
2531
+ });
2532
+ });
2533
+ this.notifyOrderRejectOpen = functoolsKit.trycatch(async (event) => {
2534
+ this.loggerService.log("telegramLogicService notifyOrderRejectOpen", {
2535
+ event,
2536
+ });
2537
+ const markdown = await this.telegramTemplateService.getOrderRejectOpenMarkdown(event);
2538
+ if (!markdown) {
2539
+ return;
2540
+ }
2541
+ await this.telegramWebService.publishNotify({
2542
+ symbol: event.symbol,
2543
+ markdown,
2544
+ });
2545
+ });
2546
+ this.notifyOrderRejectClose = functoolsKit.trycatch(async (event) => {
2547
+ this.loggerService.log("telegramLogicService notifyOrderRejectClose", {
2548
+ event,
2549
+ });
2550
+ const markdown = await this.telegramTemplateService.getOrderRejectCloseMarkdown(event);
2551
+ if (!markdown) {
2552
+ return;
2553
+ }
2554
+ await this.telegramWebService.publishNotify({
2555
+ symbol: event.symbol,
2556
+ markdown,
2557
+ });
2558
+ });
2559
+ /**
2560
+ * Post-verdict order-check CONTINUE (order_continue). Fires every live tick the
2561
+ * order survives the check, so it is throttled via functools-kit ttl:
2562
+ * one message per signalId per ORDER_CHECK_NOTIFY_TTL (15 minutes).
2563
+ */
2564
+ this.notifyOrderContinue = functoolsKit.ttl(functoolsKit.trycatch(async (event) => {
2565
+ this.loggerService.log("telegramLogicService notifyOrderContinue", {
2494
2566
  event,
2495
2567
  });
2496
- const markdown = await this.telegramTemplateService.getSignalOpenMarkdown(event);
2568
+ const markdown = await this.telegramTemplateService.getOrderContinueMarkdown(event);
2497
2569
  if (!markdown) {
2498
2570
  return;
2499
2571
  }
@@ -2501,12 +2573,21 @@ class TelegramLogicService {
2501
2573
  symbol: event.symbol,
2502
2574
  markdown,
2503
2575
  });
2576
+ }), {
2577
+ key: ([event]) => event.signalId,
2578
+ timeout: ORDER_CHECK_NOTIFY_TTL,
2504
2579
  });
2505
- this.notifySignalClose = functoolsKit.trycatch(async (event) => {
2506
- this.loggerService.log("telegramLogicService notifySignalClose", {
2580
+ /**
2581
+ * Post-verdict order-check STOP (order_stop). Terminal — fires once per signal,
2582
+ * so no throttle; drops the continue throttle slot for this signalId
2583
+ * (the signal leaves monitoring, the ttl entry would only leak otherwise).
2584
+ */
2585
+ this.notifyOrderStop = functoolsKit.trycatch(async (event) => {
2586
+ this.loggerService.log("telegramLogicService notifyOrderStop", {
2507
2587
  event,
2508
2588
  });
2509
- const markdown = await this.telegramTemplateService.getSignalCloseMarkdown(event);
2589
+ this.notifyOrderContinue.clear(event.signalId);
2590
+ const markdown = await this.telegramTemplateService.getOrderStopMarkdown(event);
2510
2591
  if (!markdown) {
2511
2592
  return;
2512
2593
  }
@@ -2611,26 +2692,54 @@ class TelegramLogicService {
2611
2692
  return;
2612
2693
  }
2613
2694
  });
2614
- const unSync = BacktestKit.listenSync(async (event) => {
2695
+ // Broker-CONFIRMED fills only (post-verdict orderFillSubject): a rejected or
2696
+ // transient gate attempt never reaches this channel, so "Order Filled" is
2697
+ // guaranteed truthful. The pre-verdict syncSubject is NOT consumed here.
2698
+ const unOrderFill = BacktestKit.listenOrderFill(async (event) => {
2615
2699
  if (event.action === "signal-open") {
2616
- // type "schedule" is a resting-order PLACEMENT, not a position open:
2617
- // the user already got the "scheduled" notification from listenSignal
2618
- if (event.type !== "active") {
2619
- return;
2620
- }
2621
- await this.notifySignalOpen(event);
2700
+ // Both open outcomes notify: type "active" — the order opened at once
2701
+ // (immediate fill, no schedule phase), type "schedule" — the resting
2702
+ // order was placed on the exchange. Either way the broker confirmed a
2703
+ // real order; the template's Order field carries the type.
2704
+ await this.notifyOrderFillOpen(event);
2705
+ return;
2706
+ }
2707
+ if (event.action === "signal-close") {
2708
+ await this.notifyOrderFillClose(event);
2709
+ return;
2710
+ }
2711
+ });
2712
+ // Terminal broker rejection (post-verdict orderRejectSubject): exactly one
2713
+ // event per dropped order attempt — the engine consumes the rejected signal
2714
+ // id, so this cannot repeat per-tick for the same signal. Dispatched by
2715
+ // action to the dedicated open/close notifications (single responsibility).
2716
+ const unOrderReject = BacktestKit.listenOrderReject(async (event) => {
2717
+ if (event.action === "signal-open") {
2718
+ await this.notifyOrderRejectOpen(event);
2622
2719
  return;
2623
2720
  }
2624
2721
  if (event.action === "signal-close") {
2625
- await this.notifySignalClose(event);
2722
+ await this.notifyOrderRejectClose(event);
2626
2723
  return;
2627
2724
  }
2628
2725
  });
2726
+ // Post-verdict check decisions: CONTINUE fires every surviving tick and is
2727
+ // throttled to one message per signalId per 15 minutes via ttl; terminal
2728
+ // STOP fires once per signal, unthrottled.
2729
+ const unOrderContinue = BacktestKit.listenOrderContinue(async (event) => {
2730
+ await this.notifyOrderContinue(event);
2731
+ });
2732
+ const unOrderStop = BacktestKit.listenOrderStop(async (event) => {
2733
+ await this.notifyOrderStop(event);
2734
+ });
2629
2735
  const unSignalNotify = BacktestKit.listenSignalNotify(async (event) => {
2630
2736
  await this.notifySignalInfo(event);
2631
2737
  });
2738
+ const unGc = functoolsKit.Source.fromInterval(90000).connect(() => {
2739
+ this.notifyOrderContinue.gc();
2740
+ });
2632
2741
  const unConnect = () => this.connect.clear();
2633
- const unListen = functoolsKit.compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unSync(), () => unSignalNotify(), () => unConnect());
2742
+ const unListen = functoolsKit.compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unGc(), () => unSignalNotify(), () => unConnect());
2634
2743
  return () => {
2635
2744
  STOP_BOT_FN();
2636
2745
  unListen();
@@ -2783,25 +2892,65 @@ class TelegramTemplateService {
2783
2892
  }
2784
2893
  return await RENDER_TEMPLATE_FN("average-buy.mustache", event, this);
2785
2894
  };
2786
- this.getSignalOpenMarkdown = async (event) => {
2787
- this.loggerService.log("telegramTemplateService getSignalOpenMarkdown", {
2895
+ this.getOrderFillOpenMarkdown = async (event) => {
2896
+ this.loggerService.log("telegramTemplateService getOrderFillOpenMarkdown", {
2897
+ event,
2898
+ });
2899
+ const adapter = await this.getTelegramAdapter();
2900
+ if (adapter?.getOrderFillOpenMarkdown) {
2901
+ return await adapter.getOrderFillOpenMarkdown(event);
2902
+ }
2903
+ return await RENDER_TEMPLATE_FN("order-fill-open.mustache", event, this);
2904
+ };
2905
+ this.getOrderFillCloseMarkdown = async (event) => {
2906
+ this.loggerService.log("telegramTemplateService getOrderFillCloseMarkdown", {
2907
+ event,
2908
+ });
2909
+ const adapter = await this.getTelegramAdapter();
2910
+ if (adapter?.getOrderFillCloseMarkdown) {
2911
+ return await adapter.getOrderFillCloseMarkdown(event);
2912
+ }
2913
+ return await RENDER_TEMPLATE_FN("order-fill-close.mustache", event, this);
2914
+ };
2915
+ this.getOrderRejectOpenMarkdown = async (event) => {
2916
+ this.loggerService.log("telegramTemplateService getOrderRejectOpenMarkdown", {
2917
+ event,
2918
+ });
2919
+ const adapter = await this.getTelegramAdapter();
2920
+ if (adapter?.getOrderRejectOpenMarkdown) {
2921
+ return await adapter.getOrderRejectOpenMarkdown(event);
2922
+ }
2923
+ return await RENDER_TEMPLATE_FN("order-reject-open.mustache", event, this);
2924
+ };
2925
+ this.getOrderRejectCloseMarkdown = async (event) => {
2926
+ this.loggerService.log("telegramTemplateService getOrderRejectCloseMarkdown", {
2927
+ event,
2928
+ });
2929
+ const adapter = await this.getTelegramAdapter();
2930
+ if (adapter?.getOrderRejectCloseMarkdown) {
2931
+ return await adapter.getOrderRejectCloseMarkdown(event);
2932
+ }
2933
+ return await RENDER_TEMPLATE_FN("order-reject-close.mustache", event, this);
2934
+ };
2935
+ this.getOrderContinueMarkdown = async (event) => {
2936
+ this.loggerService.log("telegramTemplateService getOrderContinueMarkdown", {
2788
2937
  event,
2789
2938
  });
2790
2939
  const adapter = await this.getTelegramAdapter();
2791
- if (adapter?.getSignalOpenMarkdown) {
2792
- return await adapter.getSignalOpenMarkdown(event);
2940
+ if (adapter?.getOrderContinueMarkdown) {
2941
+ return await adapter.getOrderContinueMarkdown(event);
2793
2942
  }
2794
- return await RENDER_TEMPLATE_FN("signal-open.mustache", event, this);
2943
+ return await RENDER_TEMPLATE_FN("order-continue.mustache", event, this);
2795
2944
  };
2796
- this.getSignalCloseMarkdown = async (event) => {
2797
- this.loggerService.log("telegramTemplateService getSignalCloseMarkdown", {
2945
+ this.getOrderStopMarkdown = async (event) => {
2946
+ this.loggerService.log("telegramTemplateService getOrderStopMarkdown", {
2798
2947
  event,
2799
2948
  });
2800
2949
  const adapter = await this.getTelegramAdapter();
2801
- if (adapter?.getSignalCloseMarkdown) {
2802
- return await adapter.getSignalCloseMarkdown(event);
2950
+ if (adapter?.getOrderStopMarkdown) {
2951
+ return await adapter.getOrderStopMarkdown(event);
2803
2952
  }
2804
- return await RENDER_TEMPLATE_FN("signal-close.mustache", event, this);
2953
+ return await RENDER_TEMPLATE_FN("order-stop.mustache", event, this);
2805
2954
  };
2806
2955
  this.getCancelScheduledMarkdown = async (event) => {
2807
2956
  this.loggerService.log("telegramTemplateService getCancelScheduledMarkdown", {
@@ -3538,14 +3687,14 @@ const cli = {
3538
3687
  };
3539
3688
  init();
3540
3689
 
3541
- const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "flush", "init", "docker", "help", "version"];
3690
+ const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "tune", "flush", "init", "docker", "help", "version"];
3542
3691
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
3543
3692
  const HELP_TEXT$1 = `
3544
3693
  Example:
3545
3694
 
3546
3695
  node ${ENTRY_PATH$1} --help
3547
3696
  `.trimStart();
3548
- const main$i = async () => {
3697
+ const main$k = async () => {
3549
3698
  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)))) {
3550
3699
  return;
3551
3700
  }
@@ -3553,14 +3702,14 @@ const main$i = async () => {
3553
3702
  if (MODES.some((mode) => values[mode])) {
3554
3703
  return;
3555
3704
  }
3556
- process.stdout.write(`@backtest-kit/cli ${"16.4.0"}\n`);
3705
+ process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
3557
3706
  process.stdout.write("\n");
3558
3707
  process.stdout.write(`Run with --help to see available commands.\n`);
3559
3708
  process.stdout.write("\n");
3560
3709
  process.stdout.write(HELP_TEXT$1);
3561
3710
  process.exit(0);
3562
3711
  };
3563
- main$i();
3712
+ main$k();
3564
3713
 
3565
3714
  const notifyShutdown = functoolsKit.singleshot(async () => {
3566
3715
  console.log("Graceful shutdown initiated.");
@@ -3576,7 +3725,7 @@ const flush = async (entryPoint) => {
3576
3725
  console.log(`Removed: ${target}`);
3577
3726
  }
3578
3727
  };
3579
- const main$h = async () => {
3728
+ const main$j = async () => {
3580
3729
  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)))) {
3581
3730
  return;
3582
3731
  }
@@ -3593,7 +3742,7 @@ const main$h = async () => {
3593
3742
  }
3594
3743
  process.exit(0);
3595
3744
  };
3596
- main$h();
3745
+ main$j();
3597
3746
 
3598
3747
  const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
3599
3748
  process.off("SIGINT", BEFORE_EXIT_FN$6);
@@ -3616,7 +3765,7 @@ const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
3616
3765
  const listenGracefulShutdown$6 = functoolsKit.singleshot(() => {
3617
3766
  process.on("SIGINT", BEFORE_EXIT_FN$6);
3618
3767
  });
3619
- const main$g = async () => {
3768
+ const main$i = async () => {
3620
3769
  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)))) {
3621
3770
  return;
3622
3771
  }
@@ -3634,7 +3783,7 @@ const main$g = async () => {
3634
3783
  await cli.backtestMainService.connect();
3635
3784
  listenGracefulShutdown$6();
3636
3785
  };
3637
- main$g();
3786
+ main$i();
3638
3787
 
3639
3788
  const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3640
3789
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3653,7 +3802,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3653
3802
  const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
3654
3803
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3655
3804
  });
3656
- const main$f = async () => {
3805
+ const main$h = async () => {
3657
3806
  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)))) {
3658
3807
  return;
3659
3808
  }
@@ -3672,7 +3821,7 @@ const main$f = async () => {
3672
3821
  listenGracefulShutdown$5();
3673
3822
  await cli.walkerMainService.connect();
3674
3823
  };
3675
- main$f();
3824
+ main$h();
3676
3825
 
3677
3826
  const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3678
3827
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3694,7 +3843,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3694
3843
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
3695
3844
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3696
3845
  });
3697
- const main$e = async () => {
3846
+ const main$g = async () => {
3698
3847
  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)))) {
3699
3848
  return;
3700
3849
  }
@@ -3708,7 +3857,7 @@ const main$e = async () => {
3708
3857
  await cli.paperMainService.connect();
3709
3858
  listenGracefulShutdown$4();
3710
3859
  };
3711
- main$e();
3860
+ main$g();
3712
3861
 
3713
3862
  const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3714
3863
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3730,7 +3879,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3730
3879
  const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
3731
3880
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3732
3881
  });
3733
- const main$d = async () => {
3882
+ const main$f = async () => {
3734
3883
  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)))) {
3735
3884
  return;
3736
3885
  }
@@ -3744,7 +3893,7 @@ const main$d = async () => {
3744
3893
  await cli.liveMainService.connect();
3745
3894
  listenGracefulShutdown$3();
3746
3895
  };
3747
- main$d();
3896
+ main$f();
3748
3897
 
3749
3898
  const stopBacktestList$1 = async () => {
3750
3899
  for (const item of await BacktestKit.Backtest.list()) {
@@ -3808,7 +3957,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
3808
3957
  const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
3809
3958
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3810
3959
  });
3811
- const main$c = async () => {
3960
+ const main$e = async () => {
3812
3961
  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)))) {
3813
3962
  return;
3814
3963
  }
@@ -3864,7 +4013,7 @@ const main$c = async () => {
3864
4013
  listenGracefulShutdown$2();
3865
4014
  await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
3866
4015
  };
3867
- main$c();
4016
+ main$e();
3868
4017
 
3869
4018
  const MODE_MODULE = {
3870
4019
  backtest: "backtest.module",
@@ -3967,7 +4116,7 @@ const createGracefulShutdown = (mode) => {
3967
4116
  process.on("SIGINT", handler);
3968
4117
  });
3969
4118
  };
3970
- const main$b = async () => {
4119
+ const main$d = async () => {
3971
4120
  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)))) {
3972
4121
  return;
3973
4122
  }
@@ -4043,7 +4192,7 @@ const main$b = async () => {
4043
4192
  }
4044
4193
  await getEntrySubject().next(absolutePath);
4045
4194
  };
4046
- main$b();
4195
+ main$d();
4047
4196
 
4048
4197
  const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
4049
4198
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -4054,7 +4203,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
4054
4203
  const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
4055
4204
  process.on("SIGINT", BEFORE_EXIT_FN$1);
4056
4205
  });
4057
- const main$a = async () => {
4206
+ const main$c = async () => {
4058
4207
  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)))) {
4059
4208
  return;
4060
4209
  }
@@ -4064,7 +4213,7 @@ const main$a = async () => {
4064
4213
  }
4065
4214
  listenGracefulShutdown$1();
4066
4215
  };
4067
- main$a();
4216
+ main$c();
4068
4217
 
4069
4218
  const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
4070
4219
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -4075,7 +4224,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
4075
4224
  const listenGracefulShutdown = functoolsKit.singleshot(() => {
4076
4225
  process.on("SIGINT", BEFORE_EXIT_FN);
4077
4226
  });
4078
- const main$9 = async () => {
4227
+ const main$b = async () => {
4079
4228
  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)))) {
4080
4229
  return;
4081
4230
  }
@@ -4085,7 +4234,7 @@ const main$9 = async () => {
4085
4234
  }
4086
4235
  listenGracefulShutdown();
4087
4236
  };
4088
- main$9();
4237
+ main$b();
4089
4238
 
4090
4239
  const EXTRACT_ROWS_FN = (plots, schema) => {
4091
4240
  const keys = Object.keys(schema);
@@ -4107,7 +4256,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
4107
4256
  }
4108
4257
  return rows;
4109
4258
  };
4110
- const main$8 = async () => {
4259
+ const main$a = async () => {
4111
4260
  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)))) {
4112
4261
  return;
4113
4262
  }
@@ -4207,9 +4356,9 @@ const main$8 = async () => {
4207
4356
  console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
4208
4357
  process.exit(0);
4209
4358
  };
4210
- main$8();
4359
+ main$a();
4211
4360
 
4212
- const main$7 = async () => {
4361
+ const main$9 = async () => {
4213
4362
  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)))) {
4214
4363
  return;
4215
4364
  }
@@ -4272,9 +4421,9 @@ const main$7 = async () => {
4272
4421
  };
4273
4422
  process.on("SIGINT", beforeExit);
4274
4423
  };
4275
- main$7();
4424
+ main$9();
4276
4425
 
4277
- const main$6 = async () => {
4426
+ const main$8 = async () => {
4278
4427
  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)))) {
4279
4428
  return;
4280
4429
  }
@@ -4355,9 +4504,9 @@ const main$6 = async () => {
4355
4504
  console.log(JSON.stringify(candles, null, 2));
4356
4505
  process.exit(0);
4357
4506
  };
4358
- main$6();
4507
+ main$8();
4359
4508
 
4360
- const main$5 = async () => {
4509
+ const main$7 = async () => {
4361
4510
  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)))) {
4362
4511
  return;
4363
4512
  }
@@ -4471,11 +4620,314 @@ const main$5 = async () => {
4471
4620
  }
4472
4621
  process.exit(0);
4473
4622
  };
4474
- main$5();
4623
+ main$7();
4475
4624
 
4476
- const SIMULATOR_NAME = "cli_simulator";
4477
- const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
4478
- const validateIdea = (idea, line) => {
4625
+ const getBroker = functoolsKit.singleshot(() => {
4626
+ // Reaches into BrokerAdapter internals on purpose: the public commit*
4627
+ // methods skip when payload.backtest=true and require enable(), while
4628
+ // brokerdebug must fire exactly one commit at the raw adapter proxy.
4629
+ const broker = BacktestKit.Broker["getInstance"]();
4630
+ if (!broker) {
4631
+ throw new Error("Broker instance is not initialized.");
4632
+ }
4633
+ return broker;
4634
+ });
4635
+ /** Called when a new signal is opened (position entry confirmed). */
4636
+ const commitOrderOpen = functoolsKit.trycatch(async (payload) => {
4637
+ const broker = getBroker();
4638
+ return await broker.onOrderOpenCommit(payload);
4639
+ }, {
4640
+ fallback: (error) => {
4641
+ console.log(error);
4642
+ process.exit(-1);
4643
+ },
4644
+ });
4645
+ /** Called when a new signal is closed (take-profit, stop-loss, or manual close). */
4646
+ const commitOrderClose = functoolsKit.trycatch(async (payload) => {
4647
+ const broker = getBroker();
4648
+ return await broker.onOrderCloseCommit(payload);
4649
+ }, {
4650
+ fallback: (error) => {
4651
+ console.log(error);
4652
+ process.exit(-1);
4653
+ },
4654
+ });
4655
+ /** Called when a partial profit close is committed. */
4656
+ const commitPartialProfit = functoolsKit.trycatch(async (payload) => {
4657
+ const broker = getBroker();
4658
+ return await broker.onPartialProfitCommit(payload);
4659
+ }, {
4660
+ fallback: (error) => {
4661
+ console.log(error);
4662
+ process.exit(-1);
4663
+ },
4664
+ });
4665
+ /** Called when a partial loss close is committed. */
4666
+ const commitPartialLoss = functoolsKit.trycatch(async (payload) => {
4667
+ const broker = getBroker();
4668
+ return await broker.onPartialLossCommit(payload);
4669
+ }, {
4670
+ fallback: (error) => {
4671
+ console.log(error);
4672
+ process.exit(-1);
4673
+ },
4674
+ });
4675
+ /** Called when a trailing stop update is committed. */
4676
+ const commitTrailingStop = functoolsKit.trycatch(async (payload) => {
4677
+ const broker = getBroker();
4678
+ return await broker.onTrailingStopCommit(payload);
4679
+ }, {
4680
+ fallback: (error) => {
4681
+ console.log(error);
4682
+ process.exit(-1);
4683
+ },
4684
+ });
4685
+ /** Called when a trailing take-profit update is committed. */
4686
+ const commitTrailingTake = functoolsKit.trycatch(async (payload) => {
4687
+ const broker = getBroker();
4688
+ return await broker.onTrailingTakeCommit(payload);
4689
+ }, {
4690
+ fallback: (error) => {
4691
+ console.log(error);
4692
+ process.exit(-1);
4693
+ },
4694
+ });
4695
+ /** Called when a breakeven stop is committed (stop loss moved to entry price). */
4696
+ const commitBreakeven = functoolsKit.trycatch(async (payload) => {
4697
+ const broker = getBroker();
4698
+ return await broker.onBreakevenCommit(payload);
4699
+ }, {
4700
+ fallback: (error) => {
4701
+ console.log(error);
4702
+ process.exit(-1);
4703
+ },
4704
+ });
4705
+ /** Called when a DCA (average-buy) entry is committed. */
4706
+ const commitAverageBuy = functoolsKit.trycatch(async (payload) => {
4707
+ const broker = getBroker();
4708
+ return await broker.onAverageBuyCommit(payload);
4709
+ }, {
4710
+ fallback: (error) => {
4711
+ console.log(error);
4712
+ process.exit(-1);
4713
+ },
4714
+ });
4715
+ const COMMITS = [
4716
+ "signal-open",
4717
+ "signal-close",
4718
+ "partial-profit",
4719
+ "partial-loss",
4720
+ "average-buy",
4721
+ "trailing-stop",
4722
+ "trailing-take",
4723
+ "breakeven",
4724
+ ];
4725
+ const ZERO_PNL = {
4726
+ pnlPercentage: 0,
4727
+ priceOpen: 0,
4728
+ priceClose: 0,
4729
+ pnlCost: 0,
4730
+ pnlEntries: 0,
4731
+ };
4732
+ const main$6 = async () => {
4733
+ 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)))) {
4734
+ return;
4735
+ }
4736
+ const { values } = getArgs();
4737
+ if (!values.brokerdebug) {
4738
+ return;
4739
+ }
4740
+ {
4741
+ const cwd = process.cwd();
4742
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
4743
+ }
4744
+ await cli.configConnectionService.loadConfig("setup.config");
4745
+ {
4746
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
4747
+ try {
4748
+ if (typeof loader === "function") {
4749
+ await loader();
4750
+ }
4751
+ if (typeof loader?.loader === "function") {
4752
+ await loader.loader();
4753
+ }
4754
+ }
4755
+ catch (error) {
4756
+ console.error("Module loader failed", error);
4757
+ process.exit(-1);
4758
+ }
4759
+ }
4760
+ await cli.moduleConnectionService.loadModule("brokerdebug.module");
4761
+ const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
4762
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
4763
+ const symbol = values.symbol || "BTCUSDT";
4764
+ const commit = values.commit || "signal-open";
4765
+ if (!COMMITS.includes(commit)) {
4766
+ console.error(`Unknown --commit value: ${commit}`);
4767
+ console.error(`Available: ${COMMITS.join(", ")}`);
4768
+ process.exit(1);
4769
+ }
4770
+ const candles = await BacktestKit.Exchange.getCandles(symbol, "1m", 5, { exchangeName });
4771
+ const lastCandle = candles[candles.length - 1];
4772
+ const currentPrice = lastCandle.close;
4773
+ const priceTakeProfit = currentPrice * 1.02;
4774
+ const priceStopLoss = currentPrice * 0.98;
4775
+ const context = {
4776
+ strategyName: "broker-test",
4777
+ exchangeName: exchangeName,
4778
+ };
4779
+ const signalId = `broker-debug-${Date.now()}`;
4780
+ const when = BacktestKit.alignToInterval(new Date(), "1m");
4781
+ {
4782
+ console.log(`symbol=${symbol} price=${currentPrice} commit=${commit}`);
4783
+ console.time(commit);
4784
+ }
4785
+ const run = async () => {
4786
+ if (commit === "signal-open") {
4787
+ await commitOrderOpen({
4788
+ type: "active",
4789
+ symbol,
4790
+ signalId,
4791
+ cost: 100,
4792
+ position: "long",
4793
+ priceOpen: currentPrice,
4794
+ priceTakeProfit,
4795
+ priceStopLoss,
4796
+ pnl: ZERO_PNL,
4797
+ peakProfit: ZERO_PNL,
4798
+ maxDrawdown: ZERO_PNL,
4799
+ attempt: 0,
4800
+ context,
4801
+ when,
4802
+ backtest: true,
4803
+ });
4804
+ }
4805
+ if (commit === "signal-close") {
4806
+ await commitOrderClose({
4807
+ symbol,
4808
+ signalId,
4809
+ cost: 100,
4810
+ position: "long",
4811
+ currentPrice,
4812
+ priceOpen: currentPrice,
4813
+ priceTakeProfit,
4814
+ priceStopLoss,
4815
+ totalEntries: 1,
4816
+ totalPartials: 0,
4817
+ attempt: 0,
4818
+ pnl: ZERO_PNL,
4819
+ peakProfit: ZERO_PNL,
4820
+ maxDrawdown: ZERO_PNL,
4821
+ context,
4822
+ when,
4823
+ backtest: true,
4824
+ });
4825
+ }
4826
+ if (commit === "partial-profit") {
4827
+ await commitPartialProfit({
4828
+ symbol,
4829
+ signalId,
4830
+ percentToClose: 50,
4831
+ cost: 50,
4832
+ currentPrice,
4833
+ position: "long",
4834
+ priceTakeProfit,
4835
+ priceStopLoss,
4836
+ context,
4837
+ when,
4838
+ backtest: true,
4839
+ });
4840
+ }
4841
+ if (commit === "partial-loss") {
4842
+ await commitPartialLoss({
4843
+ symbol,
4844
+ signalId,
4845
+ percentToClose: 50,
4846
+ cost: 50,
4847
+ currentPrice,
4848
+ position: "long",
4849
+ priceTakeProfit,
4850
+ priceStopLoss,
4851
+ context,
4852
+ when,
4853
+ backtest: true,
4854
+ });
4855
+ }
4856
+ if (commit === "average-buy") {
4857
+ await commitAverageBuy({
4858
+ symbol,
4859
+ signalId,
4860
+ currentPrice,
4861
+ cost: 100,
4862
+ position: "long",
4863
+ priceTakeProfit,
4864
+ priceStopLoss,
4865
+ context,
4866
+ when,
4867
+ backtest: true,
4868
+ });
4869
+ }
4870
+ if (commit === "trailing-stop") {
4871
+ await commitTrailingStop({
4872
+ symbol,
4873
+ signalId,
4874
+ percentShift: -50,
4875
+ currentPrice,
4876
+ newStopLossPrice: priceStopLoss + (currentPrice - priceStopLoss) * 0.5,
4877
+ takeProfitPrice: priceTakeProfit,
4878
+ position: "long",
4879
+ context,
4880
+ when,
4881
+ backtest: true,
4882
+ });
4883
+ }
4884
+ if (commit === "trailing-take") {
4885
+ await commitTrailingTake({
4886
+ symbol,
4887
+ signalId,
4888
+ percentShift: 50,
4889
+ currentPrice,
4890
+ newTakeProfitPrice: priceTakeProfit + (priceTakeProfit - currentPrice) * 0.5,
4891
+ takeProfitPrice: priceTakeProfit,
4892
+ position: "long",
4893
+ context,
4894
+ when,
4895
+ backtest: true,
4896
+ });
4897
+ }
4898
+ if (commit === "breakeven") {
4899
+ await commitBreakeven({
4900
+ symbol,
4901
+ signalId,
4902
+ currentPrice,
4903
+ newStopLossPrice: currentPrice,
4904
+ newTakeProfitPrice: priceTakeProfit,
4905
+ position: "long",
4906
+ context,
4907
+ when,
4908
+ backtest: true,
4909
+ });
4910
+ }
4911
+ };
4912
+ await BacktestKit.runInMockContext(run, {
4913
+ when,
4914
+ strategyName: context.strategyName,
4915
+ exchangeName: context.exchangeName,
4916
+ frameName: "",
4917
+ symbol,
4918
+ backtest: true,
4919
+ });
4920
+ {
4921
+ console.log(`${commit} OK`);
4922
+ console.timeEnd(commit);
4923
+ }
4924
+ process.exit(0);
4925
+ };
4926
+ main$6();
4927
+
4928
+ const SIMULATOR_NAME$1 = "cli_simulator";
4929
+ const IDEA_DIRECTIONS$1 = ["LONG", "SHORT", "NEUTRAL"];
4930
+ const validateIdea$1 = (idea, line) => {
4479
4931
  if (typeof idea !== "object" || idea === null) {
4480
4932
  return `line ${line}: not an object`;
4481
4933
  }
@@ -4488,15 +4940,16 @@ const validateIdea = (idea, line) => {
4488
4940
  if (typeof idea.symbol !== "string" || !idea.symbol) {
4489
4941
  return `line ${line}: "symbol" must be a non-empty string`;
4490
4942
  }
4491
- if (!IDEA_DIRECTIONS.includes(idea.direction)) {
4492
- return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
4943
+ if (!IDEA_DIRECTIONS$1.includes(idea.direction)) {
4944
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS$1.join("|")}, got ${JSON.stringify(idea.direction)}`;
4493
4945
  }
4494
4946
  if (typeof idea.author !== "string" || !idea.author) {
4495
4947
  return `line ${line}: "author" must be a non-empty string`;
4496
4948
  }
4497
4949
  return null;
4498
4950
  };
4499
- const toMarkdown = (result) => {
4951
+ const toMarkdown$1 = (result) => {
4952
+ const profitable = result.reports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
4500
4953
  const lines = [];
4501
4954
  lines.push(`# Simulator Report — ${result.symbol}`);
4502
4955
  lines.push("");
@@ -4505,33 +4958,36 @@ const toMarkdown = (result) => {
4505
4958
  lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
4506
4959
  lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
4507
4960
  lines.push(`| Authors allowed / banned | ${result.allowedAuthors.length} / ${result.bannedAuthors.length} |`);
4508
- lines.push(`| Grid points | ${result.reports.length} |`);
4961
+ lines.push(`| Profitable corridor | ${profitable} / ${result.reports.length} grid points |`);
4509
4962
  lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
4510
4963
  lines.push("");
4511
4964
  lines.push(`## Ranking winners`);
4512
4965
  lines.push("");
4513
- lines.push(`| Criterion | Stop% | Trailing% | Hold | Aligned | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4514
- lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4966
+ lines.push(`| Criterion | Stop% | Hold | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4967
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4515
4968
  for (const best of result.best) {
4516
4969
  if (!best.report) {
4517
- lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — | — | — |`);
4970
+ lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
4518
4971
  continue;
4519
4972
  }
4520
4973
  const { point } = best.report;
4521
- lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.trailingTakePercent} | ${point.holdMinutes / 60}h | ${point.minIdeasAligned} | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
4974
+ lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.holdMinutes / 60}h | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
4522
4975
  `${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
4523
4976
  }
4524
4977
  lines.push("");
4525
- lines.push(`## Allowed authors (production whitelist)`);
4978
+ lines.push(`## Allowed authors (whitelist of the sharpe winner rule)`);
4526
4979
  lines.push("");
4527
4980
  lines.push(`| Author | Ideas | Hits | HitRate |`);
4528
4981
  lines.push(`| --- | --- | --- | --- |`);
4529
- for (const stat of result.authorStats.filter(({ banned }) => !banned)) {
4982
+ // артефакт авторов живёт по-победительно в best[] — ран-левел
4983
+ // authorStats в ISimulatorResult больше нет
4984
+ const sharpeBest = result.best.find(({ criterion }) => criterion === "sharpe");
4985
+ for (const stat of (sharpeBest?.authorStats ?? []).filter(({ banned }) => !banned)) {
4530
4986
  lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
4531
4987
  }
4532
4988
  return lines.join("\n");
4533
4989
  };
4534
- const main$4 = async () => {
4990
+ const main$5 = async () => {
4535
4991
  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)))) {
4536
4992
  return;
4537
4993
  }
@@ -4568,7 +5024,7 @@ const main$4 = async () => {
4568
5024
  console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
4569
5025
  process.exit(1);
4570
5026
  }
4571
- const problem = validateIdea(idea, i + 1);
5027
+ const problem = validateIdea$1(idea, i + 1);
4572
5028
  if (problem) {
4573
5029
  console.error(`Error: ideas file does not match the idea structure — ${problem}`);
4574
5030
  console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
@@ -4604,11 +5060,48 @@ const main$4 = async () => {
4604
5060
  }
4605
5061
  const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
4606
5062
  const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
5063
+ if (values.verbose) {
5064
+ BacktestKit.overrideExchangeSchema({
5065
+ exchangeName,
5066
+ callbacks: {
5067
+ onCandleData(symbol, interval, since) {
5068
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
5069
+ },
5070
+ },
5071
+ });
5072
+ }
4607
5073
  const symbol = values.symbol || "BTCUSDT";
5074
+ // Проба осуществимости, НЕ поиск заработка (подбор параметров — у
5075
+ // --tune): собирающая прибыль механика выключена, остаётся вопрос
5076
+ // "есть ли прибыльный коридор" по стопу x холду x правилу бана —
5077
+ // 48 точек, быстрый вердикт
4608
5078
  BacktestKit.addSimulatorSchema({
4609
- simulatorName: SIMULATOR_NAME,
5079
+ simulatorName: SIMULATOR_NAME$1,
4610
5080
  exchangeName,
5081
+ gridAxes: {
5082
+ // грубая шкала катастрофы: коридор должен быть широким, не точкой
5083
+ hardStopPercent: [2, 3, 5, 7],
5084
+ // инертен: проба не собирает прибыль, выход — по времени или стопу
5085
+ trailingTakePercent: [100],
5086
+ holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
5087
+ // одного проверенного автора достаточно — консенсус не перебираем
5088
+ minIdeasAligned: [1],
5089
+ // правило бана — единственная перебираемая "умность" пробы
5090
+ minAuthorTrack: [3, 5],
5091
+ minAuthorHitRate: [0.5, 0.6],
5092
+ minAuthorWilson: [0],
5093
+ minWeightAligned: [0],
5094
+ profitLockPercent: [0],
5095
+ authorMetric: ["close"],
5096
+ banCriteria: ["sharpe", "pnl"],
5097
+ },
5098
+ reportOrder: "sharpe",
4611
5099
  callbacks: {
5100
+ onProgress: (symbol, stage, processed, total) => {
5101
+ if (values.verbose) {
5102
+ console.log("onProgress", { symbol, stage, processed, total });
5103
+ }
5104
+ },
4612
5105
  onIdeas: (symbol, ideasTotal, ideasDirectional) => {
4613
5106
  if (values.verbose) {
4614
5107
  console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
@@ -4664,7 +5157,7 @@ const main$4 = async () => {
4664
5157
  });
4665
5158
  const result = await BacktestKit.Simulator.run({
4666
5159
  symbol,
4667
- simulatorName: SIMULATOR_NAME,
5160
+ simulatorName: SIMULATOR_NAME$1,
4668
5161
  ideas,
4669
5162
  });
4670
5163
  const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
@@ -4679,11 +5172,299 @@ const main$4 = async () => {
4679
5172
  if (values.markdown) {
4680
5173
  const filePath = path.resolve(dumpDir, `${dumpName}.md`);
4681
5174
  await fs$1.mkdir(dumpDir, { recursive: true });
4682
- await fs$1.writeFile(filePath, toMarkdown(result), "utf-8");
5175
+ await fs$1.writeFile(filePath, toMarkdown$1(result), "utf-8");
4683
5176
  console.log(`Saved: ${filePath}`);
4684
5177
  process.exit(0);
4685
5178
  }
4686
- console.log(toMarkdown(result));
5179
+ console.log(toMarkdown$1(result));
5180
+ process.exit(0);
5181
+ };
5182
+ main$5();
5183
+
5184
+ const SIMULATOR_NAME = "cli_tune";
5185
+ /** Доля времени фида, уходящая в обучение; хвост — один OOS-выстрел. */
5186
+ const DEFAULT_TRAIN_SPLIT = 0.7;
5187
+ const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
5188
+ const validateIdea = (idea, line) => {
5189
+ if (typeof idea !== "object" || idea === null) {
5190
+ return `line ${line}: not an object`;
5191
+ }
5192
+ if (typeof idea.id !== "number") {
5193
+ return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
5194
+ }
5195
+ if (typeof idea.ts !== "number") {
5196
+ return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
5197
+ }
5198
+ if (typeof idea.symbol !== "string" || !idea.symbol) {
5199
+ return `line ${line}: "symbol" must be a non-empty string`;
5200
+ }
5201
+ if (!IDEA_DIRECTIONS.includes(idea.direction)) {
5202
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
5203
+ }
5204
+ if (typeof idea.author !== "string" || !idea.author) {
5205
+ return `line ${line}: "author" must be a non-empty string`;
5206
+ }
5207
+ return null;
5208
+ };
5209
+ const pointLabel = (point) => `H=${point.hardStopPercent} TT=${point.trailingTakePercent} hold=${point.holdMinutes / 60}h N=${point.minIdeasAligned} ` +
5210
+ `track=${point.minAuthorTrack} rate=${point.minAuthorHitRate} wilson=${point.minAuthorWilson} W=${point.minWeightAligned} ` +
5211
+ `lock=${point.profitLockPercent} metric=${point.authorMetric}`;
5212
+ const fmtRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
5213
+ const toMarkdown = (train, test, trainSplit) => {
5214
+ const lines = [];
5215
+ lines.push(`# Tune Report — ${train.symbol}`);
5216
+ lines.push("");
5217
+ lines.push(`Walk-forward: train on the first ${Math.round(trainSplit * 100)}% of the feed time range, ONE out-of-sample shot of the frozen sharpe winner on the tail.`);
5218
+ lines.push("");
5219
+ lines.push(`## Train (${train.ideasDirectional} directional ideas, ${train.reports.length} grid points)`);
5220
+ lines.push("");
5221
+ lines.push(`| Criterion | Point | Trades | PNL% | WinRate | DD% | Sharpe | Sortino |`);
5222
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- |`);
5223
+ for (const best of train.best) {
5224
+ if (!best.report) {
5225
+ lines.push(`| ${best.criterion} | — | — | — | — | — | — | — |`);
5226
+ continue;
5227
+ }
5228
+ lines.push(`| ${best.criterion} | ${pointLabel(best.report.point)} | ${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ` +
5229
+ `${(best.report.winRate * 100).toFixed(0)}% | ${best.report.maxSeriesDrawdownPercent.toFixed(2)} | ${fmtRatio(best.report.sharpe)} | ${fmtRatio(best.report.sortino)} |`);
5230
+ }
5231
+ lines.push("");
5232
+ lines.push(`## Out-of-sample (frozen sharpe winner, ${test.ideasDirectional} tail ideas)`);
5233
+ lines.push("");
5234
+ lines.push(`Frozen point: ${pointLabel(test.point)}`);
5235
+ lines.push("");
5236
+ lines.push(`| Metric | Value |`);
5237
+ lines.push(`| --- | --- |`);
5238
+ lines.push(`| Trades (skipped busy) | ${test.report.trades} (${test.report.skippedBusy}) |`);
5239
+ lines.push(`| PNL | ${test.report.totalPnlPercent.toFixed(2)}% |`);
5240
+ lines.push(`| Win rate | ${(test.report.winRate * 100).toFixed(0)}% |`);
5241
+ lines.push(`| Profit factor | ${fmtRatio(test.report.profitFactor)} |`);
5242
+ lines.push(`| Max series drawdown | ${test.report.maxSeriesDrawdownPercent.toFixed(2)}% |`);
5243
+ lines.push(`| Sharpe / Sortino | ${fmtRatio(test.report.sharpe)} / ${fmtRatio(test.report.sortino)} |`);
5244
+ lines.push(`| Recovery factor | ${fmtRatio(test.report.recoveryFactor)} |`);
5245
+ lines.push(`| Exits | ${Object.entries(test.report.exitReasons).map(([reason, count]) => `${reason}=${count}`).join(", ")} |`);
5246
+ lines.push("");
5247
+ lines.push(`## Test trades`);
5248
+ lines.push("");
5249
+ lines.push(`| Direction | Exit | PNL% | Hold | Entry (UTC) |`);
5250
+ lines.push(`| --- | --- | --- | --- | --- |`);
5251
+ for (const trade of test.trades) {
5252
+ lines.push(`| ${trade.direction} | ${trade.exitReason} | ${trade.pnlPercent.toFixed(2)} | ${trade.holdMinutesActual}m | ${new Date(trade.entryTimestamp).toISOString()} |`);
5253
+ }
5254
+ lines.push("");
5255
+ lines.push(`## Frozen author track record (train range, sharpe winner rule)`);
5256
+ lines.push("");
5257
+ lines.push(`Freeze these raw numbers for production \`Simulator.test\` — banned flags and vote weights are re-derived from them under the frozen point's rule; an author absent from the list is banned by default.`);
5258
+ lines.push("");
5259
+ lines.push(`| Author | Ideas | Hits | Banned |`);
5260
+ lines.push(`| --- | --- | --- | --- |`);
5261
+ for (const stat of test.authorStats) {
5262
+ lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${stat.banned ? "yes" : ""} |`);
5263
+ }
5264
+ return lines.join("\n");
5265
+ };
5266
+ const main$4 = async () => {
5267
+ 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)))) {
5268
+ return;
5269
+ }
5270
+ const { values, positionals } = getArgs();
5271
+ if (!values.tune) {
5272
+ return;
5273
+ }
5274
+ const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
5275
+ if (!ideasPath) {
5276
+ console.error("Error: positional path to an ideas .jsonl file is required");
5277
+ process.exit(1);
5278
+ }
5279
+ let ideas = [];
5280
+ {
5281
+ let content;
5282
+ try {
5283
+ content = await fs$1.readFile(path.resolve(ideasPath), "utf-8");
5284
+ }
5285
+ catch (error) {
5286
+ console.error(`Error: cannot read ideas file: ${ideasPath}`);
5287
+ process.exit(1);
5288
+ }
5289
+ const rows = content.split("\n").filter(Boolean);
5290
+ if (!rows.length) {
5291
+ console.error(`Error: ideas file is empty: ${ideasPath}`);
5292
+ process.exit(1);
5293
+ }
5294
+ for (let i = 0; i < rows.length; i++) {
5295
+ let idea;
5296
+ try {
5297
+ idea = JSON.parse(rows[i]);
5298
+ }
5299
+ catch {
5300
+ console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
5301
+ process.exit(1);
5302
+ }
5303
+ const problem = validateIdea(idea, i + 1);
5304
+ if (problem) {
5305
+ console.error(`Error: ideas file does not match the idea structure — ${problem}`);
5306
+ console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
5307
+ process.exit(1);
5308
+ }
5309
+ ideas.push(idea);
5310
+ }
5311
+ }
5312
+ const trainSplit = values.split
5313
+ ? parseFloat(values.split)
5314
+ : DEFAULT_TRAIN_SPLIT;
5315
+ if (!Number.isFinite(trainSplit) || trainSplit <= 0 || trainSplit >= 1) {
5316
+ console.error(`Error: --split must be a fraction between 0 and 1, got ${values.split}`);
5317
+ process.exit(1);
5318
+ }
5319
+ {
5320
+ const cwd = process.cwd();
5321
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
5322
+ }
5323
+ await cli.configConnectionService.loadConfig("setup.config");
5324
+ {
5325
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
5326
+ try {
5327
+ if (typeof loader === "function") {
5328
+ await loader();
5329
+ }
5330
+ if (typeof loader?.loader === "function") {
5331
+ await loader.loader();
5332
+ }
5333
+ }
5334
+ catch (error) {
5335
+ console.error("Module loader failed", error);
5336
+ process.exit(-1);
5337
+ }
5338
+ }
5339
+ await cli.moduleConnectionService.loadModule("tune.module");
5340
+ {
5341
+ await cli.exchangeSchemaService.addSchema();
5342
+ await cli.symbolSchemaService.addSchema();
5343
+ }
5344
+ const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
5345
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
5346
+ if (values.verbose) {
5347
+ BacktestKit.overrideExchangeSchema({
5348
+ exchangeName,
5349
+ callbacks: {
5350
+ onCandleData(symbol, interval, since) {
5351
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
5352
+ },
5353
+ },
5354
+ });
5355
+ }
5356
+ const symbol = values.symbol || "BTCUSDT";
5357
+ // Подбор параметров: полная сетка с собирающей прибыль механикой —
5358
+ // замок, трейлинг, взвешенный консенсус, Вильсон, обе метрики бана.
5359
+ // Честность обеспечивается сплитом: обучение видит только голову,
5360
+ // хвост тратится на один выстрел замороженного sharpe-победителя
5361
+ BacktestKit.addSimulatorSchema({
5362
+ simulatorName: SIMULATOR_NAME,
5363
+ exchangeName,
5364
+ gridAxes: {
5365
+ // стопы < 2% сидят внутри медианного шейкаута и не выигрывают
5366
+ hardStopPercent: [2, 2.5, 3, 4, 5, 7],
5367
+ trailingTakePercent: [1, 1.5, 2, 3, 4],
5368
+ holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
5369
+ minIdeasAligned: [1, 2],
5370
+ minAuthorTrack: [2, 3, 5],
5371
+ minAuthorHitRate: [0.5, 0.6],
5372
+ minAuthorWilson: [0, 0.6],
5373
+ minWeightAligned: [0, 0.6, 1.2],
5374
+ profitLockPercent: [0, 1.5, 2.5],
5375
+ authorMetric: ["close", "reach"],
5376
+ banCriteria: ["sharpe"],
5377
+ },
5378
+ reportOrder: "sharpe",
5379
+ callbacks: {
5380
+ onProgress: (symbol, stage, processed, total) => {
5381
+ if (values.verbose) {
5382
+ console.log("onProgress", { symbol, stage, processed, total });
5383
+ }
5384
+ },
5385
+ onIdeas: (symbol, ideasTotal, ideasDirectional) => {
5386
+ if (values.verbose) {
5387
+ console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
5388
+ }
5389
+ },
5390
+ onProfiles: (symbol, profiles, truncatedCount) => {
5391
+ if (values.verbose) {
5392
+ console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
5393
+ }
5394
+ },
5395
+ onAuthorsTrained: (symbol, stats, bannedIdeas) => {
5396
+ if (values.verbose) {
5397
+ console.log("onAuthorsTrained", {
5398
+ symbol,
5399
+ authors: stats.length,
5400
+ banned: stats.filter(({ banned }) => banned).length,
5401
+ bannedIdeas,
5402
+ });
5403
+ }
5404
+ },
5405
+ onRanking: (symbol, criterion, _sorted, best) => {
5406
+ if (values.verbose) {
5407
+ console.log("onRanking", {
5408
+ symbol,
5409
+ criterion,
5410
+ point: best.report?.point ?? null,
5411
+ pnl: best.report ? +best.report.totalPnlPercent.toFixed(2) : null,
5412
+ });
5413
+ }
5414
+ },
5415
+ onTestDone: (symbol, result) => {
5416
+ if (values.verbose) {
5417
+ console.log("onTestDone", {
5418
+ symbol,
5419
+ trades: result.report.trades,
5420
+ pnl: +result.report.totalPnlPercent.toFixed(2),
5421
+ });
5422
+ }
5423
+ },
5424
+ },
5425
+ });
5426
+ // обучение видит ТОЛЬКО голову ленты; хвост — один OOS-выстрел
5427
+ const sorted = [...ideas].sort((a, b) => a.ts - b.ts);
5428
+ const cutoff = sorted[0].ts + (sorted[sorted.length - 1].ts - sorted[0].ts) * trainSplit;
5429
+ const trainIdeas = sorted.filter(({ ts }) => ts < cutoff);
5430
+ const testIdeas = sorted.filter(({ ts }) => ts >= cutoff);
5431
+ const train = await BacktestKit.Simulator.run({
5432
+ symbol,
5433
+ simulatorName: SIMULATOR_NAME,
5434
+ ideas: trainIdeas,
5435
+ });
5436
+ const sharpeBest = train.best.find(({ criterion }) => criterion === "sharpe");
5437
+ if (!sharpeBest?.report) {
5438
+ console.error("Error: training produced no sharpe winner — nothing to freeze for the out-of-sample shot");
5439
+ process.exit(1);
5440
+ }
5441
+ // заморозка: точка и трек-рекорд sharpe-победителя как есть — из
5442
+ // замороженных статов test() читает только author/ideas/hits и сам
5443
+ // перевыводит баны и веса под правило точки
5444
+ const test = await BacktestKit.Simulator.test({
5445
+ symbol,
5446
+ simulatorName: SIMULATOR_NAME,
5447
+ ideas: testIdeas,
5448
+ point: sharpeBest.report.point,
5449
+ authorStats: sharpeBest.authorStats,
5450
+ });
5451
+ const dumpName = values.output || `tune_${symbol}_${Date.now()}`;
5452
+ const dumpDir = path.join(process.cwd(), "dump");
5453
+ if (values.json) {
5454
+ const filePath = path.resolve(dumpDir, `${dumpName}.json`);
5455
+ await fs$1.mkdir(dumpDir, { recursive: true });
5456
+ await fs$1.writeFile(filePath, JSON.stringify({ trainSplit, train, test }, null, 2), "utf-8");
5457
+ console.log(`Saved: ${filePath}`);
5458
+ process.exit(0);
5459
+ }
5460
+ if (values.markdown) {
5461
+ const filePath = path.resolve(dumpDir, `${dumpName}.md`);
5462
+ await fs$1.mkdir(dumpDir, { recursive: true });
5463
+ await fs$1.writeFile(filePath, toMarkdown(train, test, trainSplit), "utf-8");
5464
+ console.log(`Saved: ${filePath}`);
5465
+ process.exit(0);
5466
+ }
5467
+ console.log(toMarkdown(train, test, trainSplit));
4687
5468
  process.exit(0);
4688
5469
  };
4689
5470
  main$4();
@@ -4884,7 +5665,8 @@ Modes:
4884
5665
  --dump Fetch and save raw OHLCV candles
4885
5666
  --pnldebug Simulate PnL per minute for a given entry price and direction
4886
5667
  --brokerdebug Fire a single broker commit against the live broker adapter
4887
- --simulator <ideas.jsonl> Sweep exit/entry parameters over crowd ideas, train author whitelist
5668
+ --simulator <ideas.jsonl> Feasibility probe over crowd ideas: is there a profitable corridor at all
5669
+ --tune <ideas.jsonl> Walk-forward parameter search: train on the head, one frozen shot on the tail
4888
5670
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
4889
5671
  --init Scaffold a new project in the current directory
4890
5672
  --docker Scaffold a Docker workspace for running strategies in a container
@@ -5019,19 +5801,47 @@ Simulator flags (--simulator):
5019
5801
  "author": string }. The file is validated BEFORE any work; a line that does not
5020
5802
  match the structure aborts the run with an error.
5021
5803
 
5022
- Runs the backtest-kit Simulator entity: one 5-day candle pass per idea, flood
5023
- dedupe (one idea per author per direction per 8h), default-ban author filter
5024
- (ban thresholds are grid axes), grid evaluation with time-based Sharpe/Sortino,
5025
- three ranking winners. The report includes the production author whitelist.
5804
+ A FEASIBILITY PROBE, not a parameter search (that is --tune): the profit-harvesting
5805
+ machinery is off (no profit lock, inert trailing, no weighted consensus), and a
5806
+ fast 48-point grid of hard stop x hold x ban rule answers one question — does the
5807
+ feed contain a profitable corridor at all. One 5-day candle pass per idea, flood
5808
+ dedupe (one idea per author per direction per 8h), default-ban author filter,
5809
+ time-based Sharpe/Sortino, four ranking winners. The report carries the corridor
5810
+ share and the whitelist of the sharpe winner's ban rule as evidence.
5026
5811
  Ideas of other symbols are filtered out — one shared feed serves any --symbol.
5027
5812
 
5028
5813
  No output flag → print the Markdown summary to stdout. With --verbose every
5029
- simulator lifecycle callback (onIdeas, onProfiles, onAuthorsTrained, onGridPoint,
5030
- onRanking, onDone) is logged to the console as it fires.
5814
+ simulator lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
5815
+ onGridPoint, onRanking, onDone) is logged to the console as it fires.
5031
5816
 
5032
5817
  Module file ./modules/simulator.module is loaded automatically if it exists
5033
5818
  (register your exchange there); without it CCXT Binance is used by default.
5034
5819
 
5820
+ Tune flags (--tune):
5821
+
5822
+ --symbol <string> Trading pair to tune (default: BTCUSDT)
5823
+ --exchange <string> Exchange name (default: first registered)
5824
+ --split <string> Train share of the feed time range, 0..1 (default: 0.7)
5825
+ --output <string> Output file base name (default: tune_{SYMBOL}_{TIMESTAMP})
5826
+ --json Save { trainSplit, train, test } to ./dump/<output>.json
5827
+ --markdown Save walk-forward report to ./dump/<output>.md
5828
+ --verbose Log simulator lifecycle callbacks to the console
5829
+
5830
+ Positional: path to an ideas .jsonl file — same shape and validation as --simulator.
5831
+
5832
+ The PARAMETER SEARCH counterpart of the --simulator probe: the full grid with the
5833
+ profit-harvesting machinery on — profit lock, trailing take, weighted consensus,
5834
+ Wilson-bound ban, both author-hit metrics (close/reach). Honesty is structural:
5835
+ training sees ONLY the head of the feed (--split of its time range), then the
5836
+ sharpe winner is frozen — point and raw author track record — and fired exactly
5837
+ once on the tail via Simulator.test. Nothing is trained on the tail; authors
5838
+ unseen in training are banned by default. The report carries the train winners,
5839
+ the out-of-sample result with the trade list, and the frozen author track record
5840
+ to hardcode for production Simulator.test.
5841
+
5842
+ Module file ./modules/tune.module is loaded automatically if it exists
5843
+ (register your exchange there); without it CCXT Binance is used by default.
5844
+
5035
5845
  Flush flags (--flush):
5036
5846
 
5037
5847
  One or more positional entry points. For each entry point the following
@@ -5065,7 +5875,8 @@ Module hooks (loaded automatically by each mode):
5065
5875
  modules/dump.module --dump Exchange schema for candle dumps
5066
5876
  modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
5067
5877
  modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
5068
- modules/simulator.module --simulator Exchange schema for crowd-ideas simulation
5878
+ modules/simulator.module --simulator Exchange schema for the crowd-ideas feasibility probe
5879
+ modules/tune.module --tune Exchange schema for the walk-forward parameter search
5069
5880
 
5070
5881
  --flush has no associated module. It only removes dump subdirectories.
5071
5882
 
@@ -5094,8 +5905,10 @@ Examples:
5094
5905
  node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
5095
5906
  node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
5096
5907
  node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
5097
- node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/ts-ideas.normalized.jsonl
5098
- node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_sweep ./assets/ts-ideas.normalized.jsonl
5908
+ node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5909
+ node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_probe ./assets/tv-ideas.normalized.jsonl
5910
+ node ${ENTRY_PATH} --tune --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5911
+ node ${ENTRY_PATH} --tune --symbol BTCUSDT --split 0.7 --markdown --output jun_2026_tune ./assets/tv-ideas.normalized.jsonl
5099
5912
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
5100
5913
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
5101
5914
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -5109,7 +5922,7 @@ const main$1 = async () => {
5109
5922
  if (!values.help) {
5110
5923
  return;
5111
5924
  }
5112
- process.stdout.write(`@backtest-kit/cli ${"16.4.0"}\n\n`);
5925
+ process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n\n`);
5113
5926
  process.stdout.write(HELP_TEXT);
5114
5927
  process.exit(0);
5115
5928
  };
@@ -5123,7 +5936,7 @@ const main = async () => {
5123
5936
  if (!values.version) {
5124
5937
  return;
5125
5938
  }
5126
- process.stdout.write(`@backtest-kit/cli ${"16.4.0"}\n`);
5939
+ process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
5127
5940
  process.exit(0);
5128
5941
  };
5129
5942
  main();