@backtest-kit/cli 16.3.0 → 16.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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$i = () => {
236
+ const main$j = () => {
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$i();
243
+ main$j();
244
244
 
245
245
  const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
246
246
  function dumpStackTrace(error) {
@@ -891,6 +891,10 @@ const getArgs = functoolsKit.singleshot(() => {
891
891
  type: "boolean",
892
892
  default: false,
893
893
  },
894
+ simulator: {
895
+ type: "boolean",
896
+ default: false,
897
+ },
894
898
  commit: {
895
899
  type: "string",
896
900
  default: "",
@@ -2337,6 +2341,13 @@ const STOP_BOT_FN = functoolsKit.singleshot(async () => {
2337
2341
  const { stopBot } = await getTelegram();
2338
2342
  stopBot();
2339
2343
  });
2344
+ /**
2345
+ * Throttle window for the per-tick order_continue decision channel: at most one
2346
+ * Telegram notification per signalId per 15 minutes via functools-kit ttl. The
2347
+ * terminal order_stop is NOT throttled (fires once per signal) and clears the
2348
+ * throttle slot for its signalId.
2349
+ */
2350
+ const ORDER_CHECK_NOTIFY_TTL = 15 * 60 * 1000;
2340
2351
  class TelegramLogicService {
2341
2352
  constructor() {
2342
2353
  this.loggerService = inject(TYPES.loggerService);
@@ -2485,11 +2496,24 @@ class TelegramLogicService {
2485
2496
  markdown,
2486
2497
  });
2487
2498
  };
2488
- this.notifySignalOpen = functoolsKit.trycatch(async (event) => {
2489
- this.loggerService.log("telegramLogicService notifySignalOpen", {
2499
+ this.notifyOrderFillOpen = functoolsKit.trycatch(async (event) => {
2500
+ this.loggerService.log("telegramLogicService notifyOrderFillOpen", {
2501
+ event,
2502
+ });
2503
+ const markdown = await this.telegramTemplateService.getOrderFillOpenMarkdown(event);
2504
+ if (!markdown) {
2505
+ return;
2506
+ }
2507
+ await this.telegramWebService.publishNotify({
2508
+ symbol: event.symbol,
2509
+ markdown,
2510
+ });
2511
+ });
2512
+ this.notifyOrderFillClose = functoolsKit.trycatch(async (event) => {
2513
+ this.loggerService.log("telegramLogicService notifyOrderFillClose", {
2490
2514
  event,
2491
2515
  });
2492
- const markdown = await this.telegramTemplateService.getSignalOpenMarkdown(event);
2516
+ const markdown = await this.telegramTemplateService.getOrderFillCloseMarkdown(event);
2493
2517
  if (!markdown) {
2494
2518
  return;
2495
2519
  }
@@ -2498,11 +2522,64 @@ class TelegramLogicService {
2498
2522
  markdown,
2499
2523
  });
2500
2524
  });
2501
- this.notifySignalClose = functoolsKit.trycatch(async (event) => {
2502
- this.loggerService.log("telegramLogicService notifySignalClose", {
2525
+ this.notifyOrderRejectOpen = functoolsKit.trycatch(async (event) => {
2526
+ this.loggerService.log("telegramLogicService notifyOrderRejectOpen", {
2503
2527
  event,
2504
2528
  });
2505
- const markdown = await this.telegramTemplateService.getSignalCloseMarkdown(event);
2529
+ const markdown = await this.telegramTemplateService.getOrderRejectOpenMarkdown(event);
2530
+ if (!markdown) {
2531
+ return;
2532
+ }
2533
+ await this.telegramWebService.publishNotify({
2534
+ symbol: event.symbol,
2535
+ markdown,
2536
+ });
2537
+ });
2538
+ this.notifyOrderRejectClose = functoolsKit.trycatch(async (event) => {
2539
+ this.loggerService.log("telegramLogicService notifyOrderRejectClose", {
2540
+ event,
2541
+ });
2542
+ const markdown = await this.telegramTemplateService.getOrderRejectCloseMarkdown(event);
2543
+ if (!markdown) {
2544
+ return;
2545
+ }
2546
+ await this.telegramWebService.publishNotify({
2547
+ symbol: event.symbol,
2548
+ markdown,
2549
+ });
2550
+ });
2551
+ /**
2552
+ * Post-verdict order-check CONTINUE (order_continue). Fires every live tick the
2553
+ * order survives the check, so it is throttled via functools-kit ttl:
2554
+ * one message per signalId per ORDER_CHECK_NOTIFY_TTL (15 minutes).
2555
+ */
2556
+ this.notifyOrderContinue = functoolsKit.ttl(functoolsKit.trycatch(async (event) => {
2557
+ this.loggerService.log("telegramLogicService notifyOrderContinue", {
2558
+ event,
2559
+ });
2560
+ const markdown = await this.telegramTemplateService.getOrderContinueMarkdown(event);
2561
+ if (!markdown) {
2562
+ return;
2563
+ }
2564
+ await this.telegramWebService.publishNotify({
2565
+ symbol: event.symbol,
2566
+ markdown,
2567
+ });
2568
+ }), {
2569
+ key: ([event]) => event.signalId,
2570
+ timeout: ORDER_CHECK_NOTIFY_TTL,
2571
+ });
2572
+ /**
2573
+ * Post-verdict order-check STOP (order_stop). Terminal — fires once per signal,
2574
+ * so no throttle; drops the continue throttle slot for this signalId
2575
+ * (the signal leaves monitoring, the ttl entry would only leak otherwise).
2576
+ */
2577
+ this.notifyOrderStop = functoolsKit.trycatch(async (event) => {
2578
+ this.loggerService.log("telegramLogicService notifyOrderStop", {
2579
+ event,
2580
+ });
2581
+ this.notifyOrderContinue.clear(event.signalId);
2582
+ const markdown = await this.telegramTemplateService.getOrderStopMarkdown(event);
2506
2583
  if (!markdown) {
2507
2584
  return;
2508
2585
  }
@@ -2607,26 +2684,51 @@ class TelegramLogicService {
2607
2684
  return;
2608
2685
  }
2609
2686
  });
2610
- const unSync = BacktestKit.listenSync(async (event) => {
2687
+ // Broker-CONFIRMED fills only (post-verdict orderFillSubject): a rejected or
2688
+ // transient gate attempt never reaches this channel, so "Order Filled" is
2689
+ // guaranteed truthful. The pre-verdict syncSubject is NOT consumed here.
2690
+ const unOrderFill = BacktestKit.listenOrderFill(async (event) => {
2611
2691
  if (event.action === "signal-open") {
2612
- // type "schedule" is a resting-order PLACEMENT, not a position open:
2613
- // the user already got the "scheduled" notification from listenSignal
2614
- if (event.type !== "active") {
2615
- return;
2616
- }
2617
- await this.notifySignalOpen(event);
2692
+ // Both open outcomes notify: type "active" — the order opened at once
2693
+ // (immediate fill, no schedule phase), type "schedule" — the resting
2694
+ // order was placed on the exchange. Either way the broker confirmed a
2695
+ // real order; the template's Order field carries the type.
2696
+ await this.notifyOrderFillOpen(event);
2697
+ return;
2698
+ }
2699
+ if (event.action === "signal-close") {
2700
+ await this.notifyOrderFillClose(event);
2701
+ return;
2702
+ }
2703
+ });
2704
+ // Terminal broker rejection (post-verdict orderRejectSubject): exactly one
2705
+ // event per dropped order attempt — the engine consumes the rejected signal
2706
+ // id, so this cannot repeat per-tick for the same signal. Dispatched by
2707
+ // action to the dedicated open/close notifications (single responsibility).
2708
+ const unOrderReject = BacktestKit.listenOrderReject(async (event) => {
2709
+ if (event.action === "signal-open") {
2710
+ await this.notifyOrderRejectOpen(event);
2618
2711
  return;
2619
2712
  }
2620
2713
  if (event.action === "signal-close") {
2621
- await this.notifySignalClose(event);
2714
+ await this.notifyOrderRejectClose(event);
2622
2715
  return;
2623
2716
  }
2624
2717
  });
2718
+ // Post-verdict check decisions: CONTINUE fires every surviving tick and is
2719
+ // throttled to one message per signalId per 15 minutes via ttl; terminal
2720
+ // STOP fires once per signal, unthrottled.
2721
+ const unOrderContinue = BacktestKit.listenOrderContinue(async (event) => {
2722
+ await this.notifyOrderContinue(event);
2723
+ });
2724
+ const unOrderStop = BacktestKit.listenOrderStop(async (event) => {
2725
+ await this.notifyOrderStop(event);
2726
+ });
2625
2727
  const unSignalNotify = BacktestKit.listenSignalNotify(async (event) => {
2626
2728
  await this.notifySignalInfo(event);
2627
2729
  });
2628
2730
  const unConnect = () => this.connect.clear();
2629
- const unListen = functoolsKit.compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unSync(), () => unSignalNotify(), () => unConnect());
2731
+ const unListen = functoolsKit.compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unSignalNotify(), () => unConnect());
2630
2732
  return () => {
2631
2733
  STOP_BOT_FN();
2632
2734
  unListen();
@@ -2779,25 +2881,65 @@ class TelegramTemplateService {
2779
2881
  }
2780
2882
  return await RENDER_TEMPLATE_FN("average-buy.mustache", event, this);
2781
2883
  };
2782
- this.getSignalOpenMarkdown = async (event) => {
2783
- this.loggerService.log("telegramTemplateService getSignalOpenMarkdown", {
2884
+ this.getOrderFillOpenMarkdown = async (event) => {
2885
+ this.loggerService.log("telegramTemplateService getOrderFillOpenMarkdown", {
2886
+ event,
2887
+ });
2888
+ const adapter = await this.getTelegramAdapter();
2889
+ if (adapter?.getOrderFillOpenMarkdown) {
2890
+ return await adapter.getOrderFillOpenMarkdown(event);
2891
+ }
2892
+ return await RENDER_TEMPLATE_FN("order-fill-open.mustache", event, this);
2893
+ };
2894
+ this.getOrderFillCloseMarkdown = async (event) => {
2895
+ this.loggerService.log("telegramTemplateService getOrderFillCloseMarkdown", {
2896
+ event,
2897
+ });
2898
+ const adapter = await this.getTelegramAdapter();
2899
+ if (adapter?.getOrderFillCloseMarkdown) {
2900
+ return await adapter.getOrderFillCloseMarkdown(event);
2901
+ }
2902
+ return await RENDER_TEMPLATE_FN("order-fill-close.mustache", event, this);
2903
+ };
2904
+ this.getOrderRejectOpenMarkdown = async (event) => {
2905
+ this.loggerService.log("telegramTemplateService getOrderRejectOpenMarkdown", {
2906
+ event,
2907
+ });
2908
+ const adapter = await this.getTelegramAdapter();
2909
+ if (adapter?.getOrderRejectOpenMarkdown) {
2910
+ return await adapter.getOrderRejectOpenMarkdown(event);
2911
+ }
2912
+ return await RENDER_TEMPLATE_FN("order-reject-open.mustache", event, this);
2913
+ };
2914
+ this.getOrderRejectCloseMarkdown = async (event) => {
2915
+ this.loggerService.log("telegramTemplateService getOrderRejectCloseMarkdown", {
2916
+ event,
2917
+ });
2918
+ const adapter = await this.getTelegramAdapter();
2919
+ if (adapter?.getOrderRejectCloseMarkdown) {
2920
+ return await adapter.getOrderRejectCloseMarkdown(event);
2921
+ }
2922
+ return await RENDER_TEMPLATE_FN("order-reject-close.mustache", event, this);
2923
+ };
2924
+ this.getOrderContinueMarkdown = async (event) => {
2925
+ this.loggerService.log("telegramTemplateService getOrderContinueMarkdown", {
2784
2926
  event,
2785
2927
  });
2786
2928
  const adapter = await this.getTelegramAdapter();
2787
- if (adapter?.getSignalOpenMarkdown) {
2788
- return await adapter.getSignalOpenMarkdown(event);
2929
+ if (adapter?.getOrderContinueMarkdown) {
2930
+ return await adapter.getOrderContinueMarkdown(event);
2789
2931
  }
2790
- return await RENDER_TEMPLATE_FN("signal-open.mustache", event, this);
2932
+ return await RENDER_TEMPLATE_FN("order-continue.mustache", event, this);
2791
2933
  };
2792
- this.getSignalCloseMarkdown = async (event) => {
2793
- this.loggerService.log("telegramTemplateService getSignalCloseMarkdown", {
2934
+ this.getOrderStopMarkdown = async (event) => {
2935
+ this.loggerService.log("telegramTemplateService getOrderStopMarkdown", {
2794
2936
  event,
2795
2937
  });
2796
2938
  const adapter = await this.getTelegramAdapter();
2797
- if (adapter?.getSignalCloseMarkdown) {
2798
- return await adapter.getSignalCloseMarkdown(event);
2939
+ if (adapter?.getOrderStopMarkdown) {
2940
+ return await adapter.getOrderStopMarkdown(event);
2799
2941
  }
2800
- return await RENDER_TEMPLATE_FN("signal-close.mustache", event, this);
2942
+ return await RENDER_TEMPLATE_FN("order-stop.mustache", event, this);
2801
2943
  };
2802
2944
  this.getCancelScheduledMarkdown = async (event) => {
2803
2945
  this.loggerService.log("telegramTemplateService getCancelScheduledMarkdown", {
@@ -3534,14 +3676,14 @@ const cli = {
3534
3676
  };
3535
3677
  init();
3536
3678
 
3537
- const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "flush", "init", "docker", "help", "version"];
3679
+ const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "flush", "init", "docker", "help", "version"];
3538
3680
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
3539
3681
  const HELP_TEXT$1 = `
3540
3682
  Example:
3541
3683
 
3542
3684
  node ${ENTRY_PATH$1} --help
3543
3685
  `.trimStart();
3544
- const main$h = async () => {
3686
+ const main$i = async () => {
3545
3687
  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)))) {
3546
3688
  return;
3547
3689
  }
@@ -3549,14 +3691,14 @@ const main$h = async () => {
3549
3691
  if (MODES.some((mode) => values[mode])) {
3550
3692
  return;
3551
3693
  }
3552
- process.stdout.write(`@backtest-kit/cli ${"16.3.0"}\n`);
3694
+ process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n`);
3553
3695
  process.stdout.write("\n");
3554
3696
  process.stdout.write(`Run with --help to see available commands.\n`);
3555
3697
  process.stdout.write("\n");
3556
3698
  process.stdout.write(HELP_TEXT$1);
3557
3699
  process.exit(0);
3558
3700
  };
3559
- main$h();
3701
+ main$i();
3560
3702
 
3561
3703
  const notifyShutdown = functoolsKit.singleshot(async () => {
3562
3704
  console.log("Graceful shutdown initiated.");
@@ -3572,7 +3714,7 @@ const flush = async (entryPoint) => {
3572
3714
  console.log(`Removed: ${target}`);
3573
3715
  }
3574
3716
  };
3575
- const main$g = async () => {
3717
+ const main$h = async () => {
3576
3718
  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)))) {
3577
3719
  return;
3578
3720
  }
@@ -3589,7 +3731,7 @@ const main$g = async () => {
3589
3731
  }
3590
3732
  process.exit(0);
3591
3733
  };
3592
- main$g();
3734
+ main$h();
3593
3735
 
3594
3736
  const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
3595
3737
  process.off("SIGINT", BEFORE_EXIT_FN$6);
@@ -3612,7 +3754,7 @@ const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
3612
3754
  const listenGracefulShutdown$6 = functoolsKit.singleshot(() => {
3613
3755
  process.on("SIGINT", BEFORE_EXIT_FN$6);
3614
3756
  });
3615
- const main$f = async () => {
3757
+ const main$g = async () => {
3616
3758
  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)))) {
3617
3759
  return;
3618
3760
  }
@@ -3630,7 +3772,7 @@ const main$f = async () => {
3630
3772
  await cli.backtestMainService.connect();
3631
3773
  listenGracefulShutdown$6();
3632
3774
  };
3633
- main$f();
3775
+ main$g();
3634
3776
 
3635
3777
  const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3636
3778
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3649,7 +3791,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3649
3791
  const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
3650
3792
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3651
3793
  });
3652
- const main$e = async () => {
3794
+ const main$f = async () => {
3653
3795
  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)))) {
3654
3796
  return;
3655
3797
  }
@@ -3668,7 +3810,7 @@ const main$e = async () => {
3668
3810
  listenGracefulShutdown$5();
3669
3811
  await cli.walkerMainService.connect();
3670
3812
  };
3671
- main$e();
3813
+ main$f();
3672
3814
 
3673
3815
  const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3674
3816
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3690,7 +3832,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3690
3832
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
3691
3833
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3692
3834
  });
3693
- const main$d = async () => {
3835
+ const main$e = async () => {
3694
3836
  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)))) {
3695
3837
  return;
3696
3838
  }
@@ -3704,7 +3846,7 @@ const main$d = async () => {
3704
3846
  await cli.paperMainService.connect();
3705
3847
  listenGracefulShutdown$4();
3706
3848
  };
3707
- main$d();
3849
+ main$e();
3708
3850
 
3709
3851
  const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3710
3852
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3726,7 +3868,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3726
3868
  const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
3727
3869
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3728
3870
  });
3729
- const main$c = async () => {
3871
+ const main$d = async () => {
3730
3872
  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)))) {
3731
3873
  return;
3732
3874
  }
@@ -3740,7 +3882,7 @@ const main$c = async () => {
3740
3882
  await cli.liveMainService.connect();
3741
3883
  listenGracefulShutdown$3();
3742
3884
  };
3743
- main$c();
3885
+ main$d();
3744
3886
 
3745
3887
  const stopBacktestList$1 = async () => {
3746
3888
  for (const item of await BacktestKit.Backtest.list()) {
@@ -3804,7 +3946,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
3804
3946
  const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
3805
3947
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3806
3948
  });
3807
- const main$b = async () => {
3949
+ const main$c = async () => {
3808
3950
  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)))) {
3809
3951
  return;
3810
3952
  }
@@ -3860,7 +4002,7 @@ const main$b = async () => {
3860
4002
  listenGracefulShutdown$2();
3861
4003
  await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
3862
4004
  };
3863
- main$b();
4005
+ main$c();
3864
4006
 
3865
4007
  const MODE_MODULE = {
3866
4008
  backtest: "backtest.module",
@@ -3963,7 +4105,7 @@ const createGracefulShutdown = (mode) => {
3963
4105
  process.on("SIGINT", handler);
3964
4106
  });
3965
4107
  };
3966
- const main$a = async () => {
4108
+ const main$b = async () => {
3967
4109
  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)))) {
3968
4110
  return;
3969
4111
  }
@@ -4039,7 +4181,7 @@ const main$a = async () => {
4039
4181
  }
4040
4182
  await getEntrySubject().next(absolutePath);
4041
4183
  };
4042
- main$a();
4184
+ main$b();
4043
4185
 
4044
4186
  const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
4045
4187
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -4050,7 +4192,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
4050
4192
  const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
4051
4193
  process.on("SIGINT", BEFORE_EXIT_FN$1);
4052
4194
  });
4053
- const main$9 = async () => {
4195
+ const main$a = async () => {
4054
4196
  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)))) {
4055
4197
  return;
4056
4198
  }
@@ -4060,7 +4202,7 @@ const main$9 = async () => {
4060
4202
  }
4061
4203
  listenGracefulShutdown$1();
4062
4204
  };
4063
- main$9();
4205
+ main$a();
4064
4206
 
4065
4207
  const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
4066
4208
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -4071,7 +4213,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
4071
4213
  const listenGracefulShutdown = functoolsKit.singleshot(() => {
4072
4214
  process.on("SIGINT", BEFORE_EXIT_FN);
4073
4215
  });
4074
- const main$8 = async () => {
4216
+ const main$9 = async () => {
4075
4217
  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)))) {
4076
4218
  return;
4077
4219
  }
@@ -4081,7 +4223,7 @@ const main$8 = async () => {
4081
4223
  }
4082
4224
  listenGracefulShutdown();
4083
4225
  };
4084
- main$8();
4226
+ main$9();
4085
4227
 
4086
4228
  const EXTRACT_ROWS_FN = (plots, schema) => {
4087
4229
  const keys = Object.keys(schema);
@@ -4103,7 +4245,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
4103
4245
  }
4104
4246
  return rows;
4105
4247
  };
4106
- const main$7 = async () => {
4248
+ const main$8 = async () => {
4107
4249
  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)))) {
4108
4250
  return;
4109
4251
  }
@@ -4203,9 +4345,9 @@ const main$7 = async () => {
4203
4345
  console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
4204
4346
  process.exit(0);
4205
4347
  };
4206
- main$7();
4348
+ main$8();
4207
4349
 
4208
- const main$6 = async () => {
4350
+ const main$7 = async () => {
4209
4351
  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)))) {
4210
4352
  return;
4211
4353
  }
@@ -4268,9 +4410,9 @@ const main$6 = async () => {
4268
4410
  };
4269
4411
  process.on("SIGINT", beforeExit);
4270
4412
  };
4271
- main$6();
4413
+ main$7();
4272
4414
 
4273
- const main$5 = async () => {
4415
+ const main$6 = async () => {
4274
4416
  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)))) {
4275
4417
  return;
4276
4418
  }
@@ -4351,9 +4493,9 @@ const main$5 = async () => {
4351
4493
  console.log(JSON.stringify(candles, null, 2));
4352
4494
  process.exit(0);
4353
4495
  };
4354
- main$5();
4496
+ main$6();
4355
4497
 
4356
- const main$4 = async () => {
4498
+ const main$5 = async () => {
4357
4499
  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)))) {
4358
4500
  return;
4359
4501
  }
@@ -4467,6 +4609,236 @@ const main$4 = async () => {
4467
4609
  }
4468
4610
  process.exit(0);
4469
4611
  };
4612
+ main$5();
4613
+
4614
+ const SIMULATOR_NAME = "cli_simulator";
4615
+ const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
4616
+ const validateIdea = (idea, line) => {
4617
+ if (typeof idea !== "object" || idea === null) {
4618
+ return `line ${line}: not an object`;
4619
+ }
4620
+ if (typeof idea.id !== "number") {
4621
+ return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
4622
+ }
4623
+ if (typeof idea.ts !== "number") {
4624
+ return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
4625
+ }
4626
+ if (typeof idea.symbol !== "string" || !idea.symbol) {
4627
+ return `line ${line}: "symbol" must be a non-empty string`;
4628
+ }
4629
+ if (!IDEA_DIRECTIONS.includes(idea.direction)) {
4630
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
4631
+ }
4632
+ if (typeof idea.author !== "string" || !idea.author) {
4633
+ return `line ${line}: "author" must be a non-empty string`;
4634
+ }
4635
+ return null;
4636
+ };
4637
+ const toMarkdown = (result) => {
4638
+ const lines = [];
4639
+ lines.push(`# Simulator Report — ${result.symbol}`);
4640
+ lines.push("");
4641
+ lines.push(`| Metric | Value |`);
4642
+ lines.push(`| --- | --- |`);
4643
+ lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
4644
+ lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
4645
+ lines.push(`| Authors allowed / banned | ${result.allowedAuthors.length} / ${result.bannedAuthors.length} |`);
4646
+ lines.push(`| Grid points | ${result.reports.length} |`);
4647
+ lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
4648
+ lines.push("");
4649
+ lines.push(`## Ranking winners`);
4650
+ lines.push("");
4651
+ lines.push(`| Criterion | Stop% | Trailing% | Hold | Aligned | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4652
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4653
+ for (const best of result.best) {
4654
+ if (!best.report) {
4655
+ lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — | — | — |`);
4656
+ continue;
4657
+ }
4658
+ const { point } = best.report;
4659
+ lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.trailingTakePercent} | ${point.holdMinutes / 60}h | ${point.minIdeasAligned} | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
4660
+ `${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
4661
+ }
4662
+ lines.push("");
4663
+ lines.push(`## Allowed authors (production whitelist)`);
4664
+ lines.push("");
4665
+ lines.push(`| Author | Ideas | Hits | HitRate |`);
4666
+ lines.push(`| --- | --- | --- | --- |`);
4667
+ for (const stat of result.authorStats.filter(({ banned }) => !banned)) {
4668
+ lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
4669
+ }
4670
+ return lines.join("\n");
4671
+ };
4672
+ const main$4 = async () => {
4673
+ 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)))) {
4674
+ return;
4675
+ }
4676
+ const { values, positionals } = getArgs();
4677
+ if (!values.simulator) {
4678
+ return;
4679
+ }
4680
+ const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
4681
+ if (!ideasPath) {
4682
+ console.error("Error: positional path to an ideas .jsonl file is required");
4683
+ process.exit(1);
4684
+ }
4685
+ let ideas = [];
4686
+ {
4687
+ let content;
4688
+ try {
4689
+ content = await fs$1.readFile(path.resolve(ideasPath), "utf-8");
4690
+ }
4691
+ catch (error) {
4692
+ console.error(`Error: cannot read ideas file: ${ideasPath}`);
4693
+ process.exit(1);
4694
+ }
4695
+ const rows = content.split("\n").filter(Boolean);
4696
+ if (!rows.length) {
4697
+ console.error(`Error: ideas file is empty: ${ideasPath}`);
4698
+ process.exit(1);
4699
+ }
4700
+ for (let i = 0; i < rows.length; i++) {
4701
+ let idea;
4702
+ try {
4703
+ idea = JSON.parse(rows[i]);
4704
+ }
4705
+ catch {
4706
+ console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
4707
+ process.exit(1);
4708
+ }
4709
+ const problem = validateIdea(idea, i + 1);
4710
+ if (problem) {
4711
+ console.error(`Error: ideas file does not match the idea structure — ${problem}`);
4712
+ console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
4713
+ process.exit(1);
4714
+ }
4715
+ ideas.push(idea);
4716
+ }
4717
+ }
4718
+ {
4719
+ const cwd = process.cwd();
4720
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
4721
+ }
4722
+ await cli.configConnectionService.loadConfig("setup.config");
4723
+ {
4724
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
4725
+ try {
4726
+ if (typeof loader === "function") {
4727
+ await loader();
4728
+ }
4729
+ if (typeof loader?.loader === "function") {
4730
+ await loader.loader();
4731
+ }
4732
+ }
4733
+ catch (error) {
4734
+ console.error("Module loader failed", error);
4735
+ process.exit(-1);
4736
+ }
4737
+ }
4738
+ await cli.moduleConnectionService.loadModule("simulator.module");
4739
+ {
4740
+ await cli.exchangeSchemaService.addSchema();
4741
+ await cli.symbolSchemaService.addSchema();
4742
+ }
4743
+ const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
4744
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
4745
+ const symbol = values.symbol || "BTCUSDT";
4746
+ BacktestKit.addSimulatorSchema({
4747
+ simulatorName: SIMULATOR_NAME,
4748
+ exchangeName,
4749
+ gridAxes: {
4750
+ hardStopPercent: [1, 1.5, 2, 2.5, 3, 4, 5, 7],
4751
+ trailingTakePercent: [0.5, 1, 1.5, 2, 3, 4],
4752
+ holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
4753
+ minIdeasAligned: [1, 2, 3],
4754
+ minAuthorTrack: [2, 3, 5],
4755
+ minAuthorHitRate: [0.5, 0.6],
4756
+ minWeightAligned: [0, 0.6, 1.2],
4757
+ profitLockPercent: [0, 1.5, 2.5],
4758
+ },
4759
+ callbacks: {
4760
+ onProgress: (symbol, stage, processed, total) => {
4761
+ if (values.verbose) {
4762
+ console.log("onProgress", { symbol, stage, processed, total });
4763
+ }
4764
+ },
4765
+ onIdeas: (symbol, ideasTotal, ideasDirectional) => {
4766
+ if (values.verbose) {
4767
+ console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
4768
+ }
4769
+ },
4770
+ onProfiles: (symbol, profiles, truncatedCount) => {
4771
+ if (values.verbose) {
4772
+ console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
4773
+ }
4774
+ },
4775
+ onAuthorsTrained: (symbol, stats, bannedIdeas) => {
4776
+ if (values.verbose) {
4777
+ console.log("onAuthorsTrained", {
4778
+ symbol,
4779
+ authors: stats.length,
4780
+ banned: stats.filter(({ banned }) => banned).length,
4781
+ bannedIdeas,
4782
+ });
4783
+ }
4784
+ },
4785
+ onGridPoint: (symbol, report) => {
4786
+ if (values.verbose) {
4787
+ console.log("onGridPoint", {
4788
+ symbol,
4789
+ point: report.point,
4790
+ trades: report.trades,
4791
+ pnl: +report.totalPnlPercent.toFixed(2),
4792
+ sharpe: +report.sharpe.toFixed(2),
4793
+ });
4794
+ }
4795
+ },
4796
+ onRanking: (symbol, criterion, _sorted, best) => {
4797
+ if (values.verbose) {
4798
+ console.log("onRanking", {
4799
+ symbol,
4800
+ criterion,
4801
+ point: best.report?.point ?? null,
4802
+ pnl: best.report ? +best.report.totalPnlPercent.toFixed(2) : null,
4803
+ });
4804
+ }
4805
+ },
4806
+ onDone: (symbol, result) => {
4807
+ if (values.verbose) {
4808
+ console.log("onDone", {
4809
+ symbol,
4810
+ reports: result.reports.length,
4811
+ allowedAuthors: result.allowedAuthors.length,
4812
+ bannedAuthors: result.bannedAuthors.length,
4813
+ });
4814
+ }
4815
+ },
4816
+ },
4817
+ });
4818
+ const result = await BacktestKit.Simulator.run({
4819
+ symbol,
4820
+ simulatorName: SIMULATOR_NAME,
4821
+ ideas,
4822
+ });
4823
+ const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
4824
+ const dumpDir = path.join(process.cwd(), "dump");
4825
+ if (values.json) {
4826
+ const filePath = path.resolve(dumpDir, `${dumpName}.json`);
4827
+ await fs$1.mkdir(dumpDir, { recursive: true });
4828
+ await fs$1.writeFile(filePath, JSON.stringify(result, null, 2), "utf-8");
4829
+ console.log(`Saved: ${filePath}`);
4830
+ process.exit(0);
4831
+ }
4832
+ if (values.markdown) {
4833
+ const filePath = path.resolve(dumpDir, `${dumpName}.md`);
4834
+ await fs$1.mkdir(dumpDir, { recursive: true });
4835
+ await fs$1.writeFile(filePath, toMarkdown(result), "utf-8");
4836
+ console.log(`Saved: ${filePath}`);
4837
+ process.exit(0);
4838
+ }
4839
+ console.log(toMarkdown(result));
4840
+ process.exit(0);
4841
+ };
4470
4842
  main$4();
4471
4843
 
4472
4844
  const __filename$2 = url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)));
@@ -4665,6 +5037,7 @@ Modes:
4665
5037
  --dump Fetch and save raw OHLCV candles
4666
5038
  --pnldebug Simulate PnL per minute for a given entry price and direction
4667
5039
  --brokerdebug Fire a single broker commit against the live broker adapter
5040
+ --simulator <ideas.jsonl> Sweep exit/entry parameters over crowd ideas, train author whitelist
4668
5041
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
4669
5042
  --init Scaffold a new project in the current directory
4670
5043
  --docker Scaffold a Docker workspace for running strategies in a container
@@ -4785,6 +5158,33 @@ Broker debug flags (--brokerdebug):
4785
5158
  Loads ./live.module, fetches the last candle for --symbol/--timeframe, and calls
4786
5159
  the selected broker commit with synthetic payload values derived from current price.
4787
5160
 
5161
+ Simulator flags (--simulator):
5162
+
5163
+ --symbol <string> Trading pair to simulate (default: BTCUSDT)
5164
+ --exchange <string> Exchange name (default: first registered)
5165
+ --output <string> Output file base name (default: simulator_{SYMBOL}_{TIMESTAMP})
5166
+ --json Save full ISimulatorResult to ./dump/<output>.json
5167
+ --markdown Save summary report to ./dump/<output>.md
5168
+ --verbose Log every simulator lifecycle callback to the console
5169
+
5170
+ Positional: path to an ideas .jsonl file — one idea per line with the exact shape
5171
+ { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL",
5172
+ "author": string }. The file is validated BEFORE any work; a line that does not
5173
+ match the structure aborts the run with an error.
5174
+
5175
+ Runs the backtest-kit Simulator entity: one 5-day candle pass per idea, flood
5176
+ dedupe (one idea per author per direction per 8h), default-ban author filter
5177
+ (ban thresholds are grid axes), grid evaluation with time-based Sharpe/Sortino,
5178
+ four ranking winners. The report includes the production author whitelist.
5179
+ Ideas of other symbols are filtered out — one shared feed serves any --symbol.
5180
+
5181
+ No output flag → print the Markdown summary to stdout. With --verbose every
5182
+ simulator lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
5183
+ onGridPoint, onRanking, onDone) is logged to the console as it fires.
5184
+
5185
+ Module file ./modules/simulator.module is loaded automatically if it exists
5186
+ (register your exchange there); without it CCXT Binance is used by default.
5187
+
4788
5188
  Flush flags (--flush):
4789
5189
 
4790
5190
  One or more positional entry points. For each entry point the following
@@ -4818,6 +5218,7 @@ Module hooks (loaded automatically by each mode):
4818
5218
  modules/dump.module --dump Exchange schema for candle dumps
4819
5219
  modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
4820
5220
  modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
5221
+ modules/simulator.module --simulator Exchange schema for crowd-ideas simulation
4821
5222
 
4822
5223
  --flush has no associated module. It only removes dump subdirectories.
4823
5224
 
@@ -4846,6 +5247,8 @@ Examples:
4846
5247
  node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
4847
5248
  node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
4848
5249
  node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
5250
+ node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5251
+ node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_sweep ./assets/tv-ideas.normalized.jsonl
4849
5252
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
4850
5253
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
4851
5254
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -4859,7 +5262,7 @@ const main$1 = async () => {
4859
5262
  if (!values.help) {
4860
5263
  return;
4861
5264
  }
4862
- process.stdout.write(`@backtest-kit/cli ${"16.3.0"}\n\n`);
5265
+ process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n\n`);
4863
5266
  process.stdout.write(HELP_TEXT);
4864
5267
  process.exit(0);
4865
5268
  };
@@ -4873,7 +5276,7 @@ const main = async () => {
4873
5276
  if (!values.version) {
4874
5277
  return;
4875
5278
  }
4876
- process.stdout.write(`@backtest-kit/cli ${"16.3.0"}\n`);
5279
+ process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n`);
4877
5280
  process.exit(0);
4878
5281
  };
4879
5282
  main();