@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.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import * as BacktestKit from 'backtest-kit';
3
- import { setConfig, listenExit, emitters, Notification, Cron, Recent, Storage, Markdown, Report, Dump, State, Memory, MarkdownWriter, ReportWriter, PersistSignalAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistPartialAdapter, PersistBreakevenAdapter, PersistCandleAdapter, PersistStorageAdapter, PersistNotificationAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistIntervalAdapter, PersistMemoryAdapter, PersistRecentAdapter, PersistStateAdapter, PersistSessionAdapter, PersistStrategyAdapter, Log, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, RecentLive, RecentBacktest, Broker, SessionLive, SessionBacktest, MemoryLive, MemoryBacktest, StateLive, StateBacktest, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, System, Cache, Interval, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenSync, listenSignalNotify, Exchange, addSimulatorSchema, Simulator } from 'backtest-kit';
4
- import { singleshot, sleep, getErrorMessage, errorData, BehaviorSubject, Subject, str, compose, createAwaiter, execpool, queued, randomString, TIMEOUT_SYMBOL, typo, retry, trycatch, memoize, isObject } from 'functools-kit';
3
+ import { setConfig, listenExit, emitters, Notification, Cron, Recent, Storage, Markdown, Report, Dump, State, Memory, MarkdownWriter, ReportWriter, PersistSignalAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistPartialAdapter, PersistBreakevenAdapter, PersistCandleAdapter, PersistStorageAdapter, PersistNotificationAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistIntervalAdapter, PersistMemoryAdapter, PersistRecentAdapter, PersistStateAdapter, PersistSessionAdapter, PersistStrategyAdapter, Log, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, RecentLive, RecentBacktest, Broker, SessionLive, SessionBacktest, MemoryLive, MemoryBacktest, StateLive, StateBacktest, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, System, Cache, Interval, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenOrderFill, listenOrderReject, listenOrderContinue, listenOrderStop, listenSignalNotify, Exchange, runInMockContext, addSimulatorSchema, Simulator } from 'backtest-kit';
4
+ import { singleshot, sleep, getErrorMessage, errorData, BehaviorSubject, Subject, str, compose, createAwaiter, execpool, queued, randomString, TIMEOUT_SYMBOL, typo, retry, trycatch, ttl, Source, memoize, isObject } from 'functools-kit';
5
5
  import { exec, spawn } from 'child_process';
6
6
  import fs, { constants, realpathSync } from 'fs';
7
7
  import * as stackTrace from 'stack-trace';
@@ -28,7 +28,7 @@ import Module, { createRequire } from 'module';
28
28
  import * as BacktestKitGraph from '@backtest-kit/graph';
29
29
  import * as BacktestKitOllama from '@backtest-kit/ollama';
30
30
  import * as BacktestKitPinets from '@backtest-kit/pinets';
31
- import { run as run$1, Code, toMarkdown as toMarkdown$1 } from '@backtest-kit/pinets';
31
+ import { run as run$1, Code, toMarkdown as toMarkdown$2 } from '@backtest-kit/pinets';
32
32
  import * as BacktestKitSignals from '@backtest-kit/signals';
33
33
  import open from 'open';
34
34
 
@@ -208,14 +208,14 @@ const getReadySubject = singleshot(() => {
208
208
  return new Subject();
209
209
  });
210
210
 
