@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.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 } 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, 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, 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 } from '@backtest-kit/pinets';
31
+ import { run as run$1, Code, toMarkdown as toMarkdown$1 } 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$i = () => {
211
+ const main$j = () => {
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$i();
218
+ main$j();
219
219
 
220
220
  const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
221
221
  function dumpStackTrace(error) {
@@ -866,6 +866,10 @@ const getArgs = singleshot(() => {
866
866
  type: "boolean",
867
867
  default: false,
868
868
  },
869
+ simulator: {
870
+ type: "boolean",
871
+ default: false,
872
+ },
869
873
  commit: {
870
874
  type: "string",
871
875
  default: "",
@@ -2312,6 +2316,13 @@ const STOP_BOT_FN = singleshot(async () => {
2312
2316
  const { stopBot } = await getTelegram();
2313
2317
  stopBot();
2314
2318
  });
2319
+ /**
2320
+ * Throttle window for the per-tick order_continue decision channel: at most one
2321
+ * Telegram notification per signalId per 15 minutes via functools-kit ttl. The
2322
+ * terminal order_stop is NOT throttled (fires once per signal) and clears the
2323
+ * throttle slot for its signalId.
2324
+ */
2325
+ const ORDER_CHECK_NOTIFY_TTL = 15 * 60 * 1000;
2315
2326
  class TelegramLogicService {
2316
2327
  constructor() {
2317
2328
  this.loggerService = inject(TYPES.loggerService);
@@ -2460,11 +2471,24 @@ class TelegramLogicService {
2460
2471
  markdown,
2461
2472
  });
2462
2473
  };
2463
- this.notifySignalOpen = trycatch(async (event) => {
2464
- this.loggerService.log("telegramLogicService notifySignalOpen", {
2474
+ this.notifyOrderFillOpen = trycatch(async (event) => {
2475
+ this.loggerService.log("telegramLogicService notifyOrderFillOpen", {
2476
+ event,
2477
+ });
2478
+ const markdown = await this.telegramTemplateService.getOrderFillOpenMarkdown(event);
2479
+ if (!markdown) {
2480
+ return;
2481
+ }
2482
+ await this.telegramWebService.publishNotify({
2483
+ symbol: event.symbol,
2484
+ markdown,
2485
+ });
2486
+ });
2487
+ this.notifyOrderFillClose = trycatch(async (event) => {
2488
+ this.loggerService.log("telegramLogicService notifyOrderFillClose", {
2465
2489
  event,
2466
2490
  });
2467
- const markdown = await this.telegramTemplateService.getSignalOpenMarkdown(event);
2491
+ const markdown = await this.telegramTemplateService.getOrderFillCloseMarkdown(event);
2468
2492
  if (!markdown) {
2469
2493
  return;
2470
2494
  }
@@ -2473,11 +2497,64 @@ class TelegramLogicService {
2473
2497
  markdown,
2474
2498
  });
2475
2499
  });
2476
- this.notifySignalClose = trycatch(async (event) => {
2477
- this.loggerService.log("telegramLogicService notifySignalClose", {
2500
+ this.notifyOrderRejectOpen = trycatch(async (event) => {
2501
+ this.loggerService.log("telegramLogicService notifyOrderRejectOpen", {
2478
2502
  event,
2479
2503
  });
2480
- const markdown = await this.telegramTemplateService.getSignalCloseMarkdown(event);
2504
+ const markdown = await this.telegramTemplateService.getOrderRejectOpenMarkdown(event);
2505
+ if (!markdown) {
2506
+ return;
2507
+ }
2508
+ await this.telegramWebService.publishNotify({
2509
+ symbol: event.symbol,
2510
+ markdown,
2511
+ });
2512
+ });
2513
+ this.notifyOrderRejectClose = trycatch(async (event) => {
2514
+ this.loggerService.log("telegramLogicService notifyOrderRejectClose", {
2515
+ event,
2516
+ });
2517
+ const markdown = await this.telegramTemplateService.getOrderRejectCloseMarkdown(event);
2518
+ if (!markdown) {
2519
+ return;
2520
+ }
2521
+ await this.telegramWebService.publishNotify({
2522
+ symbol: event.symbol,
2523
+ markdown,
2524
+ });
2525
+ });
2526
+ /**
2527
+ * Post-verdict order-check CONTINUE (order_continue). Fires every live tick the
2528
+ * order survives the check, so it is throttled via functools-kit ttl:
2529
+ * one message per signalId per ORDER_CHECK_NOTIFY_TTL (15 minutes).
2530
+ */
2531
+ this.notifyOrderContinue = ttl(trycatch(async (event) => {
2532
+ this.loggerService.log("telegramLogicService notifyOrderContinue", {
2533
+ event,
2534
+ });
2535
+ const markdown = await this.telegramTemplateService.getOrderContinueMarkdown(event);
2536
+ if (!markdown) {
2537
+ return;
2538
+ }
2539
+ await this.telegramWebService.publishNotify({
2540
+ symbol: event.symbol,
2541
+ markdown,
2542
+ });
2543
+ }), {
2544
+ key: ([event]) => event.signalId,
2545
+ timeout: ORDER_CHECK_NOTIFY_TTL,
2546
+ });
2547
+ /**
2548
+ * Post-verdict order-check STOP (order_stop). Terminal — fires once per signal,
2549
+ * so no throttle; drops the continue throttle slot for this signalId
2550
+ * (the signal leaves monitoring, the ttl entry would only leak otherwise).
2551
+ */
2552
+ this.notifyOrderStop = trycatch(async (event) => {
2553
+ this.loggerService.log("telegramLogicService notifyOrderStop", {
2554
+ event,
2555
+ });
2556
+ this.notifyOrderContinue.clear(event.signalId);
2557
+ const markdown = await this.telegramTemplateService.getOrderStopMarkdown(event);
2481
2558
  if (!markdown) {
2482
2559
  return;
2483
2560
  }
@@ -2582,26 +2659,51 @@ class TelegramLogicService {
2582
2659
  return;
2583
2660
  }
2584
2661
  });
2585
- const unSync = listenSync(async (event) => {
2662
+ // Broker-CONFIRMED fills only (post-verdict orderFillSubject): a rejected or
2663
+ // transient gate attempt never reaches this channel, so "Order Filled" is
2664
+ // guaranteed truthful. The pre-verdict syncSubject is NOT consumed here.
2665
+ const unOrderFill = listenOrderFill(async (event) => {
2586
2666
  if (event.action === "signal-open") {
2587
- // type "schedule" is a resting-order PLACEMENT, not a position open:
2588
- // the user already got the "scheduled" notification from listenSignal
2589
- if (event.type !== "active") {
2590
- return;
2591
- }
2592
- await this.notifySignalOpen(event);
2667
+ // Both open outcomes notify: type "active" — the order opened at once
2668
+ // (immediate fill, no schedule phase), type "schedule" — the resting
2669
+ // order was placed on the exchange. Either way the broker confirmed a
2670
+ // real order; the template's Order field carries the type.
2671
+ await this.notifyOrderFillOpen(event);
2672
+ return;
2673
+ }
2674
+ if (event.action === "signal-close") {
2675
+ await this.notifyOrderFillClose(event);
2676
+ return;
2677
+ }
2678
+ });
2679
+ // Terminal broker rejection (post-verdict orderRejectSubject): exactly one
2680
+ // event per dropped order attempt — the engine consumes the rejected signal
2681
+ // id, so this cannot repeat per-tick for the same signal. Dispatched by
2682
+ // action to the dedicated open/close notifications (single responsibility).
2683
+ const unOrderReject = listenOrderReject(async (event) => {
2684
+ if (event.action === "signal-open") {
2685
+ await this.notifyOrderRejectOpen(event);
2593
2686
  return;
2594
2687
  }
2595
2688
  if (event.action === "signal-close") {
2596
- await this.notifySignalClose(event);
2689
+ await this.notifyOrderRejectClose(event);
2597
2690
  return;
2598
2691
  }
2599
2692
  });
2693
+ // Post-verdict check decisions: CONTINUE fires every surviving tick and is
2694
+ // throttled to one message per signalId per 15 minutes via ttl; terminal
2695
+ // STOP fires once per signal, unthrottled.
2696
+ const unOrderContinue = listenOrderContinue(async (event) => {
2697
+ await this.notifyOrderContinue(event);
2698
+ });
2699
+ const unOrderStop = listenOrderStop(async (event) => {
2700
+ await this.notifyOrderStop(event);
2701
+ });
2600
2702
  const unSignalNotify = listenSignalNotify(async (event) => {
2601
2703
  await this.notifySignalInfo(event);
2602
2704
  });
2603
2705
  const unConnect = () => this.connect.clear();
2604
- const unListen = compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unSync(), () => unSignalNotify(), () => unConnect());
2706
+ const unListen = compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unSignalNotify(), () => unConnect());
2605
2707
  return () => {
2606
2708
  STOP_BOT_FN();
2607
2709
  unListen();
@@ -2754,25 +2856,65 @@ class TelegramTemplateService {
2754
2856
  }
2755
2857
  return await RENDER_TEMPLATE_FN("average-buy.mustache", event, this);
2756
2858
  };
2757
- this.getSignalOpenMarkdown = async (event) => {
2758
- this.loggerService.log("telegramTemplateService getSignalOpenMarkdown", {
2859
+ this.getOrderFillOpenMarkdown = async (event) => {
2860
+ this.loggerService.log("telegramTemplateService getOrderFillOpenMarkdown", {
2861
+ event,
2862
+ });
2863
+ const adapter = await this.getTelegramAdapter();
2864
+ if (adapter?.getOrderFillOpenMarkdown) {
2865
+ return await adapter.getOrderFillOpenMarkdown(event);
2866
+ }
2867
+ return await RENDER_TEMPLATE_FN("order-fill-open.mustache", event, this);
2868
+ };
2869
+ this.getOrderFillCloseMarkdown = async (event) => {
2870
+ this.loggerService.log("telegramTemplateService getOrderFillCloseMarkdown", {
2871
+ event,
2872
+ });
2873
+ const adapter = await this.getTelegramAdapter();
2874
+ if (adapter?.getOrderFillCloseMarkdown) {
2875
+ return await adapter.getOrderFillCloseMarkdown(event);
2876
+ }
2877
+ return await RENDER_TEMPLATE_FN("order-fill-close.mustache", event, this);
2878
+ };
2879
+ this.getOrderRejectOpenMarkdown = async (event) => {
2880
+ this.loggerService.log("telegramTemplateService getOrderRejectOpenMarkdown", {
2881
+ event,
2882
+ });
2883
+ const adapter = await this.getTelegramAdapter();
2884
+ if (adapter?.getOrderRejectOpenMarkdown) {
2885
+ return await adapter.getOrderRejectOpenMarkdown(event);
2886
+ }
2887
+ return await RENDER_TEMPLATE_FN("order-reject-open.mustache", event, this);
2888
+ };
2889
+ this.getOrderRejectCloseMarkdown = async (event) => {
2890
+ this.loggerService.log("telegramTemplateService getOrderRejectCloseMarkdown", {
2891
+ event,
2892
+ });
2893
+ const adapter = await this.getTelegramAdapter();
2894
+ if (adapter?.getOrderRejectCloseMarkdown) {
2895
+ return await adapter.getOrderRejectCloseMarkdown(event);
2896
+ }
2897
+ return await RENDER_TEMPLATE_FN("order-reject-close.mustache", event, this);
2898
+ };
2899
+ this.getOrderContinueMarkdown = async (event) => {
2900
+ this.loggerService.log("telegramTemplateService getOrderContinueMarkdown", {
2759
2901
  event,
2760
2902
  });
2761
2903
  const adapter = await this.getTelegramAdapter();
2762
- if (adapter?.getSignalOpenMarkdown) {
2763
- return await adapter.getSignalOpenMarkdown(event);
2904
+ if (adapter?.getOrderContinueMarkdown) {
2905
+ return await adapter.getOrderContinueMarkdown(event);
2764
2906
  }
2765
- return await RENDER_TEMPLATE_FN("signal-open.mustache", event, this);
2907
+ return await RENDER_TEMPLATE_FN("order-continue.mustache", event, this);
2766
2908
  };
2767
- this.getSignalCloseMarkdown = async (event) => {
2768
- this.loggerService.log("telegramTemplateService getSignalCloseMarkdown", {
2909
+ this.getOrderStopMarkdown = async (event) => {
2910
+ this.loggerService.log("telegramTemplateService getOrderStopMarkdown", {
2769
2911
  event,
2770
2912
  });
2771
2913
  const adapter = await this.getTelegramAdapter();
2772
- if (adapter?.getSignalCloseMarkdown) {
2773
- return await adapter.getSignalCloseMarkdown(event);
2914
+ if (adapter?.getOrderStopMarkdown) {
2915
+ return await adapter.getOrderStopMarkdown(event);
2774
2916
  }
2775
- return await RENDER_TEMPLATE_FN("signal-close.mustache", event, this);
2917
+ return await RENDER_TEMPLATE_FN("order-stop.mustache", event, this);
2776
2918
  };
2777
2919
  this.getCancelScheduledMarkdown = async (event) => {
2778
2920
  this.loggerService.log("telegramTemplateService getCancelScheduledMarkdown", {
@@ -3502,14 +3644,14 @@ const cli = {
3502
3644
  };
3503
3645
  init();
3504
3646
 
3505
- const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "flush", "init", "docker", "help", "version"];
3647
+ const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "flush", "init", "docker", "help", "version"];
3506
3648
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
3507
3649
  const HELP_TEXT$1 = `
3508
3650
  Example:
3509
3651
 
3510
3652
  node ${ENTRY_PATH$1} --help
3511
3653
  `.trimStart();
3512
- const main$h = async () => {
3654
+ const main$i = async () => {
3513
3655
  if (!getEntry(import.meta.url)) {
3514
3656
  return;
3515
3657
  }
@@ -3517,14 +3659,14 @@ const main$h = async () => {
3517
3659
  if (MODES.some((mode) => values[mode])) {
3518
3660
  return;
3519
3661
  }
3520
- process.stdout.write(`@backtest-kit/cli ${"16.3.0"}\n`);
3662
+ process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n`);
3521
3663
  process.stdout.write("\n");
3522
3664
  process.stdout.write(`Run with --help to see available commands.\n`);
3523
3665
  process.stdout.write("\n");
3524
3666
  process.stdout.write(HELP_TEXT$1);
3525
3667
  process.exit(0);
3526
3668
  };
3527
- main$h();
3669
+ main$i();
3528
3670
 
3529
3671
  const notifyShutdown = singleshot(async () => {
3530
3672
  console.log("Graceful shutdown initiated.");
@@ -3540,7 +3682,7 @@ const flush = async (entryPoint) => {
3540
3682
  console.log(`Removed: ${target}`);
3541
3683
  }
3542
3684
  };
3543
- const main$g = async () => {
3685
+ const main$h = async () => {
3544
3686
  if (!getEntry(import.meta.url)) {
3545
3687
  return;
3546
3688
  }
@@ -3557,7 +3699,7 @@ const main$g = async () => {
3557
3699
  }
3558
3700
  process.exit(0);
3559
3701
  };
3560
- main$g();
3702
+ main$h();
3561
3703
 
3562
3704
  const BEFORE_EXIT_FN$6 = singleshot(async () => {
3563
3705
  process.off("SIGINT", BEFORE_EXIT_FN$6);
@@ -3580,7 +3722,7 @@ const BEFORE_EXIT_FN$6 = singleshot(async () => {
3580
3722
  const listenGracefulShutdown$6 = singleshot(() => {
3581
3723
  process.on("SIGINT", BEFORE_EXIT_FN$6);
3582
3724
  });
3583
- const main$f = async () => {
3725
+ const main$g = async () => {
3584
3726
  if (!getEntry(import.meta.url)) {
3585
3727
  return;
3586
3728
  }
@@ -3598,7 +3740,7 @@ const main$f = async () => {
3598
3740
  await cli.backtestMainService.connect();
3599
3741
  listenGracefulShutdown$6();
3600
3742
  };
3601
- main$f();
3743
+ main$g();
3602
3744
 
3603
3745
  const BEFORE_EXIT_FN$5 = singleshot(async () => {
3604
3746
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3617,7 +3759,7 @@ const BEFORE_EXIT_FN$5 = singleshot(async () => {
3617
3759
  const listenGracefulShutdown$5 = singleshot(() => {
3618
3760
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3619
3761
  });
3620
- const main$e = async () => {
3762
+ const main$f = async () => {
3621
3763
  if (!getEntry(import.meta.url)) {
3622
3764
  return;
3623
3765
  }
@@ -3636,7 +3778,7 @@ const main$e = async () => {
3636
3778
  listenGracefulShutdown$5();
3637
3779
  await cli.walkerMainService.connect();
3638
3780
  };
3639
- main$e();
3781
+ main$f();
3640
3782
 
3641
3783
  const BEFORE_EXIT_FN$4 = singleshot(async () => {
3642
3784
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3658,7 +3800,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
3658
3800
  const listenGracefulShutdown$4 = singleshot(() => {
3659
3801
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3660
3802
  });
3661
- const main$d = async () => {
3803
+ const main$e = async () => {
3662
3804
  if (!getEntry(import.meta.url)) {
3663
3805
  return;
3664
3806
  }
@@ -3672,7 +3814,7 @@ const main$d = async () => {
3672
3814
  await cli.paperMainService.connect();
3673
3815
  listenGracefulShutdown$4();
3674
3816
  };
3675
- main$d();
3817
+ main$e();
3676
3818
 
3677
3819
  const BEFORE_EXIT_FN$3 = singleshot(async () => {
3678
3820
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3694,7 +3836,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
3694
3836
  const listenGracefulShutdown$3 = singleshot(() => {
3695
3837
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3696
3838
  });
3697
- const main$c = async () => {
3839
+ const main$d = async () => {
3698
3840
  if (!getEntry(import.meta.url)) {
3699
3841
  return;
3700
3842
  }
@@ -3708,7 +3850,7 @@ const main$c = async () => {
3708
3850
  await cli.liveMainService.connect();
3709
3851
  listenGracefulShutdown$3();
3710
3852
  };
3711
- main$c();
3853
+ main$d();
3712
3854
 
3713
3855
  const stopBacktestList$1 = async () => {
3714
3856
  for (const item of await Backtest.list()) {
@@ -3772,7 +3914,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
3772
3914
  const listenGracefulShutdown$2 = singleshot(() => {
3773
3915
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3774
3916
  });
3775
- const main$b = async () => {
3917
+ const main$c = async () => {
3776
3918
  if (!getEntry(import.meta.url)) {
3777
3919
  return;
3778
3920
  }
@@ -3828,7 +3970,7 @@ const main$b = async () => {
3828
3970
  listenGracefulShutdown$2();
3829
3971
  await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
3830
3972
  };
3831
- main$b();
3973
+ main$c();
3832
3974
 
3833
3975
  const MODE_MODULE = {
3834
3976
  backtest: "backtest.module",
@@ -3931,7 +4073,7 @@ const createGracefulShutdown = (mode) => {
3931
4073
  process.on("SIGINT", handler);
3932
4074
  });
3933
4075
  };
3934
- const main$a = async () => {
4076
+ const main$b = async () => {
3935
4077
  if (!getEntry(import.meta.url)) {
3936
4078
  return;
3937
4079
  }
@@ -4007,7 +4149,7 @@ const main$a = async () => {
4007
4149
  }
4008
4150
  await getEntrySubject().next(absolutePath);
4009
4151
  };
4010
- main$a();
4152
+ main$b();
4011
4153
 
4012
4154
  const BEFORE_EXIT_FN$1 = singleshot(async () => {
4013
4155
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -4018,7 +4160,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
4018
4160
  const listenGracefulShutdown$1 = singleshot(() => {
4019
4161
  process.on("SIGINT", BEFORE_EXIT_FN$1);
4020
4162
  });
4021
- const main$9 = async () => {
4163
+ const main$a = async () => {
4022
4164
  if (!getEntry(import.meta.url)) {
4023
4165
  return;
4024
4166
  }
@@ -4028,7 +4170,7 @@ const main$9 = async () => {
4028
4170
  }
4029
4171
  listenGracefulShutdown$1();
4030
4172
  };
4031
- main$9();
4173
+ main$a();
4032
4174
 
4033
4175
  const BEFORE_EXIT_FN = singleshot(async () => {
4034
4176
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -4039,7 +4181,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
4039
4181
  const listenGracefulShutdown = singleshot(() => {
4040
4182
  process.on("SIGINT", BEFORE_EXIT_FN);
4041
4183
  });
4042
- const main$8 = async () => {
4184
+ const main$9 = async () => {
4043
4185
  if (!getEntry(import.meta.url)) {
4044
4186
  return;
4045
4187
  }
@@ -4049,7 +4191,7 @@ const main$8 = async () => {
4049
4191
  }
4050
4192
  listenGracefulShutdown();
4051
4193
  };
4052
- main$8();
4194
+ main$9();
4053
4195
 
4054
4196
  const EXTRACT_ROWS_FN = (plots, schema) => {
4055
4197
  const keys = Object.keys(schema);
@@ -4071,7 +4213,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
4071
4213
  }
4072
4214
  return rows;
4073
4215
  };
4074
- const main$7 = async () => {
4216
+ const main$8 = async () => {
4075
4217
  if (!getEntry(import.meta.url)) {
4076
4218
  return;
4077
4219
  }
@@ -4163,17 +4305,17 @@ const main$7 = async () => {
4163
4305
  if (values.markdown) {
4164
4306
  const filePath = resolve(dumpDir, `${dumpName}.md`);
4165
4307
  await mkdir(dumpDir, { recursive: true });
4166
- await writeFile(filePath, await toMarkdown(signalId, plots, signalSchema), "utf-8");
4308
+ await writeFile(filePath, await toMarkdown$1(signalId, plots, signalSchema), "utf-8");
4167
4309
  console.log(`Saved: ${filePath}`);
4168
4310
  process.exit(0);
4169
4311
  return;
4170
4312
  }
4171
- console.log(await toMarkdown(signalId, plots, signalSchema));
4313
+ console.log(await toMarkdown$1(signalId, plots, signalSchema));
4172
4314
  process.exit(0);
4173
4315
  };
4174
- main$7();
4316
+ main$8();
4175
4317
 
4176
- const main$6 = async () => {
4318
+ const main$7 = async () => {
4177
4319
  if (!getEntry(import.meta.url)) {
4178
4320
  return;
4179
4321
  }
@@ -4236,9 +4378,9 @@ const main$6 = async () => {
4236
4378
  };
4237
4379
  process.on("SIGINT", beforeExit);
4238
4380
  };
4239
- main$6();
4381
+ main$7();
4240
4382
 
4241
- const main$5 = async () => {
4383
+ const main$6 = async () => {
4242
4384
  if (!getEntry(import.meta.url)) {
4243
4385
  return;
4244
4386
  }
@@ -4319,9 +4461,9 @@ const main$5 = async () => {
4319
4461
  console.log(JSON.stringify(candles, null, 2));
4320
4462
  process.exit(0);
4321
4463
  };
4322
- main$5();
4464
+ main$6();
4323
4465
 
4324
- const main$4 = async () => {
4466
+ const main$5 = async () => {
4325
4467
  if (!getEntry(import.meta.url)) {
4326
4468
  return;
4327
4469
  }
@@ -4435,6 +4577,236 @@ const main$4 = async () => {
4435
4577
  }
4436
4578
  process.exit(0);
4437
4579
  };
4580
+ main$5();
4581
+
4582
+ const SIMULATOR_NAME = "cli_simulator";
4583
+ const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
4584
+ const validateIdea = (idea, line) => {
4585
+ if (typeof idea !== "object" || idea === null) {
4586
+ return `line ${line}: not an object`;
4587
+ }
4588
+ if (typeof idea.id !== "number") {
4589
+ return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
4590
+ }
4591
+ if (typeof idea.ts !== "number") {
4592
+ return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
4593
+ }
4594
+ if (typeof idea.symbol !== "string" || !idea.symbol) {
4595
+ return `line ${line}: "symbol" must be a non-empty string`;
4596
+ }
4597
+ if (!IDEA_DIRECTIONS.includes(idea.direction)) {
4598
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
4599
+ }
4600
+ if (typeof idea.author !== "string" || !idea.author) {
4601
+ return `line ${line}: "author" must be a non-empty string`;
4602
+ }
4603
+ return null;
4604
+ };
4605
+ const toMarkdown = (result) => {
4606
+ const lines = [];
4607
+ lines.push(`# Simulator Report — ${result.symbol}`);
4608
+ lines.push("");
4609
+ lines.push(`| Metric | Value |`);
4610
+ lines.push(`| --- | --- |`);
4611
+ lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
4612
+ lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
4613
+ lines.push(`| Authors allowed / banned | ${result.allowedAuthors.length} / ${result.bannedAuthors.length} |`);
4614
+ lines.push(`| Grid points | ${result.reports.length} |`);
4615
+ lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
4616
+ lines.push("");
4617
+ lines.push(`## Ranking winners`);
4618
+ lines.push("");
4619
+ lines.push(`| Criterion | Stop% | Trailing% | Hold | Aligned | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4620
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4621
+ for (const best of result.best) {
4622
+ if (!best.report) {
4623
+ lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — | — | — |`);
4624
+ continue;
4625
+ }
4626
+ const { point } = best.report;
4627
+ lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.trailingTakePercent} | ${point.holdMinutes / 60}h | ${point.minIdeasAligned} | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
4628
+ `${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
4629
+ }
4630
+ lines.push("");
4631
+ lines.push(`## Allowed authors (production whitelist)`);
4632
+ lines.push("");
4633
+ lines.push(`| Author | Ideas | Hits | HitRate |`);
4634
+ lines.push(`| --- | --- | --- | --- |`);
4635
+ for (const stat of result.authorStats.filter(({ banned }) => !banned)) {
4636
+ lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
4637
+ }
4638
+ return lines.join("\n");
4639
+ };
4640
+ const main$4 = async () => {
4641
+ if (!getEntry(import.meta.url)) {
4642
+ return;
4643
+ }
4644
+ const { values, positionals } = getArgs();
4645
+ if (!values.simulator) {
4646
+ return;
4647
+ }
4648
+ const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
4649
+ if (!ideasPath) {
4650
+ console.error("Error: positional path to an ideas .jsonl file is required");
4651
+ process.exit(1);
4652
+ }
4653
+ let ideas = [];
4654
+ {
4655
+ let content;
4656
+ try {
4657
+ content = await readFile(resolve(ideasPath), "utf-8");
4658
+ }
4659
+ catch (error) {
4660
+ console.error(`Error: cannot read ideas file: ${ideasPath}`);
4661
+ process.exit(1);
4662
+ }
4663
+ const rows = content.split("\n").filter(Boolean);
4664
+ if (!rows.length) {
4665
+ console.error(`Error: ideas file is empty: ${ideasPath}`);
4666
+ process.exit(1);
4667
+ }
4668
+ for (let i = 0; i < rows.length; i++) {
4669
+ let idea;
4670
+ try {
4671
+ idea = JSON.parse(rows[i]);
4672
+ }
4673
+ catch {
4674
+ console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
4675
+ process.exit(1);
4676
+ }
4677
+ const problem = validateIdea(idea, i + 1);
4678
+ if (problem) {
4679
+ console.error(`Error: ideas file does not match the idea structure — ${problem}`);
4680
+ console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
4681
+ process.exit(1);
4682
+ }
4683
+ ideas.push(idea);
4684
+ }
4685
+ }
4686
+ {
4687
+ const cwd = process.cwd();
4688
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
4689
+ }
4690
+ await cli.configConnectionService.loadConfig("setup.config");
4691
+ {
4692
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
4693
+ try {
4694
+ if (typeof loader === "function") {
4695
+ await loader();
4696
+ }
4697
+ if (typeof loader?.loader === "function") {
4698
+ await loader.loader();
4699
+ }
4700
+ }
4701
+ catch (error) {
4702
+ console.error("Module loader failed", error);
4703
+ process.exit(-1);
4704
+ }
4705
+ }
4706
+ await cli.moduleConnectionService.loadModule("simulator.module");
4707
+ {
4708
+ await cli.exchangeSchemaService.addSchema();
4709
+ await cli.symbolSchemaService.addSchema();
4710
+ }
4711
+ const [defaultExchangeName = null] = await listExchangeSchema();
4712
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
4713
+ const symbol = values.symbol || "BTCUSDT";
4714
+ addSimulatorSchema({
4715
+ simulatorName: SIMULATOR_NAME,
4716
+ exchangeName,
4717
+ gridAxes: {
4718
+ hardStopPercent: [1, 1.5, 2, 2.5, 3, 4, 5, 7],
4719
+ trailingTakePercent: [0.5, 1, 1.5, 2, 3, 4],
4720
+ holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
4721
+ minIdeasAligned: [1, 2, 3],
4722
+ minAuthorTrack: [2, 3, 5],
4723
+ minAuthorHitRate: [0.5, 0.6],
4724
+ minWeightAligned: [0, 0.6, 1.2],
4725
+ profitLockPercent: [0, 1.5, 2.5],
4726
+ },
4727
+ callbacks: {
4728
+ onProgress: (symbol, stage, processed, total) => {
4729
+ if (values.verbose) {
4730
+ console.log("onProgress", { symbol, stage, processed, total });
4731
+ }
4732
+ },
4733
+ onIdeas: (symbol, ideasTotal, ideasDirectional) => {
4734
+ if (values.verbose) {
4735
+ console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
4736
+ }
4737
+ },
4738
+ onProfiles: (symbol, profiles, truncatedCount) => {
4739
+ if (values.verbose) {
4740
+ console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
4741
+ }
4742
+ },
4743
+ onAuthorsTrained: (symbol, stats, bannedIdeas) => {
4744
+ if (values.verbose) {
4745
+ console.log("onAuthorsTrained", {
4746
+ symbol,
4747
+ authors: stats.length,
4748
+ banned: stats.filter(({ banned }) => banned).length,
4749
+ bannedIdeas,
4750
+ });
4751
+ }
4752
+ },
4753
+ onGridPoint: (symbol, report) => {
4754
+ if (values.verbose) {
4755
+ console.log("onGridPoint", {
4756
+ symbol,
4757
+ point: report.point,
4758
+ trades: report.trades,
4759
+ pnl: +report.totalPnlPercent.toFixed(2),
4760
+ sharpe: +report.sharpe.toFixed(2),
4761
+ });
4762
+ }
4763
+ },
4764
+ onRanking: (symbol, criterion, _sorted, best) => {
4765
+ if (values.verbose) {
4766
+ console.log("onRanking", {
4767
+ symbol,
4768
+ criterion,
4769
+ point: best.report?.point ?? null,
4770
+ pnl: best.report ? +best.report.totalPnlPercent.toFixed(2) : null,
4771
+ });
4772
+ }
4773
+ },
4774
+ onDone: (symbol, result) => {
4775
+ if (values.verbose) {
4776
+ console.log("onDone", {
4777
+ symbol,
4778
+ reports: result.reports.length,
4779
+ allowedAuthors: result.allowedAuthors.length,
4780
+ bannedAuthors: result.bannedAuthors.length,
4781
+ });
4782
+ }
4783
+ },
4784
+ },
4785
+ });
4786
+ const result = await Simulator.run({
4787
+ symbol,
4788
+ simulatorName: SIMULATOR_NAME,
4789
+ ideas,
4790
+ });
4791
+ const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
4792
+ const dumpDir = join(process.cwd(), "dump");
4793
+ if (values.json) {
4794
+ const filePath = resolve(dumpDir, `${dumpName}.json`);
4795
+ await mkdir(dumpDir, { recursive: true });
4796
+ await writeFile(filePath, JSON.stringify(result, null, 2), "utf-8");
4797
+ console.log(`Saved: ${filePath}`);
4798
+ process.exit(0);
4799
+ }
4800
+ if (values.markdown) {
4801
+ const filePath = resolve(dumpDir, `${dumpName}.md`);
4802
+ await mkdir(dumpDir, { recursive: true });
4803
+ await writeFile(filePath, toMarkdown(result), "utf-8");
4804
+ console.log(`Saved: ${filePath}`);
4805
+ process.exit(0);
4806
+ }
4807
+ console.log(toMarkdown(result));
4808
+ process.exit(0);
4809
+ };
4438
4810
  main$4();
4439
4811
 
4440
4812
  const __filename$1 = fileURLToPath(import.meta.url);
@@ -4633,6 +5005,7 @@ Modes:
4633
5005
  --dump Fetch and save raw OHLCV candles
4634
5006
  --pnldebug Simulate PnL per minute for a given entry price and direction
4635
5007
  --brokerdebug Fire a single broker commit against the live broker adapter
5008
+ --simulator <ideas.jsonl> Sweep exit/entry parameters over crowd ideas, train author whitelist
4636
5009
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
4637
5010
  --init Scaffold a new project in the current directory
4638
5011
  --docker Scaffold a Docker workspace for running strategies in a container
@@ -4753,6 +5126,33 @@ Broker debug flags (--brokerdebug):
4753
5126
  Loads ./live.module, fetches the last candle for --symbol/--timeframe, and calls
4754
5127
  the selected broker commit with synthetic payload values derived from current price.
4755
5128
 
5129
+ Simulator flags (--simulator):
5130
+
5131
+ --symbol <string> Trading pair to simulate (default: BTCUSDT)
5132
+ --exchange <string> Exchange name (default: first registered)
5133
+ --output <string> Output file base name (default: simulator_{SYMBOL}_{TIMESTAMP})
5134
+ --json Save full ISimulatorResult to ./dump/<output>.json
5135
+ --markdown Save summary report to ./dump/<output>.md
5136
+ --verbose Log every simulator lifecycle callback to the console
5137
+
5138
+ Positional: path to an ideas .jsonl file — one idea per line with the exact shape
5139
+ { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL",
5140
+ "author": string }. The file is validated BEFORE any work; a line that does not
5141
+ match the structure aborts the run with an error.
5142
+
5143
+ Runs the backtest-kit Simulator entity: one 5-day candle pass per idea, flood
5144
+ dedupe (one idea per author per direction per 8h), default-ban author filter
5145
+ (ban thresholds are grid axes), grid evaluation with time-based Sharpe/Sortino,
5146
+ four ranking winners. The report includes the production author whitelist.
5147
+ Ideas of other symbols are filtered out — one shared feed serves any --symbol.
5148
+
5149
+ No output flag → print the Markdown summary to stdout. With --verbose every
5150
+ simulator lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
5151
+ onGridPoint, onRanking, onDone) is logged to the console as it fires.
5152
+
5153
+ Module file ./modules/simulator.module is loaded automatically if it exists
5154
+ (register your exchange there); without it CCXT Binance is used by default.
5155
+
4756
5156
  Flush flags (--flush):
4757
5157
 
4758
5158
  One or more positional entry points. For each entry point the following
@@ -4786,6 +5186,7 @@ Module hooks (loaded automatically by each mode):
4786
5186
  modules/dump.module --dump Exchange schema for candle dumps
4787
5187
  modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
4788
5188
  modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
5189
+ modules/simulator.module --simulator Exchange schema for crowd-ideas simulation
4789
5190
 
4790
5191
  --flush has no associated module. It only removes dump subdirectories.
4791
5192
 
@@ -4814,6 +5215,8 @@ Examples:
4814
5215
  node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
4815
5216
  node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
4816
5217
  node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
5218
+ node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5219
+ node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_sweep ./assets/tv-ideas.normalized.jsonl
4817
5220
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
4818
5221
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
4819
5222
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -4827,7 +5230,7 @@ const main$1 = async () => {
4827
5230
  if (!values.help) {
4828
5231
  return;
4829
5232
  }
4830
- process.stdout.write(`@backtest-kit/cli ${"16.3.0"}\n\n`);
5233
+ process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n\n`);
4831
5234
  process.stdout.write(HELP_TEXT);
4832
5235
  process.exit(0);
4833
5236
  };
@@ -4841,7 +5244,7 @@ const main = async () => {
4841
5244
  if (!values.version) {
4842
5245
  return;
4843
5246
  }
4844
- process.stdout.write(`@backtest-kit/cli ${"16.3.0"}\n`);
5247
+ process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n`);
4845
5248
  process.exit(0);
4846
5249
  };
4847
5250
  main();