211
- const main$j = () => {
211
+ const main$l = () => {
212
212
  const entrySubject = getEntrySubject();
213
213
  entrySubject.subscribe(async (path) => {
214
214
  console.log("Running", path);
215
215
  await getReadySubject().next();
216
216
  });
217
217
  };
218
- main$j();
218
+ main$l();
219
219
 
220
220
  const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
221
221
  function dumpStackTrace(error) {
@@ -870,6 +870,14 @@ const getArgs = singleshot(() => {
870
870
  type: "boolean",
871
871
  default: false,
872
872
  },
873
+ tune: {
874
+ type: "boolean",
875
+ default: false,
876
+ },
877
+ split: {
878
+ type: "string",
879
+ default: "",
880
+ },
873
881
  commit: {
874
882
  type: "string",
875
883
  default: "",
@@ -2316,6 +2324,13 @@ const STOP_BOT_FN = singleshot(async () => {
2316
2324
  const { stopBot } = await getTelegram();
2317
2325
  stopBot();
2318
2326
  });
2327
+ /**
2328
+ * Throttle window for the per-tick order_continue decision channel: at most one
2329
+ * Telegram notification per signalId per 15 minutes via functools-kit ttl. The
2330
+ * terminal order_stop is NOT throttled (fires once per signal) and clears the
2331
+ * throttle slot for its signalId.
2332
+ */
2333
+ const ORDER_CHECK_NOTIFY_TTL = 15 * 60 * 1000;
2319
2334
  class TelegramLogicService {
2320
2335
  constructor() {
2321
2336
  this.loggerService = inject(TYPES.loggerService);
@@ -2464,11 +2479,68 @@ class TelegramLogicService {
2464
2479
  markdown,
2465
2480
  });
2466
2481
  };
2467
- this.notifySignalOpen = trycatch(async (event) => {
2468
- this.loggerService.log("telegramLogicService notifySignalOpen", {
2482
+ this.notifyOrderFillOpen = trycatch(async (event) => {
2483
+ this.loggerService.log("telegramLogicService notifyOrderFillOpen", {
2484
+ event,
2485
+ });
2486
+ const markdown = await this.telegramTemplateService.getOrderFillOpenMarkdown(event);
2487
+ if (!markdown) {
2488
+ return;
2489
+ }
2490
+ await this.telegramWebService.publishNotify({
2491
+ symbol: event.symbol,
2492
+ markdown,
2493
+ });
2494
+ });
2495
+ this.notifyOrderFillClose = trycatch(async (event) => {
2496
+ this.loggerService.log("telegramLogicService notifyOrderFillClose", {
2497
+ event,
2498
+ });
2499
+ const markdown = await this.telegramTemplateService.getOrderFillCloseMarkdown(event);
2500
+ if (!markdown) {
2501
+ return;
2502
+ }
2503
+ await this.telegramWebService.publishNotify({
2504
+ symbol: event.symbol,
2505
+ markdown,
2506
+ });
2507
+ });
2508
+ this.notifyOrderRejectOpen = trycatch(async (event) => {
2509
+ this.loggerService.log("telegramLogicService notifyOrderRejectOpen", {
2510
+ event,
2511
+ });
2512
+ const markdown = await this.telegramTemplateService.getOrderRejectOpenMarkdown(event);
2513
+ if (!markdown) {
2514
+ return;
2515
+ }
2516
+ await this.telegramWebService.publishNotify({
2517
+ symbol: event.symbol,
2518
+ markdown,
2519
+ });
2520
+ });
2521
+ this.notifyOrderRejectClose = trycatch(async (event) => {
2522
+ this.loggerService.log("telegramLogicService notifyOrderRejectClose", {
2523
+ event,
2524
+ });
2525
+ const markdown = await this.telegramTemplateService.getOrderRejectCloseMarkdown(event);
2526
+ if (!markdown) {
2527
+ return;
2528
+ }
2529
+ await this.telegramWebService.publishNotify({
2530
+ symbol: event.symbol,
2531
+ markdown,
2532
+ });
2533
+ });
2534
+ /**
2535
+ * Post-verdict order-check CONTINUE (order_continue). Fires every live tick the
2536
+ * order survives the check, so it is throttled via functools-kit ttl:
2537
+ * one message per signalId per ORDER_CHECK_NOTIFY_TTL (15 minutes).
2538
+ */
2539
+ this.notifyOrderContinue = ttl(trycatch(async (event) => {
2540
+ this.loggerService.log("telegramLogicService notifyOrderContinue", {
2469
2541
  event,
2470
2542
  });
2471
- const markdown = await this.telegramTemplateService.getSignalOpenMarkdown(event);
2543
+ const markdown = await this.telegramTemplateService.getOrderContinueMarkdown(event);
2472
2544
  if (!markdown) {
2473
2545
  return;
2474
2546
  }
@@ -2476,12 +2548,21 @@ class TelegramLogicService {
2476
2548
  symbol: event.symbol,
2477
2549
  markdown,
2478
2550
  });
2551
+ }), {
2552
+ key: ([event]) => event.signalId,
2553
+ timeout: ORDER_CHECK_NOTIFY_TTL,
2479
2554
  });
2480
- this.notifySignalClose = trycatch(async (event) => {
2481
- this.loggerService.log("telegramLogicService notifySignalClose", {
2555
+ /**
2556
+ * Post-verdict order-check STOP (order_stop). Terminal — fires once per signal,
2557
+ * so no throttle; drops the continue throttle slot for this signalId
2558
+ * (the signal leaves monitoring, the ttl entry would only leak otherwise).
2559
+ */
2560
+ this.notifyOrderStop = trycatch(async (event) => {
2561
+ this.loggerService.log("telegramLogicService notifyOrderStop", {
2482
2562
  event,
2483
2563
  });
2484
- const markdown = await this.telegramTemplateService.getSignalCloseMarkdown(event);
2564
+ this.notifyOrderContinue.clear(event.signalId);
2565
+ const markdown = await this.telegramTemplateService.getOrderStopMarkdown(event);
2485
2566
  if (!markdown) {
2486
2567
  return;
2487
2568
  }
@@ -2586,26 +2667,54 @@ class TelegramLogicService {
2586
2667
  return;
2587
2668
  }
2588
2669
  });
2589
- const unSync = listenSync(async (event) => {
2670
+ // Broker-CONFIRMED fills only (post-verdict orderFillSubject): a rejected or
2671
+ // transient gate attempt never reaches this channel, so "Order Filled" is
2672
+ // guaranteed truthful. The pre-verdict syncSubject is NOT consumed here.
2673
+ const unOrderFill = listenOrderFill(async (event) => {
2590
2674
  if (event.action === "signal-open") {
2591
- // type "schedule" is a resting-order PLACEMENT, not a position open:
2592
- // the user already got the "scheduled" notification from listenSignal
2593
- if (event.type !== "active") {
2594
- return;
2595
- }
2596
- await this.notifySignalOpen(event);
2675
+ // Both open outcomes notify: type "active" — the order opened at once
2676
+ // (immediate fill, no schedule phase), type "schedule" — the resting
2677
+ // order was placed on the exchange. Either way the broker confirmed a
2678
+ // real order; the template's Order field carries the type.
2679
+ await this.notifyOrderFillOpen(event);
2680
+ return;
2681
+ }
2682
+ if (event.action === "signal-close") {
2683
+ await this.notifyOrderFillClose(event);
2684
+ return;
2685
+ }
2686
+ });
2687
+ // Terminal broker rejection (post-verdict orderRejectSubject): exactly one
2688
+ // event per dropped order attempt — the engine consumes the rejected signal
2689
+ // id, so this cannot repeat per-tick for the same signal. Dispatched by
2690
+ // action to the dedicated open/close notifications (single responsibility).
2691
+ const unOrderReject = listenOrderReject(async (event) => {
2692
+ if (event.action === "signal-open") {
2693
+ await this.notifyOrderRejectOpen(event);
2597
2694
  return;
2598
2695
  }
2599
2696
  if (event.action === "signal-close") {
2600
- await this.notifySignalClose(event);
2697
+ await this.notifyOrderRejectClose(event);
2601
2698
  return;
2602
2699
  }
2603
2700
  });
2701
+ // Post-verdict check decisions: CONTINUE fires every surviving tick and is
2702
+ // throttled to one message per signalId per 15 minutes via ttl; terminal
2703
+ // STOP fires once per signal, unthrottled.
2704
+ const unOrderContinue = listenOrderContinue(async (event) => {
2705
+ await this.notifyOrderContinue(event);
2706
+ });
2707
+ const unOrderStop = listenOrderStop(async (event) => {
2708
+ await this.notifyOrderStop(event);
2709
+ });
2604
2710
  const unSignalNotify = listenSignalNotify(async (event) => {
2605
2711
  await this.notifySignalInfo(event);
2606
2712
  });
2713
+ const unGc = Source.fromInterval(90000).connect(() => {
2714
+ this.notifyOrderContinue.gc();
2715
+ });
2607
2716
  const unConnect = () => this.connect.clear();
2608
- const unListen = compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unSync(), () => unSignalNotify(), () => unConnect());
2717
+ const unListen = compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unGc(), () => unSignalNotify(), () => unConnect());
2609
2718
  return () => {
2610
2719
  STOP_BOT_FN();
2611
2720
  unListen();
@@ -2758,25 +2867,65 @@ class TelegramTemplateService {
2758
2867
  }
2759
2868
  return await RENDER_TEMPLATE_FN("average-buy.mustache", event, this);
2760
2869
  };
2761
- this.getSignalOpenMarkdown = async (event) => {
2762
- this.loggerService.log("telegramTemplateService getSignalOpenMarkdown", {
2870
+ this.getOrderFillOpenMarkdown = async (event) => {
2871
+ this.loggerService.log("telegramTemplateService getOrderFillOpenMarkdown", {
2872
+ event,
2873
+ });
2874
+ const adapter = await this.getTelegramAdapter();
2875
+ if (adapter?.getOrderFillOpenMarkdown) {
2876
+ return await adapter.getOrderFillOpenMarkdown(event);
2877
+ }
2878
+ return await RENDER_TEMPLATE_FN("order-fill-open.mustache", event, this);
2879
+ };
2880
+ this.getOrderFillCloseMarkdown = async (event) => {
2881
+ this.loggerService.log("telegramTemplateService getOrderFillCloseMarkdown", {
2882
+ event,
2883
+ });
2884
+ const adapter = await this.getTelegramAdapter();
2885
+ if (adapter?.getOrderFillCloseMarkdown) {
2886
+ return await adapter.getOrderFillCloseMarkdown(event);
2887
+ }
2888
+ return await RENDER_TEMPLATE_FN("order-fill-close.mustache", event, this);
2889
+ };
2890
+ this.getOrderRejectOpenMarkdown = async (event) => {
2891
+ this.loggerService.log("telegramTemplateService getOrderRejectOpenMarkdown", {
2892
+ event,
2893
+ });
2894
+ const adapter = await this.getTelegramAdapter();
2895
+ if (adapter?.getOrderRejectOpenMarkdown) {
2896
+ return await adapter.getOrderRejectOpenMarkdown(event);
2897
+ }
2898
+ return await RENDER_TEMPLATE_FN("order-reject-open.mustache", event, this);
2899
+ };
2900
+ this.getOrderRejectCloseMarkdown = async (event) => {
2901
+ this.loggerService.log("telegramTemplateService getOrderRejectCloseMarkdown", {
2902
+ event,
2903
+ });
2904
+ const adapter = await this.getTelegramAdapter();
2905
+ if (adapter?.getOrderRejectCloseMarkdown) {
2906
+ return await adapter.getOrderRejectCloseMarkdown(event);
2907
+ }
2908
+ return await RENDER_TEMPLATE_FN("order-reject-close.mustache", event, this);
2909
+ };
2910
+ this.getOrderContinueMarkdown = async (event) => {
2911
+ this.loggerService.log("telegramTemplateService getOrderContinueMarkdown", {
2763
2912
  event,
2764
2913
  });
2765
2914
  const adapter = await this.getTelegramAdapter();
2766
- if (adapter?.getSignalOpenMarkdown) {
2767
- return await adapter.getSignalOpenMarkdown(event);
2915
+ if (adapter?.getOrderContinueMarkdown) {
2916
+ return await adapter.getOrderContinueMarkdown(event);
2768
2917
  }
2769
- return await RENDER_TEMPLATE_FN("signal-open.mustache", event, this);
2918
+ return await RENDER_TEMPLATE_FN("order-continue.mustache", event, this);
2770
2919
  };
2771
- this.getSignalCloseMarkdown = async (event) => {
2772
- this.loggerService.log("telegramTemplateService getSignalCloseMarkdown", {
2920
+ this.getOrderStopMarkdown = async (event) => {
2921
+ this.loggerService.log("telegramTemplateService getOrderStopMarkdown", {
2773
2922
  event,
2774
2923
  });
2775
2924
  const adapter = await this.getTelegramAdapter();
2776
- if (adapter?.getSignalCloseMarkdown) {
2777
- return await adapter.getSignalCloseMarkdown(event);
2925
+ if (adapter?.getOrderStopMarkdown) {
2926
+ return await adapter.getOrderStopMarkdown(event);
2778
2927
  }
2779
- return await RENDER_TEMPLATE_FN("signal-close.mustache", event, this);
2928
+ return await RENDER_TEMPLATE_FN("order-stop.mustache", event, this);
2780
2929
  };
2781
2930
  this.getCancelScheduledMarkdown = async (event) => {
2782
2931
  this.loggerService.log("telegramTemplateService getCancelScheduledMarkdown", {
@@ -3506,14 +3655,14 @@ const cli = {
3506
3655
  };
3507
3656
  init();
3508
3657
 
3509
- const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "flush", "init", "docker", "help", "version"];
3658
+ const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "tune", "flush", "init", "docker", "help", "version"];
3510
3659
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
3511
3660
  const HELP_TEXT$1 = `
3512
3661
  Example:
3513
3662
 
3514
3663
  node ${ENTRY_PATH$1} --help
3515
3664
  `.trimStart();
3516
- const main$i = async () => {
3665
+ const main$k = async () => {
3517
3666
  if (!getEntry(import.meta.url)) {
3518
3667
  return;
3519
3668
  }
@@ -3521,14 +3670,14 @@ const main$i = async () => {
3521
3670
  if (MODES.some((mode) => values[mode])) {
3522
3671
  return;
3523
3672
  }
3524
- process.stdout.write(`@backtest-kit/cli ${"16.4.0"}\n`);
3673
+ process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
3525
3674
  process.stdout.write("\n");
3526
3675
  process.stdout.write(`Run with --help to see available commands.\n`);
3527
3676
  process.stdout.write("\n");
3528
3677
  process.stdout.write(HELP_TEXT$1);
3529
3678
  process.exit(0);
3530
3679
  };
3531
- main$i();
3680
+ main$k();
3532
3681
 
3533
3682
  const notifyShutdown = singleshot(async () => {
3534
3683
  console.log("Graceful shutdown initiated.");
@@ -3544,7 +3693,7 @@ const flush = async (entryPoint) => {
3544
3693
  console.log(`Removed: ${target}`);
3545
3694
  }
3546
3695
  };
3547
- const main$h = async () => {
3696
+ const main$j = async () => {
3548
3697
  if (!getEntry(import.meta.url)) {
3549
3698
  return;
3550
3699
  }
@@ -3561,7 +3710,7 @@ const main$h = async () => {
3561
3710
  }
3562
3711
  process.exit(0);
3563
3712
  };
3564
- main$h();
3713
+ main$j();
3565
3714
 
3566
3715
  const BEFORE_EXIT_FN$6 = singleshot(async () => {
3567
3716
  process.off("SIGINT", BEFORE_EXIT_FN$6);
@@ -3584,7 +3733,7 @@ const BEFORE_EXIT_FN$6 = singleshot(async () => {
3584
3733
  const listenGracefulShutdown$6 = singleshot(() => {
3585
3734
  process.on("SIGINT", BEFORE_EXIT_FN$6);
3586
3735
  });
3587
- const main$g = async () => {
3736
+ const main$i = async () => {
3588
3737
  if (!getEntry(import.meta.url)) {
3589
3738
  return;
3590
3739
  }
@@ -3602,7 +3751,7 @@ const main$g = async () => {
3602
3751
  await cli.backtestMainService.connect();
3603
3752
  listenGracefulShutdown$6();
3604
3753
  };
3605
- main$g();
3754
+ main$i();
3606
3755
 
3607
3756
  const BEFORE_EXIT_FN$5 = singleshot(async () => {
3608
3757
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3621,7 +3770,7 @@ const BEFORE_EXIT_FN$5 = singleshot(async () => {
3621
3770
  const listenGracefulShutdown$5 = singleshot(() => {
3622
3771
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3623
3772
  });
3624
- const main$f = async () => {
3773
+ const main$h = async () => {
3625
3774
  if (!getEntry(import.meta.url)) {
3626
3775
  return;
3627
3776
  }
@@ -3640,7 +3789,7 @@ const main$f = async () => {
3640
3789
  listenGracefulShutdown$5();
3641
3790
  await cli.walkerMainService.connect();
3642
3791
  };
3643
- main$f();
3792
+ main$h();
3644
3793
 
3645
3794
  const BEFORE_EXIT_FN$4 = singleshot(async () => {
3646
3795
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3662,7 +3811,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
3662
3811
  const listenGracefulShutdown$4 = singleshot(() => {
3663
3812
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3664
3813
  });
3665
- const main$e = async () => {
3814
+ const main$g = async () => {
3666
3815
  if (!getEntry(import.meta.url)) {
3667
3816
  return;
3668
3817
  }
@@ -3676,7 +3825,7 @@ const main$e = async () => {
3676
3825
  await cli.paperMainService.connect();
3677
3826
  listenGracefulShutdown$4();
3678
3827
  };
3679
- main$e();
3828
+ main$g();
3680
3829
 
3681
3830
  const BEFORE_EXIT_FN$3 = singleshot(async () => {
3682
3831
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3698,7 +3847,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
3698
3847
  const listenGracefulShutdown$3 = singleshot(() => {
3699
3848
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3700
3849
  });
3701
- const main$d = async () => {
3850
+ const main$f = async () => {
3702
3851
  if (!getEntry(import.meta.url)) {
3703
3852
  return;
3704
3853
  }
@@ -3712,7 +3861,7 @@ const main$d = async () => {
3712
3861
  await cli.liveMainService.connect();
3713
3862
  listenGracefulShutdown$3();
3714
3863
  };
3715
- main$d();
3864
+ main$f();
3716
3865
 
3717
3866
  const stopBacktestList$1 = async () => {
3718
3867
  for (const item of await Backtest.list()) {
@@ -3776,7 +3925,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
3776
3925
  const listenGracefulShutdown$2 = singleshot(() => {
3777
3926
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3778
3927
  });
3779
- const main$c = async () => {
3928
+ const main$e = async () => {
3780
3929
  if (!getEntry(import.meta.url)) {
3781
3930
  return;
3782
3931
  }
@@ -3832,7 +3981,7 @@ const main$c = async () => {
3832
3981
  listenGracefulShutdown$2();
3833
3982
  await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
3834
3983
  };
3835
- main$c();
3984
+ main$e();
3836
3985
 
3837
3986
  const MODE_MODULE = {
3838
3987
  backtest: "backtest.module",
@@ -3935,7 +4084,7 @@ const createGracefulShutdown = (mode) => {
3935
4084
  process.on("SIGINT", handler);
3936
4085
  });
3937
4086
  };
3938
- const main$b = async () => {
4087
+ const main$d = async () => {
3939
4088
  if (!getEntry(import.meta.url)) {
3940
4089
  return;
3941
4090
  }
@@ -4011,7 +4160,7 @@ const main$b = async () => {
4011
4160
  }
4012
4161
  await getEntrySubject().next(absolutePath);
4013
4162
  };
4014
- main$b();
4163
+ main$d();
4015
4164
 
4016
4165
  const BEFORE_EXIT_FN$1 = singleshot(async () => {
4017
4166
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -4022,7 +4171,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
4022
4171
  const listenGracefulShutdown$1 = singleshot(() => {
4023
4172
  process.on("SIGINT", BEFORE_EXIT_FN$1);
4024
4173
  });
4025
- const main$a = async () => {
4174
+ const main$c = async () => {
4026
4175
  if (!getEntry(import.meta.url)) {
4027
4176
  return;
4028
4177
  }
@@ -4032,7 +4181,7 @@ const main$a = async () => {
4032
4181
  }
4033
4182
  listenGracefulShutdown$1();
4034
4183
  };
4035
- main$a();
4184
+ main$c();
4036
4185
 
4037
4186
  const BEFORE_EXIT_FN = singleshot(async () => {
4038
4187
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -4043,7 +4192,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
4043
4192
  const listenGracefulShutdown = singleshot(() => {
4044
4193
  process.on("SIGINT", BEFORE_EXIT_FN);
4045
4194
  });
4046
- const main$9 = async () => {
4195
+ const main$b = async () => {
4047
4196
  if (!getEntry(import.meta.url)) {
4048
4197
  return;
4049
4198
  }
@@ -4053,7 +4202,7 @@ const main$9 = async () => {
4053
4202
  }
4054
4203
  listenGracefulShutdown();
4055
4204
  };
4056
- main$9();
4205
+ main$b();
4057
4206
 
4058
4207
  const EXTRACT_ROWS_FN = (plots, schema) => {
4059
4208
  const keys = Object.keys(schema);
@@ -4075,7 +4224,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
4075
4224
  }
4076
4225
  return rows;
4077
4226
  };
4078
- const main$8 = async () => {
4227
+ const main$a = async () => {
4079
4228
  if (!getEntry(import.meta.url)) {
4080
4229
  return;
4081
4230
  }
@@ -4167,17 +4316,17 @@ const main$8 = async () => {
4167
4316
  if (values.markdown) {
4168
4317
  const filePath = resolve(dumpDir, `${dumpName}.md`);
4169
4318
  await mkdir(dumpDir, { recursive: true });
4170
- await writeFile(filePath, await toMarkdown$1(signalId, plots, signalSchema), "utf-8");
4319
+ await writeFile(filePath, await toMarkdown$2(signalId, plots, signalSchema), "utf-8");
4171
4320
  console.log(`Saved: ${filePath}`);
4172
4321
  process.exit(0);
4173
4322
  return;
4174
4323
  }
4175
- console.log(await toMarkdown$1(signalId, plots, signalSchema));
4324
+ console.log(await toMarkdown$2(signalId, plots, signalSchema));
4176
4325
  process.exit(0);
4177
4326
  };
4178
- main$8();
4327
+ main$a();
4179
4328
 
4180
- const main$7 = async () => {
4329
+ const main$9 = async () => {
4181
4330
  if (!getEntry(import.meta.url)) {
4182
4331
  return;
4183
4332
  }
@@ -4240,9 +4389,9 @@ const main$7 = async () => {
4240
4389
  };
4241
4390
  process.on("SIGINT", beforeExit);
4242
4391
  };
4243
- main$7();
4392
+ main$9();
4244
4393
 
4245
- const main$6 = async () => {
4394
+ const main$8 = async () => {
4246
4395
  if (!getEntry(import.meta.url)) {
4247
4396
  return;
4248
4397
  }
@@ -4323,9 +4472,9 @@ const main$6 = async () => {
4323
4472
  console.log(JSON.stringify(candles, null, 2));
4324
4473
  process.exit(0);
4325
4474
  };
4326
- main$6();
4475
+ main$8();
4327
4476
 
4328
- const main$5 = async () => {
4477
+ const main$7 = async () => {
4329
4478
  if (!getEntry(import.meta.url)) {
4330
4479
  return;
4331
4480
  }
@@ -4439,11 +4588,314 @@ const main$5 = async () => {
4439
4588
  }
4440
4589
  process.exit(0);
4441
4590
  };
4442
- main$5();
4591
+ main$7();
4443
4592
 
4444
- const SIMULATOR_NAME = "cli_simulator";
4445
- const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
4446
- const validateIdea = (idea, line) => {
4593
+ const getBroker = singleshot(() => {
4594
+ // Reaches into BrokerAdapter internals on purpose: the public commit*
4595
+ // methods skip when payload.backtest=true and require enable(), while
4596
+ // brokerdebug must fire exactly one commit at the raw adapter proxy.
4597
+ const broker = Broker["getInstance"]();
4598
+ if (!broker) {
4599
+ throw new Error("Broker instance is not initialized.");
4600
+ }
4601
+ return broker;
4602
+ });
4603
+ /** Called when a new signal is opened (position entry confirmed). */
4604
+ const commitOrderOpen = trycatch(async (payload) => {
4605
+ const broker = getBroker();
4606
+ return await broker.onOrderOpenCommit(payload);
4607
+ }, {
4608
+ fallback: (error) => {
4609
+ console.log(error);
4610
+ process.exit(-1);
4611
+ },
4612
+ });
4613
+ /** Called when a new signal is closed (take-profit, stop-loss, or manual close). */
4614
+ const commitOrderClose = trycatch(async (payload) => {
4615
+ const broker = getBroker();
4616
+ return await broker.onOrderCloseCommit(payload);
4617
+ }, {
4618
+ fallback: (error) => {
4619
+ console.log(error);
4620
+ process.exit(-1);
4621
+ },
4622
+ });
4623
+ /** Called when a partial profit close is committed. */
4624
+ const commitPartialProfit = trycatch(async (payload) => {
4625
+ const broker = getBroker();
4626
+ return await broker.onPartialProfitCommit(payload);
4627
+ }, {
4628
+ fallback: (error) => {
4629
+ console.log(error);
4630
+ process.exit(-1);
4631
+ },
4632
+ });
4633
+ /** Called when a partial loss close is committed. */
4634
+ const commitPartialLoss = trycatch(async (payload) => {
4635
+ const broker = getBroker();
4636
+ return await broker.onPartialLossCommit(payload);
4637
+ }, {
4638
+ fallback: (error) => {
4639
+ console.log(error);
4640
+ process.exit(-1);
4641
+ },
4642
+ });
4643
+ /** Called when a trailing stop update is committed. */
4644
+ const commitTrailingStop = trycatch(async (payload) => {
4645
+ const broker = getBroker();
4646
+ return await broker.onTrailingStopCommit(payload);
4647
+ }, {
4648
+ fallback: (error) => {
4649
+ console.log(error);
4650
+ process.exit(-1);
4651
+ },
4652
+ });
4653
+ /** Called when a trailing take-profit update is committed. */
4654
+ const commitTrailingTake = trycatch(async (payload) => {
4655
+ const broker = getBroker();
4656
+ return await broker.onTrailingTakeCommit(payload);
4657
+ }, {
4658
+ fallback: (error) => {
4659
+ console.log(error);
4660
+ process.exit(-1);
4661
+ },
4662
+ });
4663
+ /** Called when a breakeven stop is committed (stop loss moved to entry price). */
4664
+ const commitBreakeven = trycatch(async (payload) => {
4665
+ const broker = getBroker();
4666
+ return await broker.onBreakevenCommit(payload);
4667
+ }, {
4668
+ fallback: (error) => {
4669
+ console.log(error);
4670
+ process.exit(-1);
4671
+ },
4672
+ });
4673
+ /** Called when a DCA (average-buy) entry is committed. */
4674
+ const commitAverageBuy = trycatch(async (payload) => {
4675
+ const broker = getBroker();
4676
+ return await broker.onAverageBuyCommit(payload);
4677
+ }, {
4678
+ fallback: (error) => {
4679
+ console.log(error);
4680
+ process.exit(-1);
4681
+ },
4682
+ });
4683
+ const COMMITS = [
4684
+ "signal-open",
4685
+ "signal-close",
4686
+ "partial-profit",
4687
+ "partial-loss",
4688
+ "average-buy",
4689
+ "trailing-stop",
4690
+ "trailing-take",
4691
+ "breakeven",
4692
+ ];
4693
+ const ZERO_PNL = {
4694
+ pnlPercentage: 0,
4695
+ priceOpen: 0,
4696
+ priceClose: 0,
4697
+ pnlCost: 0,
4698
+ pnlEntries: 0,
4699
+ };
4700
+ const main$6 = async () => {
4701
+ if (!getEntry(import.meta.url)) {
4702
+ return;
4703
+ }
4704
+ const { values } = getArgs();
4705
+ if (!values.brokerdebug) {
4706
+ return;
4707
+ }
4708
+ {
4709
+ const cwd = process.cwd();
4710
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
4711
+ }
4712
+ await cli.configConnectionService.loadConfig("setup.config");
4713
+ {
4714
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
4715
+ try {
4716
+ if (typeof loader === "function") {
4717
+ await loader();
4718
+ }
4719
+ if (typeof loader?.loader === "function") {
4720
+ await loader.loader();
4721
+ }
4722
+ }
4723
+ catch (error) {
4724
+ console.error("Module loader failed", error);
4725
+ process.exit(-1);
4726
+ }
4727
+ }
4728
+ await cli.moduleConnectionService.loadModule("brokerdebug.module");
4729
+ const [defaultExchangeName = null] = await listExchangeSchema();
4730
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
4731
+ const symbol = values.symbol || "BTCUSDT";
4732
+ const commit = values.commit || "signal-open";
4733
+ if (!COMMITS.includes(commit)) {
4734
+ console.error(`Unknown --commit value: ${commit}`);
4735
+ console.error(`Available: ${COMMITS.join(", ")}`);
4736
+ process.exit(1);
4737
+ }
4738
+ const candles = await Exchange.getCandles(symbol, "1m", 5, { exchangeName });
4739
+ const lastCandle = candles[candles.length - 1];
4740
+ const currentPrice = lastCandle.close;
4741
+ const priceTakeProfit = currentPrice * 1.02;
4742
+ const priceStopLoss = currentPrice * 0.98;
4743
+ const context = {
4744
+ strategyName: "broker-test",
4745
+ exchangeName: exchangeName,
4746
+ };
4747
+ const signalId = `broker-debug-${Date.now()}`;
4748
+ const when = alignToInterval(new Date(), "1m");
4749
+ {
4750
+ console.log(`symbol=${symbol} price=${currentPrice} commit=${commit}`);
4751
+ console.time(commit);
4752
+ }
4753
+ const run = async () => {
4754
+ if (commit === "signal-open") {
4755
+ await commitOrderOpen({
4756
+ type: "active",
4757
+ symbol,
4758
+ signalId,
4759
+ cost: 100,
4760
+ position: "long",
4761
+ priceOpen: currentPrice,
4762
+ priceTakeProfit,
4763
+ priceStopLoss,
4764
+ pnl: ZERO_PNL,
4765
+ peakProfit: ZERO_PNL,
4766
+ maxDrawdown: ZERO_PNL,
4767
+ attempt: 0,
4768
+ context,
4769
+ when,
4770
+ backtest: true,
4771
+ });
4772
+ }
4773
+ if (commit === "signal-close") {
4774
+ await commitOrderClose({
4775
+ symbol,
4776
+ signalId,
4777
+ cost: 100,
4778
+ position: "long",
4779
+ currentPrice,
4780
+ priceOpen: currentPrice,
4781
+ priceTakeProfit,
4782
+ priceStopLoss,
4783
+ totalEntries: 1,
4784
+ totalPartials: 0,
4785
+ attempt: 0,
4786
+ pnl: ZERO_PNL,
4787
+ peakProfit: ZERO_PNL,
4788
+ maxDrawdown: ZERO_PNL,
4789
+ context,
4790
+ when,
4791
+ backtest: true,
4792
+ });
4793
+ }
4794
+ if (commit === "partial-profit") {
4795
+ await commitPartialProfit({
4796
+ symbol,
4797
+ signalId,
4798
+ percentToClose: 50,
4799
+ cost: 50,
4800
+ currentPrice,
4801
+ position: "long",
4802
+ priceTakeProfit,
4803
+ priceStopLoss,
4804
+ context,
4805
+ when,
4806
+ backtest: true,
4807
+ });
4808
+ }
4809
+ if (commit === "partial-loss") {
4810
+ await commitPartialLoss({
4811
+ symbol,
4812
+ signalId,
4813
+ percentToClose: 50,
4814
+ cost: 50,
4815
+ currentPrice,
4816
+ position: "long",
4817
+ priceTakeProfit,
4818
+ priceStopLoss,
4819
+ context,
4820
+ when,
4821
+ backtest: true,
4822
+ });
4823
+ }
4824
+ if (commit === "average-buy") {
4825
+ await commitAverageBuy({
4826
+ symbol,
4827
+ signalId,
4828
+ currentPrice,
4829
+ cost: 100,
4830
+ position: "long",
4831
+ priceTakeProfit,
4832
+ priceStopLoss,
4833
+ context,
4834
+ when,
4835
+ backtest: true,
4836
+ });
4837
+ }
4838
+ if (commit === "trailing-stop") {
4839
+ await commitTrailingStop({
4840
+ symbol,
4841
+ signalId,
4842
+ percentShift: -50,
4843
+ currentPrice,
4844
+ newStopLossPrice: priceStopLoss + (currentPrice - priceStopLoss) * 0.5,
4845
+ takeProfitPrice: priceTakeProfit,
4846
+ position: "long",
4847
+ context,
4848
+ when,
4849
+ backtest: true,
4850
+ });
4851
+ }
4852
+ if (commit === "trailing-take") {
4853
+ await commitTrailingTake({
4854
+ symbol,
4855
+ signalId,
4856
+ percentShift: 50,
4857
+ currentPrice,
4858
+ newTakeProfitPrice: priceTakeProfit + (priceTakeProfit - currentPrice) * 0.5,
4859
+ takeProfitPrice: priceTakeProfit,
4860
+ position: "long",
4861
+ context,
4862
+ when,
4863
+ backtest: true,
4864
+ });
4865
+ }
4866
+ if (commit === "breakeven") {
4867
+ await commitBreakeven({
4868
+ symbol,
4869
+ signalId,
4870
+ currentPrice,
4871
+ newStopLossPrice: currentPrice,
4872
+ newTakeProfitPrice: priceTakeProfit,
4873
+ position: "long",
4874
+ context,
4875
+ when,
4876
+ backtest: true,
4877
+ });
4878
+ }
4879
+ };
4880
+ await runInMockContext(run, {
4881
+ when,
4882
+ strategyName: context.strategyName,
4883
+ exchangeName: context.exchangeName,
4884
+ frameName: "",
4885
+ symbol,
4886
+ backtest: true,
4887
+ });
4888
+ {
4889
+ console.log(`${commit} OK`);
4890
+ console.timeEnd(commit);
4891
+ }
4892
+ process.exit(0);
4893
+ };
4894
+ main$6();
4895
+
4896
+ const SIMULATOR_NAME$1 = "cli_simulator";
4897
+ const IDEA_DIRECTIONS$1 = ["LONG", "SHORT", "NEUTRAL"];
4898
+ const validateIdea$1 = (idea, line) => {
4447
4899
  if (typeof idea !== "object" || idea === null) {
4448
4900
  return `line ${line}: not an object`;
4449
4901
  }
@@ -4456,15 +4908,16 @@ const validateIdea = (idea, line) => {
4456
4908
  if (typeof idea.symbol !== "string" || !idea.symbol) {
4457
4909
  return `line ${line}: "symbol" must be a non-empty string`;
4458
4910
  }
4459
- if (!IDEA_DIRECTIONS.includes(idea.direction)) {
4460
- return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
4911
+ if (!IDEA_DIRECTIONS$1.includes(idea.direction)) {
4912
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS$1.join("|")}, got ${JSON.stringify(idea.direction)}`;
4461
4913
  }
4462
4914
  if (typeof idea.author !== "string" || !idea.author) {
4463
4915
  return `line ${line}: "author" must be a non-empty string`;
4464
4916
  }
4465
4917
  return null;
4466
4918
  };
4467
- const toMarkdown = (result) => {
4919
+ const toMarkdown$1 = (result) => {
4920
+ const profitable = result.reports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
4468
4921
  const lines = [];
4469
4922
  lines.push(`# Simulator Report — ${result.symbol}`);
4470
4923
  lines.push("");
@@ -4473,33 +4926,36 @@ const toMarkdown = (result) => {
4473
4926
  lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
4474
4927
  lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
4475
4928
  lines.push(`| Authors allowed / banned | ${result.allowedAuthors.length} / ${result.bannedAuthors.length} |`);
4476
- lines.push(`| Grid points | ${result.reports.length} |`);
4929
+ lines.push(`| Profitable corridor | ${profitable} / ${result.reports.length} grid points |`);
4477
4930
  lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
4478
4931
  lines.push("");
4479
4932
  lines.push(`## Ranking winners`);
4480
4933
  lines.push("");
4481
- lines.push(`| Criterion | Stop% | Trailing% | Hold | Aligned | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4482
- lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4934
+ lines.push(`| Criterion | Stop% | Hold | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4935
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4483
4936
  for (const best of result.best) {
4484
4937
  if (!best.report) {
4485
- lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — | — | — |`);
4938
+ lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
4486
4939
  continue;
4487
4940
  }
4488
4941
  const { point } = best.report;
4489
- lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.trailingTakePercent} | ${point.holdMinutes / 60}h | ${point.minIdeasAligned} | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
4942
+ lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.holdMinutes / 60}h | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
4490
4943
  `${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
4491
4944
  }
4492
4945
  lines.push("");
4493
- lines.push(`## Allowed authors (production whitelist)`);
4946
+ lines.push(`## Allowed authors (whitelist of the sharpe winner rule)`);
4494
4947
  lines.push("");
4495
4948
  lines.push(`| Author | Ideas | Hits | HitRate |`);
4496
4949
  lines.push(`| --- | --- | --- | --- |`);
4497
- for (const stat of result.authorStats.filter(({ banned }) => !banned)) {
4950
+ // артефакт авторов живёт по-победительно в best[] — ран-левел
4951
+ // authorStats в ISimulatorResult больше нет
4952
+ const sharpeBest = result.best.find(({ criterion }) => criterion === "sharpe");
4953
+ for (const stat of (sharpeBest?.authorStats ?? []).filter(({ banned }) => !banned)) {
4498
4954
  lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
4499
4955
  }
4500
4956
  return lines.join("\n");
4501
4957
  };
4502
- const main$4 = async () => {
4958
+ const main$5 = async () => {
4503
4959
  if (!getEntry(import.meta.url)) {
4504
4960
  return;
4505
4961
  }
@@ -4536,7 +4992,7 @@ const main$4 = async () => {
4536
4992
  console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
4537
4993
  process.exit(1);
4538
4994
  }
4539
- const problem = validateIdea(idea, i + 1);
4995
+ const problem = validateIdea$1(idea, i + 1);
4540
4996
  if (problem) {
4541
4997
  console.error(`Error: ideas file does not match the idea structure — ${problem}`);
4542
4998
  console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
@@ -4572,11 +5028,48 @@ const main$4 = async () => {
4572
5028
  }
4573
5029
  const [defaultExchangeName = null] = await listExchangeSchema();
4574
5030
  const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
5031
+ if (values.verbose) {
5032
+ overrideExchangeSchema({
5033
+ exchangeName,
5034
+ callbacks: {
5035
+ onCandleData(symbol, interval, since) {
5036
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
5037
+ },
5038
+ },
5039
+ });
5040
+ }
4575
5041
  const symbol = values.symbol || "BTCUSDT";
5042
+ // Проба осуществимости, НЕ поиск заработка (подбор параметров — у
5043
+ // --tune): собирающая прибыль механика выключена, остаётся вопрос
5044
+ // "есть ли прибыльный коридор" по стопу x холду x правилу бана —
5045
+ // 48 точек, быстрый вердикт
4576
5046
  addSimulatorSchema({
4577
- simulatorName: SIMULATOR_NAME,
5047
+ simulatorName: SIMULATOR_NAME$1,
4578
5048
  exchangeName,
5049
+ gridAxes: {
5050
+ // грубая шкала катастрофы: коридор должен быть широким, не точкой
5051
+ hardStopPercent: [2, 3, 5, 7],
5052
+ // инертен: проба не собирает прибыль, выход — по времени или стопу
5053
+ trailingTakePercent: [100],
5054
+ holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
5055
+ // одного проверенного автора достаточно — консенсус не перебираем
5056
+ minIdeasAligned: [1],
5057
+ // правило бана — единственная перебираемая "умность" пробы
5058
+ minAuthorTrack: [3, 5],
5059
+ minAuthorHitRate: [0.5, 0.6],
5060
+ minAuthorWilson: [0],
5061
+ minWeightAligned: [0],
5062
+ profitLockPercent: [0],
5063
+ authorMetric: ["close"],
5064
+ banCriteria: ["sharpe", "pnl"],
5065
+ },
5066
+ reportOrder: "sharpe",
4579
5067
  callbacks: {
5068
+ onProgress: (symbol, stage, processed, total) => {
5069
+ if (values.verbose) {
5070
+ console.log("onProgress", { symbol, stage, processed, total });
5071
+ }
5072
+ },
4580
5073
  onIdeas: (symbol, ideasTotal, ideasDirectional) => {
4581
5074
  if (values.verbose) {
4582
5075
  console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
@@ -4632,7 +5125,7 @@ const main$4 = async () => {
4632
5125
  });
4633
5126
  const result = await Simulator.run({
4634
5127
  symbol,
4635
- simulatorName: SIMULATOR_NAME,
5128
+ simulatorName: SIMULATOR_NAME$1,
4636
5129
  ideas,
4637
5130
  });
4638
5131
  const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
@@ -4647,11 +5140,299 @@ const main$4 = async () => {
4647
5140
  if (values.markdown) {
4648
5141
  const filePath = resolve(dumpDir, `${dumpName}.md`);
4649
5142
  await mkdir(dumpDir, { recursive: true });
4650
- await writeFile(filePath, toMarkdown(result), "utf-8");
5143
+ await writeFile(filePath, toMarkdown$1(result), "utf-8");
4651
5144
  console.log(`Saved: ${filePath}`);
4652
5145
  process.exit(0);
4653
5146
  }
4654
- console.log(toMarkdown(result));
5147
+ console.log(toMarkdown$1(result));
5148
+ process.exit(0);
5149
+ };
5150
+ main$5();
5151
+
5152
+ const SIMULATOR_NAME = "cli_tune";
5153
+ /** Доля времени фида, уходящая в обучение; хвост — один OOS-выстрел. */
5154
+ const DEFAULT_TRAIN_SPLIT = 0.7;
5155
+ const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
5156
+ const validateIdea = (idea, line) => {
5157
+ if (typeof idea !== "object" || idea === null) {
5158
+ return `line ${line}: not an object`;
5159
+ }
5160
+ if (typeof idea.id !== "number") {
5161
+ return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
5162
+ }
5163
+ if (typeof idea.ts !== "number") {
5164
+ return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
5165
+ }
5166
+ if (typeof idea.symbol !== "string" || !idea.symbol) {
5167
+ return `line ${line}: "symbol" must be a non-empty string`;
5168
+ }
5169
+ if (!IDEA_DIRECTIONS.includes(idea.direction)) {
5170
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
5171
+ }
5172
+ if (typeof idea.author !== "string" || !idea.author) {
5173
+ return `line ${line}: "author" must be a non-empty string`;
5174
+ }
5175
+ return null;
5176
+ };
5177
+ const pointLabel = (point) => `H=${point.hardStopPercent} TT=${point.trailingTakePercent} hold=${point.holdMinutes / 60}h N=${point.minIdeasAligned} ` +
5178
+ `track=${point.minAuthorTrack} rate=${point.minAuthorHitRate} wilson=${point.minAuthorWilson} W=${point.minWeightAligned} ` +
5179
+ `lock=${point.profitLockPercent} metric=${point.authorMetric}`;
5180
+ const fmtRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
5181
+ const toMarkdown = (train, test, trainSplit) => {
5182
+ const lines = [];
5183
+ lines.push(`# Tune Report — ${train.symbol}`);
5184
+ lines.push("");
5185
+ 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.`);
5186
+ lines.push("");
5187
+ lines.push(`## Train (${train.ideasDirectional} directional ideas, ${train.reports.length} grid points)`);
5188
+ lines.push("");
5189
+ lines.push(`| Criterion | Point | Trades | PNL% | WinRate | DD% | Sharpe | Sortino |`);
5190
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- |`);
5191
+ for (const best of train.best) {
5192
+ if (!best.report) {
5193
+ lines.push(`| ${best.criterion} | — | — | — | — | — | — | — |`);
5194
+ continue;
5195
+ }
5196
+ lines.push(`| ${best.criterion} | ${pointLabel(best.report.point)} | ${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ` +
5197
+ `${(best.report.winRate * 100).toFixed(0)}% | ${best.report.maxSeriesDrawdownPercent.toFixed(2)} | ${fmtRatio(best.report.sharpe)} | ${fmtRatio(best.report.sortino)} |`);
5198
+ }
5199
+ lines.push("");
5200
+ lines.push(`## Out-of-sample (frozen sharpe winner, ${test.ideasDirectional} tail ideas)`);
5201
+ lines.push("");
5202
+ lines.push(`Frozen point: ${pointLabel(test.point)}`);
5203
+ lines.push("");
5204
+ lines.push(`| Metric | Value |`);
5205
+ lines.push(`| --- | --- |`);
5206
+ lines.push(`| Trades (skipped busy) | ${test.report.trades} (${test.report.skippedBusy}) |`);
5207
+ lines.push(`| PNL | ${test.report.totalPnlPercent.toFixed(2)}% |`);
5208
+ lines.push(`| Win rate | ${(test.report.winRate * 100).toFixed(0)}% |`);
5209
+ lines.push(`| Profit factor | ${fmtRatio(test.report.profitFactor)} |`);
5210
+ lines.push(`| Max series drawdown | ${test.report.maxSeriesDrawdownPercent.toFixed(2)}% |`);
5211
+ lines.push(`| Sharpe / Sortino | ${fmtRatio(test.report.sharpe)} / ${fmtRatio(test.report.sortino)} |`);
5212
+ lines.push(`| Recovery factor | ${fmtRatio(test.report.recoveryFactor)} |`);
5213
+ lines.push(`| Exits | ${Object.entries(test.report.exitReasons).map(([reason, count]) => `${reason}=${count}`).join(", ")} |`);
5214
+ lines.push("");
5215
+ lines.push(`## Test trades`);
5216
+ lines.push("");
5217
+ lines.push(`| Direction | Exit | PNL% | Hold | Entry (UTC) |`);
5218
+ lines.push(`| --- | --- | --- | --- | --- |`);
5219
+ for (const trade of test.trades) {
5220
+ lines.push(`| ${trade.direction} | ${trade.exitReason} | ${trade.pnlPercent.toFixed(2)} | ${trade.holdMinutesActual}m | ${new Date(trade.entryTimestamp).toISOString()} |`);
5221
+ }
5222
+ lines.push("");
5223
+ lines.push(`## Frozen author track record (train range, sharpe winner rule)`);
5224
+ lines.push("");
5225
+ 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.`);
5226
+ lines.push("");
5227
+ lines.push(`| Author | Ideas | Hits | Banned |`);
5228
+ lines.push(`| --- | --- | --- | --- |`);
5229
+ for (const stat of test.authorStats) {
5230
+ lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${stat.banned ? "yes" : ""} |`);
5231
+ }
5232
+ return lines.join("\n");
5233
+ };
5234
+ const main$4 = async () => {
5235
+ if (!getEntry(import.meta.url)) {
5236
+ return;
5237
+ }
5238
+ const { values, positionals } = getArgs();
5239
+ if (!values.tune) {
5240
+ return;
5241
+ }
5242
+ const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
5243
+ if (!ideasPath) {
5244
+ console.error("Error: positional path to an ideas .jsonl file is required");
5245
+ process.exit(1);
5246
+ }
5247
+ let ideas = [];
5248
+ {
5249
+ let content;
5250
+ try {
5251
+ content = await readFile(resolve(ideasPath), "utf-8");
5252
+ }
5253
+ catch (error) {
5254
+ console.error(`Error: cannot read ideas file: ${ideasPath}`);
5255
+ process.exit(1);
5256
+ }
5257
+ const rows = content.split("\n").filter(Boolean);
5258
+ if (!rows.length) {
5259
+ console.error(`Error: ideas file is empty: ${ideasPath}`);
5260
+ process.exit(1);
5261
+ }
5262
+ for (let i = 0; i < rows.length; i++) {
5263
+ let idea;
5264
+ try {
5265
+ idea = JSON.parse(rows[i]);
5266
+ }
5267
+ catch {
5268
+ console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
5269
+ process.exit(1);
5270
+ }
5271
+ const problem = validateIdea(idea, i + 1);
5272
+ if (problem) {
5273
+ console.error(`Error: ideas file does not match the idea structure — ${problem}`);
5274
+ console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
5275
+ process.exit(1);
5276
+ }
5277
+ ideas.push(idea);
5278
+ }
5279
+ }
5280
+ const trainSplit = values.split
5281
+ ? parseFloat(values.split)
5282
+ : DEFAULT_TRAIN_SPLIT;
5283
+ if (!Number.isFinite(trainSplit) || trainSplit <= 0 || trainSplit >= 1) {
5284
+ console.error(`Error: --split must be a fraction between 0 and 1, got ${values.split}`);
5285
+ process.exit(1);
5286
+ }
5287
+ {
5288
+ const cwd = process.cwd();
5289
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
5290
+ }
5291
+ await cli.configConnectionService.loadConfig("setup.config");
5292
+ {
5293
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
5294
+ try {
5295
+ if (typeof loader === "function") {
5296
+ await loader();
5297
+ }
5298
+ if (typeof loader?.loader === "function") {
5299
+ await loader.loader();
5300
+ }
5301
+ }
5302
+ catch (error) {
5303
+ console.error("Module loader failed", error);
5304
+ process.exit(-1);
5305
+ }
5306
+ }
5307
+ await cli.moduleConnectionService.loadModule("tune.module");
5308
+ {
5309
+ await cli.exchangeSchemaService.addSchema();
5310
+ await cli.symbolSchemaService.addSchema();
5311
+ }
5312
+ const [defaultExchangeName = null] = await listExchangeSchema();
5313
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
5314
+ if (values.verbose) {
5315
+ overrideExchangeSchema({
5316
+ exchangeName,
5317
+ callbacks: {
5318
+ onCandleData(symbol, interval, since) {
5319
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
5320
+ },
5321
+ },
5322
+ });
5323
+ }
5324
+ const symbol = values.symbol || "BTCUSDT";
5325
+ // Подбор параметров: полная сетка с собирающей прибыль механикой —
5326
+ // замок, трейлинг, взвешенный консенсус, Вильсон, обе метрики бана.
5327
+ // Честность обеспечивается сплитом: обучение видит только голову,
5328
+ // хвост тратится на один выстрел замороженного sharpe-победителя
5329
+ addSimulatorSchema({
5330
+ simulatorName: SIMULATOR_NAME,
5331
+ exchangeName,
5332
+ gridAxes: {
5333
+ // стопы < 2% сидят внутри медианного шейкаута и не выигрывают
5334
+ hardStopPercent: [2, 2.5, 3, 4, 5, 7],
5335
+ trailingTakePercent: [1, 1.5, 2, 3, 4],
5336
+ holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
5337
+ minIdeasAligned: [1, 2],
5338
+ minAuthorTrack: [2, 3, 5],
5339
+ minAuthorHitRate: [0.5, 0.6],
5340
+ minAuthorWilson: [0, 0.6],
5341
+ minWeightAligned: [0, 0.6, 1.2],
5342
+ profitLockPercent: [0, 1.5, 2.5],
5343
+ authorMetric: ["close", "reach"],
5344
+ banCriteria: ["sharpe"],
5345
+ },
5346
+ reportOrder: "sharpe",
5347
+ callbacks: {
5348
+ onProgress: (symbol, stage, processed, total) => {
5349
+ if (values.verbose) {
5350
+ console.log("onProgress", { symbol, stage, processed, total });
5351
+ }
5352
+ },
5353
+ onIdeas: (symbol, ideasTotal, ideasDirectional) => {
5354
+ if (values.verbose) {
5355
+ console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
5356
+ }
5357
+ },
5358
+ onProfiles: (symbol, profiles, truncatedCount) => {
5359
+ if (values.verbose) {
5360
+ console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
5361
+ }
5362
+ },
5363
+ onAuthorsTrained: (symbol, stats, bannedIdeas) => {
5364
+ if (values.verbose) {
5365
+ console.log("onAuthorsTrained", {
5366
+ symbol,
5367
+ authors: stats.length,
5368
+ banned: stats.filter(({ banned }) => banned).length,
5369
+ bannedIdeas,
5370
+ });
5371
+ }
5372
+ },
5373
+ onRanking: (symbol, criterion, _sorted, best) => {
5374
+ if (values.verbose) {
5375
+ console.log("onRanking", {
5376
+ symbol,
5377
+ criterion,
5378
+ point: best.report?.point ?? null,
5379
+ pnl: best.report ? +best.report.totalPnlPercent.toFixed(2) : null,
5380
+ });
5381
+ }
5382
+ },
5383
+ onTestDone: (symbol, result) => {
5384
+ if (values.verbose) {
5385
+ console.log("onTestDone", {
5386
+ symbol,
5387
+ trades: result.report.trades,
5388
+ pnl: +result.report.totalPnlPercent.toFixed(2),
5389
+ });
5390
+ }
5391
+ },
5392
+ },
5393
+ });
5394
+ // обучение видит ТОЛЬКО голову ленты; хвост — один OOS-выстрел
5395
+ const sorted = [...ideas].sort((a, b) => a.ts - b.ts);
5396
+ const cutoff = sorted[0].ts + (sorted[sorted.length - 1].ts - sorted[0].ts) * trainSplit;
5397
+ const trainIdeas = sorted.filter(({ ts }) => ts < cutoff);
5398
+ const testIdeas = sorted.filter(({ ts }) => ts >= cutoff);
5399
+ const train = await Simulator.run({
5400
+ symbol,
5401
+ simulatorName: SIMULATOR_NAME,
5402
+ ideas: trainIdeas,
5403
+ });
5404
+ const sharpeBest = train.best.find(({ criterion }) => criterion === "sharpe");
5405
+ if (!sharpeBest?.report) {
5406
+ console.error("Error: training produced no sharpe winner — nothing to freeze for the out-of-sample shot");
5407
+ process.exit(1);
5408
+ }
5409
+ // заморозка: точка и трек-рекорд sharpe-победителя как есть — из
5410
+ // замороженных статов test() читает только author/ideas/hits и сам
5411
+ // перевыводит баны и веса под правило точки
5412
+ const test = await Simulator.test({
5413
+ symbol,
5414
+ simulatorName: SIMULATOR_NAME,
5415
+ ideas: testIdeas,
5416
+ point: sharpeBest.report.point,
5417
+ authorStats: sharpeBest.authorStats,
5418
+ });
5419
+ const dumpName = values.output || `tune_${symbol}_${Date.now()}`;
5420
+ const dumpDir = join(process.cwd(), "dump");
5421
+ if (values.json) {
5422
+ const filePath = resolve(dumpDir, `${dumpName}.json`);
5423
+ await mkdir(dumpDir, { recursive: true });
5424
+ await writeFile(filePath, JSON.stringify({ trainSplit, train, test }, null, 2), "utf-8");
5425
+ console.log(`Saved: ${filePath}`);
5426
+ process.exit(0);
5427
+ }
5428
+ if (values.markdown) {
5429
+ const filePath = resolve(dumpDir, `${dumpName}.md`);
5430
+ await mkdir(dumpDir, { recursive: true });
5431
+ await writeFile(filePath, toMarkdown(train, test, trainSplit), "utf-8");
5432
+ console.log(`Saved: ${filePath}`);
5433
+ process.exit(0);
5434
+ }
5435
+ console.log(toMarkdown(train, test, trainSplit));
4655
5436
  process.exit(0);
4656
5437
  };
4657
5438
  main$4();
@@ -4852,7 +5633,8 @@ Modes:
4852
5633
  --dump Fetch and save raw OHLCV candles
4853
5634
  --pnldebug Simulate PnL per minute for a given entry price and direction
4854
5635
  --brokerdebug Fire a single broker commit against the live broker adapter
4855
- --simulator <ideas.jsonl> Sweep exit/entry parameters over crowd ideas, train author whitelist
5636
+ --simulator <ideas.jsonl> Feasibility probe over crowd ideas: is there a profitable corridor at all
5637
+ --tune <ideas.jsonl> Walk-forward parameter search: train on the head, one frozen shot on the tail
4856
5638
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
4857
5639
  --init Scaffold a new project in the current directory
4858
5640
  --docker Scaffold a Docker workspace for running strategies in a container
@@ -4987,19 +5769,47 @@ Simulator flags (--simulator):
4987
5769
  "author": string }. The file is validated BEFORE any work; a line that does not
4988
5770
  match the structure aborts the run with an error.
4989
5771
 
4990
- Runs the backtest-kit Simulator entity: one 5-day candle pass per idea, flood
4991
- dedupe (one idea per author per direction per 8h), default-ban author filter
4992
- (ban thresholds are grid axes), grid evaluation with time-based Sharpe/Sortino,
4993
- three ranking winners. The report includes the production author whitelist.
5772
+ A FEASIBILITY PROBE, not a parameter search (that is --tune): the profit-harvesting
5773
+ machinery is off (no profit lock, inert trailing, no weighted consensus), and a
5774
+ fast 48-point grid of hard stop x hold x ban rule answers one question — does the
5775
+ feed contain a profitable corridor at all. One 5-day candle pass per idea, flood
5776
+ dedupe (one idea per author per direction per 8h), default-ban author filter,
5777
+ time-based Sharpe/Sortino, four ranking winners. The report carries the corridor
5778
+ share and the whitelist of the sharpe winner's ban rule as evidence.
4994
5779
  Ideas of other symbols are filtered out — one shared feed serves any --symbol.
4995
5780
 
4996
5781
  No output flag → print the Markdown summary to stdout. With --verbose every
4997
- simulator lifecycle callback (onIdeas, onProfiles, onAuthorsTrained, onGridPoint,
4998
- onRanking, onDone) is logged to the console as it fires.
5782
+ simulator lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
5783
+ onGridPoint, onRanking, onDone) is logged to the console as it fires.
4999
5784
 
5000
5785
  Module file ./modules/simulator.module is loaded automatically if it exists
5001
5786
  (register your exchange there); without it CCXT Binance is used by default.
5002
5787
 
5788
+ Tune flags (--tune):
5789
+
5790
+ --symbol <string> Trading pair to tune (default: BTCUSDT)
5791
+ --exchange <string> Exchange name (default: first registered)
5792
+ --split <string> Train share of the feed time range, 0..1 (default: 0.7)
5793
+ --output <string> Output file base name (default: tune_{SYMBOL}_{TIMESTAMP})
5794
+ --json Save { trainSplit, train, test } to ./dump/<output>.json
5795
+ --markdown Save walk-forward report to ./dump/<output>.md
5796
+ --verbose Log simulator lifecycle callbacks to the console
5797
+
5798
+ Positional: path to an ideas .jsonl file — same shape and validation as --simulator.
5799
+
5800
+ The PARAMETER SEARCH counterpart of the --simulator probe: the full grid with the
5801
+ profit-harvesting machinery on — profit lock, trailing take, weighted consensus,
5802
+ Wilson-bound ban, both author-hit metrics (close/reach). Honesty is structural:
5803
+ training sees ONLY the head of the feed (--split of its time range), then the
5804
+ sharpe winner is frozen — point and raw author track record — and fired exactly
5805
+ once on the tail via Simulator.test. Nothing is trained on the tail; authors
5806
+ unseen in training are banned by default. The report carries the train winners,
5807
+ the out-of-sample result with the trade list, and the frozen author track record
5808
+ to hardcode for production Simulator.test.
5809
+
5810
+ Module file ./modules/tune.module is loaded automatically if it exists
5811
+ (register your exchange there); without it CCXT Binance is used by default.
5812
+
5003
5813
  Flush flags (--flush):
5004
5814
 
5005
5815
  One or more positional entry points. For each entry point the following
@@ -5033,7 +5843,8 @@ Module hooks (loaded automatically by each mode):
5033
5843
  modules/dump.module --dump Exchange schema for candle dumps
5034
5844
  modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
5035
5845
  modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
5036
- modules/simulator.module --simulator Exchange schema for crowd-ideas simulation
5846
+ modules/simulator.module --simulator Exchange schema for the crowd-ideas feasibility probe
5847
+ modules/tune.module --tune Exchange schema for the walk-forward parameter search
5037
5848
 
5038
5849
  --flush has no associated module. It only removes dump subdirectories.
5039
5850
 
@@ -5062,8 +5873,10 @@ Examples:
5062
5873
  node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
5063
5874
  node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
5064
5875
  node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
5065
- node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/ts-ideas.normalized.jsonl
5066
- node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_sweep ./assets/ts-ideas.normalized.jsonl
5876
+ node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5877
+ node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_probe ./assets/tv-ideas.normalized.jsonl
5878
+ node ${ENTRY_PATH} --tune --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5879
+ node ${ENTRY_PATH} --tune --symbol BTCUSDT --split 0.7 --markdown --output jun_2026_tune ./assets/tv-ideas.normalized.jsonl
5067
5880
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
5068
5881
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
5069
5882
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -5077,7 +5890,7 @@ const main$1 = async () => {
5077
5890
  if (!values.help) {
5078
5891
  return;
5079
5892
  }
5080
- process.stdout.write(`@backtest-kit/cli ${"16.4.0"}\n\n`);
5893
+ process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n\n`);
5081
5894
  process.stdout.write(HELP_TEXT);
5082
5895
  process.exit(0);
5083
5896
  };
@@ -5091,7 +5904,7 @@ const main = async () => {
5091
5904
  if (!values.version) {
5092
5905
  return;
5093
5906
  }
5094
- process.stdout.write(`@backtest-kit/cli ${"16.4.0"}\n`);
5907
+ process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
5095
5908
  process.exit(0);
5096
5909
  };
5097
5910
  main();