@backtest-kit/cli 16.5.0 → 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -233,14 +233,14 @@ const getReadySubject = functoolsKit.singleshot(() => {
233
233
  return new functoolsKit.Subject();
234
234
  });
235
235
 
236
- const main$j = () => {
236
+ const main$l = () => {
237
237
  const entrySubject = getEntrySubject();
238
238
  entrySubject.subscribe(async (path) => {
239
239
  console.log("Running", path);
240
240
  await getReadySubject().next();
241
241
  });
242
242
  };
243
- main$j();
243
+ main$l();
244
244
 
245
245
  const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
246
246
  function dumpStackTrace(error) {
@@ -895,6 +895,10 @@ const getArgs = functoolsKit.singleshot(() => {
895
895
  type: "boolean",
896
896
  default: false,
897
897
  },
898
+ tune: {
899
+ type: "boolean",
900
+ default: false,
901
+ },
898
902
  commit: {
899
903
  type: "string",
900
904
  default: "",
@@ -2727,8 +2731,11 @@ class TelegramLogicService {
2727
2731
  const unSignalNotify = BacktestKit.listenSignalNotify(async (event) => {
2728
2732
  await this.notifySignalInfo(event);
2729
2733
  });
2734
+ const unGc = functoolsKit.Source.fromInterval(90000).connect(() => {
2735
+ this.notifyOrderContinue.gc();
2736
+ });
2730
2737
  const unConnect = () => this.connect.clear();
2731
- const unListen = functoolsKit.compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unSignalNotify(), () => unConnect());
2738
+ const unListen = functoolsKit.compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unGc(), () => unSignalNotify(), () => unConnect());
2732
2739
  return () => {
2733
2740
  STOP_BOT_FN();
2734
2741
  unListen();
@@ -3676,14 +3683,14 @@ const cli = {
3676
3683
  };
3677
3684
  init();
3678
3685
 
3679
- const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "flush", "init", "docker", "help", "version"];
3686
+ const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "tune", "flush", "init", "docker", "help", "version"];
3680
3687
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
3681
3688
  const HELP_TEXT$1 = `
3682
3689
  Example:
3683
3690
 
3684
3691
  node ${ENTRY_PATH$1} --help
3685
3692
  `.trimStart();
3686
- const main$i = async () => {
3693
+ const main$k = async () => {
3687
3694
  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)))) {
3688
3695
  return;
3689
3696
  }
@@ -3691,14 +3698,14 @@ const main$i = async () => {
3691
3698
  if (MODES.some((mode) => values[mode])) {
3692
3699
  return;
3693
3700
  }
3694
- process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n`);
3701
+ process.stdout.write(`@backtest-kit/cli ${"17.0.0"}\n`);
3695
3702
  process.stdout.write("\n");
3696
3703
  process.stdout.write(`Run with --help to see available commands.\n`);
3697
3704
  process.stdout.write("\n");
3698
3705
  process.stdout.write(HELP_TEXT$1);
3699
3706
  process.exit(0);
3700
3707
  };
3701
- main$i();
3708
+ main$k();
3702
3709
 
3703
3710
  const notifyShutdown = functoolsKit.singleshot(async () => {
3704
3711
  console.log("Graceful shutdown initiated.");
@@ -3714,7 +3721,7 @@ const flush = async (entryPoint) => {
3714
3721
  console.log(`Removed: ${target}`);
3715
3722
  }
3716
3723
  };
3717
- const main$h = async () => {
3724
+ const main$j = async () => {
3718
3725
  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)))) {
3719
3726
  return;
3720
3727
  }
@@ -3731,7 +3738,7 @@ const main$h = async () => {
3731
3738
  }
3732
3739
  process.exit(0);
3733
3740
  };
3734
- main$h();
3741
+ main$j();
3735
3742
 
3736
3743
  const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
3737
3744
  process.off("SIGINT", BEFORE_EXIT_FN$6);
@@ -3754,7 +3761,7 @@ const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
3754
3761
  const listenGracefulShutdown$6 = functoolsKit.singleshot(() => {
3755
3762
  process.on("SIGINT", BEFORE_EXIT_FN$6);
3756
3763
  });
3757
- const main$g = async () => {
3764
+ const main$i = async () => {
3758
3765
  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)))) {
3759
3766
  return;
3760
3767
  }
@@ -3772,7 +3779,7 @@ const main$g = async () => {
3772
3779
  await cli.backtestMainService.connect();
3773
3780
  listenGracefulShutdown$6();
3774
3781
  };
3775
- main$g();
3782
+ main$i();
3776
3783
 
3777
3784
  const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3778
3785
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3791,7 +3798,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3791
3798
  const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
3792
3799
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3793
3800
  });
3794
- const main$f = async () => {
3801
+ const main$h = async () => {
3795
3802
  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)))) {
3796
3803
  return;
3797
3804
  }
@@ -3810,7 +3817,7 @@ const main$f = async () => {
3810
3817
  listenGracefulShutdown$5();
3811
3818
  await cli.walkerMainService.connect();
3812
3819
  };
3813
- main$f();
3820
+ main$h();
3814
3821
 
3815
3822
  const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3816
3823
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3832,7 +3839,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3832
3839
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
3833
3840
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3834
3841
  });
3835
- const main$e = async () => {
3842
+ const main$g = async () => {
3836
3843
  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)))) {
3837
3844
  return;
3838
3845
  }
@@ -3846,7 +3853,7 @@ const main$e = async () => {
3846
3853
  await cli.paperMainService.connect();
3847
3854
  listenGracefulShutdown$4();
3848
3855
  };
3849
- main$e();
3856
+ main$g();
3850
3857
 
3851
3858
  const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3852
3859
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3868,7 +3875,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3868
3875
  const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
3869
3876
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3870
3877
  });
3871
- const main$d = async () => {
3878
+ const main$f = async () => {
3872
3879
  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)))) {
3873
3880
  return;
3874
3881
  }
@@ -3882,7 +3889,7 @@ const main$d = async () => {
3882
3889
  await cli.liveMainService.connect();
3883
3890
  listenGracefulShutdown$3();
3884
3891
  };
3885
- main$d();
3892
+ main$f();
3886
3893
 
3887
3894
  const stopBacktestList$1 = async () => {
3888
3895
  for (const item of await BacktestKit.Backtest.list()) {
@@ -3946,7 +3953,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
3946
3953
  const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
3947
3954
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3948
3955
  });
3949
- const main$c = async () => {
3956
+ const main$e = async () => {
3950
3957
  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)))) {
3951
3958
  return;
3952
3959
  }
@@ -4002,7 +4009,7 @@ const main$c = async () => {
4002
4009
  listenGracefulShutdown$2();
4003
4010
  await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
4004
4011
  };
4005
- main$c();
4012
+ main$e();
4006
4013
 
4007
4014
  const MODE_MODULE = {
4008
4015
  backtest: "backtest.module",
@@ -4105,7 +4112,7 @@ const createGracefulShutdown = (mode) => {
4105
4112
  process.on("SIGINT", handler);
4106
4113
  });
4107
4114
  };
4108
- const main$b = async () => {
4115
+ const main$d = async () => {
4109
4116
  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)))) {
4110
4117
  return;
4111
4118
  }
@@ -4181,7 +4188,7 @@ const main$b = async () => {
4181
4188
  }
4182
4189
  await getEntrySubject().next(absolutePath);
4183
4190
  };
4184
- main$b();
4191
+ main$d();
4185
4192
 
4186
4193
  const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
4187
4194
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -4192,7 +4199,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
4192
4199
  const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
4193
4200
  process.on("SIGINT", BEFORE_EXIT_FN$1);
4194
4201
  });
4195
- const main$a = async () => {
4202
+ const main$c = async () => {
4196
4203
  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)))) {
4197
4204
  return;
4198
4205
  }
@@ -4202,7 +4209,7 @@ const main$a = async () => {
4202
4209
  }
4203
4210
  listenGracefulShutdown$1();
4204
4211
  };
4205
- main$a();
4212
+ main$c();
4206
4213
 
4207
4214
  const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
4208
4215
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -4213,7 +4220,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
4213
4220
  const listenGracefulShutdown = functoolsKit.singleshot(() => {
4214
4221
  process.on("SIGINT", BEFORE_EXIT_FN);
4215
4222
  });
4216
- const main$9 = async () => {
4223
+ const main$b = async () => {
4217
4224
  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)))) {
4218
4225
  return;
4219
4226
  }
@@ -4223,7 +4230,7 @@ const main$9 = async () => {
4223
4230
  }
4224
4231
  listenGracefulShutdown();
4225
4232
  };
4226
- main$9();
4233
+ main$b();
4227
4234
 
4228
4235
  const EXTRACT_ROWS_FN = (plots, schema) => {
4229
4236
  const keys = Object.keys(schema);
@@ -4245,7 +4252,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
4245
4252
  }
4246
4253
  return rows;
4247
4254
  };
4248
- const main$8 = async () => {
4255
+ const main$a = async () => {
4249
4256
  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)))) {
4250
4257
  return;
4251
4258
  }
@@ -4345,9 +4352,9 @@ const main$8 = async () => {
4345
4352
  console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
4346
4353
  process.exit(0);
4347
4354
  };
4348
- main$8();
4355
+ main$a();
4349
4356
 
4350
- const main$7 = async () => {
4357
+ const main$9 = async () => {
4351
4358
  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)))) {
4352
4359
  return;
4353
4360
  }
@@ -4410,9 +4417,9 @@ const main$7 = async () => {
4410
4417
  };
4411
4418
  process.on("SIGINT", beforeExit);
4412
4419
  };
4413
- main$7();
4420
+ main$9();
4414
4421
 
4415
- const main$6 = async () => {
4422
+ const main$8 = async () => {
4416
4423
  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)))) {
4417
4424
  return;
4418
4425
  }
@@ -4493,9 +4500,9 @@ const main$6 = async () => {
4493
4500
  console.log(JSON.stringify(candles, null, 2));
4494
4501
  process.exit(0);
4495
4502
  };
4496
- main$6();
4503
+ main$8();
4497
4504
 
4498
- const main$5 = async () => {
4505
+ const main$7 = async () => {
4499
4506
  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)))) {
4500
4507
  return;
4501
4508
  }
@@ -4609,11 +4616,326 @@ const main$5 = async () => {
4609
4616
  }
4610
4617
  process.exit(0);
4611
4618
  };
4612
- main$5();
4619
+ main$7();
4613
4620
 
4614
- const SIMULATOR_NAME = "cli_simulator";
4615
- const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
4616
- const validateIdea = (idea, line) => {
4621
+ const getBroker = functoolsKit.singleshot(() => {
4622
+ // Reaches into BrokerAdapter internals on purpose: the public commit*
4623
+ // methods skip when payload.backtest=true and require enable(), while
4624
+ // brokerdebug must fire exactly one commit at the raw adapter proxy.
4625
+ const broker = BacktestKit.Broker["getInstance"]();
4626
+ if (!broker) {
4627
+ throw new Error("Broker instance is not initialized.");
4628
+ }
4629
+ return broker;
4630
+ });
4631
+ /** Called when a new signal is opened (position entry confirmed). */
4632
+ const commitOrderOpen = functoolsKit.trycatch(async (payload) => {
4633
+ const broker = getBroker();
4634
+ return await broker.onOrderOpenCommit(payload);
4635
+ }, {
4636
+ fallback: (error) => {
4637
+ console.log(error);
4638
+ process.exit(-1);
4639
+ },
4640
+ });
4641
+ /** Called when a new signal is closed (take-profit, stop-loss, or manual close). */
4642
+ const commitOrderClose = functoolsKit.trycatch(async (payload) => {
4643
+ const broker = getBroker();
4644
+ return await broker.onOrderCloseCommit(payload);
4645
+ }, {
4646
+ fallback: (error) => {
4647
+ console.log(error);
4648
+ process.exit(-1);
4649
+ },
4650
+ });
4651
+ /** Called when a partial profit close is committed. */
4652
+ const commitPartialProfit = functoolsKit.trycatch(async (payload) => {
4653
+ const broker = getBroker();
4654
+ return await broker.onPartialProfitCommit(payload);
4655
+ }, {
4656
+ fallback: (error) => {
4657
+ console.log(error);
4658
+ process.exit(-1);
4659
+ },
4660
+ });
4661
+ /** Called when a partial loss close is committed. */
4662
+ const commitPartialLoss = functoolsKit.trycatch(async (payload) => {
4663
+ const broker = getBroker();
4664
+ return await broker.onPartialLossCommit(payload);
4665
+ }, {
4666
+ fallback: (error) => {
4667
+ console.log(error);
4668
+ process.exit(-1);
4669
+ },
4670
+ });
4671
+ /** Called when a trailing stop update is committed. */
4672
+ const commitTrailingStop = functoolsKit.trycatch(async (payload) => {
4673
+ const broker = getBroker();
4674
+ return await broker.onTrailingStopCommit(payload);
4675
+ }, {
4676
+ fallback: (error) => {
4677
+ console.log(error);
4678
+ process.exit(-1);
4679
+ },
4680
+ });
4681
+ /** Called when a trailing take-profit update is committed. */
4682
+ const commitTrailingTake = functoolsKit.trycatch(async (payload) => {
4683
+ const broker = getBroker();
4684
+ return await broker.onTrailingTakeCommit(payload);
4685
+ }, {
4686
+ fallback: (error) => {
4687
+ console.log(error);
4688
+ process.exit(-1);
4689
+ },
4690
+ });
4691
+ /** Called when a breakeven stop is committed (stop loss moved to entry price). */
4692
+ const commitBreakeven = functoolsKit.trycatch(async (payload) => {
4693
+ const broker = getBroker();
4694
+ return await broker.onBreakevenCommit(payload);
4695
+ }, {
4696
+ fallback: (error) => {
4697
+ console.log(error);
4698
+ process.exit(-1);
4699
+ },
4700
+ });
4701
+ /** Called when a DCA (average-buy) entry is committed. */
4702
+ const commitAverageBuy = functoolsKit.trycatch(async (payload) => {
4703
+ const broker = getBroker();
4704
+ return await broker.onAverageBuyCommit(payload);
4705
+ }, {
4706
+ fallback: (error) => {
4707
+ console.log(error);
4708
+ process.exit(-1);
4709
+ },
4710
+ });
4711
+ const COMMITS = [
4712
+ "signal-open",
4713
+ "signal-close",
4714
+ "partial-profit",
4715
+ "partial-loss",
4716
+ "average-buy",
4717
+ "trailing-stop",
4718
+ "trailing-take",
4719
+ "breakeven",
4720
+ ];
4721
+ const ZERO_PNL = {
4722
+ pnlPercentage: 0,
4723
+ priceOpen: 0,
4724
+ priceClose: 0,
4725
+ pnlCost: 0,
4726
+ pnlEntries: 0,
4727
+ };
4728
+ const main$6 = async () => {
4729
+ 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)))) {
4730
+ return;
4731
+ }
4732
+ const { values } = getArgs();
4733
+ if (!values.brokerdebug) {
4734
+ return;
4735
+ }
4736
+ {
4737
+ const cwd = process.cwd();
4738
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
4739
+ }
4740
+ await cli.configConnectionService.loadConfig("setup.config");
4741
+ {
4742
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
4743
+ try {
4744
+ if (typeof loader === "function") {
4745
+ await loader();
4746
+ }
4747
+ if (typeof loader?.loader === "function") {
4748
+ await loader.loader();
4749
+ }
4750
+ }
4751
+ catch (error) {
4752
+ console.error("Module loader failed", error);
4753
+ process.exit(-1);
4754
+ }
4755
+ }
4756
+ await cli.moduleConnectionService.loadModule("brokerdebug.module");
4757
+ const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
4758
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
4759
+ const symbol = values.symbol || "BTCUSDT";
4760
+ const commit = values.commit || "signal-open";
4761
+ if (!COMMITS.includes(commit)) {
4762
+ console.error(`Unknown --commit value: ${commit}`);
4763
+ console.error(`Available: ${COMMITS.join(", ")}`);
4764
+ process.exit(1);
4765
+ }
4766
+ const candles = await BacktestKit.Exchange.getCandles(symbol, "1m", 5, { exchangeName });
4767
+ const lastCandle = candles[candles.length - 1];
4768
+ const currentPrice = lastCandle.close;
4769
+ const priceTakeProfit = currentPrice * 1.02;
4770
+ const priceStopLoss = currentPrice * 0.98;
4771
+ const context = {
4772
+ strategyName: "broker-test",
4773
+ exchangeName: exchangeName,
4774
+ };
4775
+ const signalId = `broker-debug-${Date.now()}`;
4776
+ const when = BacktestKit.alignToInterval(new Date(), "1m");
4777
+ {
4778
+ console.log(`symbol=${symbol} price=${currentPrice} commit=${commit}`);
4779
+ console.time(commit);
4780
+ }
4781
+ const run = async () => {
4782
+ if (commit === "signal-open") {
4783
+ await commitOrderOpen({
4784
+ type: "active",
4785
+ symbol,
4786
+ signalId,
4787
+ cost: 100,
4788
+ position: "long",
4789
+ priceOpen: currentPrice,
4790
+ priceTakeProfit,
4791
+ priceStopLoss,
4792
+ pnl: ZERO_PNL,
4793
+ peakProfit: ZERO_PNL,
4794
+ maxDrawdown: ZERO_PNL,
4795
+ attempt: 0,
4796
+ context,
4797
+ when,
4798
+ backtest: true,
4799
+ });
4800
+ }
4801
+ if (commit === "signal-close") {
4802
+ await commitOrderClose({
4803
+ symbol,
4804
+ signalId,
4805
+ cost: 100,
4806
+ position: "long",
4807
+ currentPrice,
4808
+ priceOpen: currentPrice,
4809
+ priceTakeProfit,
4810
+ priceStopLoss,
4811
+ totalEntries: 1,
4812
+ totalPartials: 0,
4813
+ attempt: 0,
4814
+ pnl: ZERO_PNL,
4815
+ peakProfit: ZERO_PNL,
4816
+ maxDrawdown: ZERO_PNL,
4817
+ context,
4818
+ when,
4819
+ backtest: true,
4820
+ });
4821
+ }
4822
+ if (commit === "partial-profit") {
4823
+ await commitPartialProfit({
4824
+ symbol,
4825
+ signalId,
4826
+ percentToClose: 50,
4827
+ cost: 50,
4828
+ currentPrice,
4829
+ position: "long",
4830
+ priceTakeProfit,
4831
+ priceStopLoss,
4832
+ context,
4833
+ when,
4834
+ backtest: true,
4835
+ });
4836
+ }
4837
+ if (commit === "partial-loss") {
4838
+ await commitPartialLoss({
4839
+ symbol,
4840
+ signalId,
4841
+ percentToClose: 50,
4842
+ cost: 50,
4843
+ currentPrice,
4844
+ position: "long",
4845
+ priceTakeProfit,
4846
+ priceStopLoss,
4847
+ context,
4848
+ when,
4849
+ backtest: true,
4850
+ });
4851
+ }
4852
+ if (commit === "average-buy") {
4853
+ await commitAverageBuy({
4854
+ symbol,
4855
+ signalId,
4856
+ currentPrice,
4857
+ cost: 100,
4858
+ position: "long",
4859
+ priceTakeProfit,
4860
+ priceStopLoss,
4861
+ context,
4862
+ when,
4863
+ backtest: true,
4864
+ });
4865
+ }
4866
+ if (commit === "trailing-stop") {
4867
+ await commitTrailingStop({
4868
+ symbol,
4869
+ signalId,
4870
+ percentShift: -50,
4871
+ currentPrice,
4872
+ newStopLossPrice: priceStopLoss + (currentPrice - priceStopLoss) * 0.5,
4873
+ takeProfitPrice: priceTakeProfit,
4874
+ position: "long",
4875
+ context,
4876
+ when,
4877
+ backtest: true,
4878
+ });
4879
+ }
4880
+ if (commit === "trailing-take") {
4881
+ await commitTrailingTake({
4882
+ symbol,
4883
+ signalId,
4884
+ percentShift: 50,
4885
+ currentPrice,
4886
+ newTakeProfitPrice: priceTakeProfit + (priceTakeProfit - currentPrice) * 0.5,
4887
+ takeProfitPrice: priceTakeProfit,
4888
+ position: "long",
4889
+ context,
4890
+ when,
4891
+ backtest: true,
4892
+ });
4893
+ }
4894
+ if (commit === "breakeven") {
4895
+ await commitBreakeven({
4896
+ symbol,
4897
+ signalId,
4898
+ currentPrice,
4899
+ newStopLossPrice: currentPrice,
4900
+ newTakeProfitPrice: priceTakeProfit,
4901
+ position: "long",
4902
+ context,
4903
+ when,
4904
+ backtest: true,
4905
+ });
4906
+ }
4907
+ };
4908
+ await BacktestKit.runInMockContext(run, {
4909
+ when,
4910
+ strategyName: context.strategyName,
4911
+ exchangeName: context.exchangeName,
4912
+ frameName: "",
4913
+ symbol,
4914
+ backtest: true,
4915
+ });
4916
+ {
4917
+ console.log(`${commit} OK`);
4918
+ console.timeEnd(commit);
4919
+ }
4920
+ process.exit(0);
4921
+ };
4922
+ main$6();
4923
+
4924
+ const SIMULATOR_NAME$1 = "cli_simulator";
4925
+ const CONFIG_KEYS$1 = ["gridAxes", "reportOrder"];
4926
+ const validateConfig$1 = (config) => {
4927
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
4928
+ return "config must be a JSON object";
4929
+ }
4930
+ for (const key of Object.keys(config)) {
4931
+ if (!CONFIG_KEYS$1.includes(key)) {
4932
+ return `unknown key "${key}" (expected: ${CONFIG_KEYS$1.join(", ")})`;
4933
+ }
4934
+ }
4935
+ return null;
4936
+ };
4937
+ const IDEA_DIRECTIONS$1 = ["LONG", "SHORT", "NEUTRAL"];
4938
+ const validateIdea$1 = (idea, line) => {
4617
4939
  if (typeof idea !== "object" || idea === null) {
4618
4940
  return `line ${line}: not an object`;
4619
4941
  }
@@ -4626,15 +4948,18 @@ const validateIdea = (idea, line) => {
4626
4948
  if (typeof idea.symbol !== "string" || !idea.symbol) {
4627
4949
  return `line ${line}: "symbol" must be a non-empty string`;
4628
4950
  }
4629
- if (!IDEA_DIRECTIONS.includes(idea.direction)) {
4630
- return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
4951
+ if (!IDEA_DIRECTIONS$1.includes(idea.direction)) {
4952
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS$1.join("|")}, got ${JSON.stringify(idea.direction)}`;
4631
4953
  }
4632
4954
  if (typeof idea.author !== "string" || !idea.author) {
4633
4955
  return `line ${line}: "author" must be a non-empty string`;
4634
4956
  }
4635
4957
  return null;
4636
4958
  };
4637
- const toMarkdown = (result) => {
4959
+ const toMarkdown$1 = (result) => {
4960
+ const buckets = Object.entries(result.reports).filter(([, bucket]) => bucket.reports.length > 0);
4961
+ const allReports = buckets.flatMap(([, bucket]) => bucket.reports);
4962
+ const profitable = allReports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
4638
4963
  const lines = [];
4639
4964
  lines.push(`# Simulator Report — ${result.symbol}`);
4640
4965
  lines.push("");
@@ -4642,34 +4967,38 @@ const toMarkdown = (result) => {
4642
4967
  lines.push(`| --- | --- |`);
4643
4968
  lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
4644
4969
  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} |`);
4970
+ lines.push(`| Profitable corridor | ${profitable} / ${allReports.length} grid points |`);
4647
4971
  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;
4972
+ // каждая метрика — самодостаточная корзина: свои победители и
4973
+ // свои словари банов, между собой не склеиваются
4974
+ for (const [metric, bucket] of buckets) {
4975
+ lines.push("");
4976
+ lines.push(`## Metric: ${metric}`);
4977
+ lines.push("");
4978
+ lines.push(`| Criterion | Stop% | Hold | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4979
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4980
+ for (const best of bucket.best) {
4981
+ if (!best.report) {
4982
+ lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
4983
+ continue;
4984
+ }
4985
+ const { point } = best.report;
4986
+ lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.holdMinutes / 60}h | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
4987
+ `${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
4988
+ }
4989
+ const sharpeBest = bucket.best.find(({ criterion }) => criterion === "sharpe");
4990
+ lines.push("");
4991
+ lines.push(`### Allowed authors (sharpe winner rule)`);
4992
+ lines.push("");
4993
+ lines.push(`| Author | Ideas | Hits | HitRate |`);
4994
+ lines.push(`| --- | --- | --- | --- |`);
4995
+ for (const stat of (sharpeBest?.authorStats ?? []).filter(({ banned }) => !banned)) {
4996
+ lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
4657
4997
  }
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
4998
  }
4670
4999
  return lines.join("\n");
4671
5000
  };
4672
- const main$4 = async () => {
5001
+ const main$5 = async () => {
4673
5002
  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
5003
  return;
4675
5004
  }
@@ -4682,6 +5011,33 @@ const main$4 = async () => {
4682
5011
  console.error("Error: positional path to an ideas .jsonl file is required");
4683
5012
  process.exit(1);
4684
5013
  }
5014
+ // конфиг — позиционный JSON; без него подтягивается пустой объект
5015
+ // и схема живёт на дефолтах движка (оси, порядок отчёта)
5016
+ const [configPath = null] = positionals.filter((value) => value.endsWith(".json") && !value.endsWith(".jsonl"));
5017
+ let config = {};
5018
+ if (configPath) {
5019
+ let content;
5020
+ try {
5021
+ content = await fs$1.readFile(path.resolve(configPath), "utf-8");
5022
+ }
5023
+ catch (error) {
5024
+ console.error(`Error: cannot read config file: ${configPath}`);
5025
+ process.exit(1);
5026
+ }
5027
+ try {
5028
+ config = JSON.parse(content);
5029
+ }
5030
+ catch {
5031
+ console.error(`Error: invalid JSON in config file: ${configPath}`);
5032
+ process.exit(1);
5033
+ }
5034
+ const problem = validateConfig$1(config);
5035
+ if (problem) {
5036
+ console.error(`Error: simulator config does not match the structure — ${problem}`);
5037
+ console.error(`Expected shape: { "gridAxes"?: ISimulatorGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }`);
5038
+ process.exit(1);
5039
+ }
5040
+ }
4685
5041
  let ideas = [];
4686
5042
  {
4687
5043
  let content;
@@ -4706,7 +5062,7 @@ const main$4 = async () => {
4706
5062
  console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
4707
5063
  process.exit(1);
4708
5064
  }
4709
- const problem = validateIdea(idea, i + 1);
5065
+ const problem = validateIdea$1(idea, i + 1);
4710
5066
  if (problem) {
4711
5067
  console.error(`Error: ideas file does not match the idea structure — ${problem}`);
4712
5068
  console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
@@ -4742,20 +5098,24 @@ const main$4 = async () => {
4742
5098
  }
4743
5099
  const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
4744
5100
  const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
5101
+ if (values.verbose) {
5102
+ BacktestKit.overrideExchangeSchema({
5103
+ exchangeName,
5104
+ callbacks: {
5105
+ onCandleData(symbol, interval, since) {
5106
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
5107
+ },
5108
+ },
5109
+ });
5110
+ }
4745
5111
  const symbol = values.symbol || "BTCUSDT";
5112
+ // оси и порядок отчёта приходят позиционным конфигом потребителя;
5113
+ // пустой конфиг — дефолтная сетка движка из connection-сервиса
4746
5114
  BacktestKit.addSimulatorSchema({
4747
- simulatorName: SIMULATOR_NAME,
5115
+ simulatorName: SIMULATOR_NAME$1,
4748
5116
  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
- },
5117
+ ...(config.gridAxes ? { gridAxes: config.gridAxes } : {}),
5118
+ ...(config.reportOrder ? { reportOrder: config.reportOrder } : {}),
4759
5119
  callbacks: {
4760
5120
  onProgress: (symbol, stage, processed, total) => {
4761
5121
  if (values.verbose) {
@@ -4807,9 +5167,8 @@ const main$4 = async () => {
4807
5167
  if (values.verbose) {
4808
5168
  console.log("onDone", {
4809
5169
  symbol,
4810
- reports: result.reports.length,
4811
- allowedAuthors: result.allowedAuthors.length,
4812
- bannedAuthors: result.bannedAuthors.length,
5170
+ reports: Object.values(result.reports).flatMap((bucket) => bucket.reports).length,
5171
+ bans: Object.values(result.reports).flatMap((bucket) => bucket.bans).length,
4813
5172
  });
4814
5173
  }
4815
5174
  },
@@ -4817,7 +5176,7 @@ const main$4 = async () => {
4817
5176
  });
4818
5177
  const result = await BacktestKit.Simulator.run({
4819
5178
  symbol,
4820
- simulatorName: SIMULATOR_NAME,
5179
+ simulatorName: SIMULATOR_NAME$1,
4821
5180
  ideas,
4822
5181
  });
4823
5182
  const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
@@ -4832,11 +5191,308 @@ const main$4 = async () => {
4832
5191
  if (values.markdown) {
4833
5192
  const filePath = path.resolve(dumpDir, `${dumpName}.md`);
4834
5193
  await fs$1.mkdir(dumpDir, { recursive: true });
4835
- await fs$1.writeFile(filePath, toMarkdown(result), "utf-8");
5194
+ await fs$1.writeFile(filePath, toMarkdown$1(result), "utf-8");
4836
5195
  console.log(`Saved: ${filePath}`);
4837
5196
  process.exit(0);
4838
5197
  }
4839
- console.log(toMarkdown(result));
5198
+ console.log(toMarkdown$1(result));
5199
+ process.exit(0);
5200
+ };
5201
+ main$5();
5202
+
5203
+ const SIMULATOR_NAME = "cli_tune";
5204
+ const CONFIG_KEYS = ["point", "authorStats", "gridAxes", "reportOrder"];
5205
+ const POINT_NUMBER_FIELDS = [
5206
+ "hardStopPercent",
5207
+ "trailingTakePercent",
5208
+ "holdMinutes",
5209
+ "minAuthorTrack",
5210
+ "minAuthorHitRate",
5211
+ "profitLockPercent",
5212
+ ];
5213
+ const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
5214
+ const validateIdea = (idea, line) => {
5215
+ if (typeof idea !== "object" || idea === null) {
5216
+ return `line ${line}: not an object`;
5217
+ }
5218
+ if (typeof idea.id !== "number") {
5219
+ return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
5220
+ }
5221
+ if (typeof idea.ts !== "number") {
5222
+ return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
5223
+ }
5224
+ if (typeof idea.symbol !== "string" || !idea.symbol) {
5225
+ return `line ${line}: "symbol" must be a non-empty string`;
5226
+ }
5227
+ if (!IDEA_DIRECTIONS.includes(idea.direction)) {
5228
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
5229
+ }
5230
+ if (typeof idea.author !== "string" || !idea.author) {
5231
+ return `line ${line}: "author" must be a non-empty string`;
5232
+ }
5233
+ return null;
5234
+ };
5235
+ const validateConfig = (config) => {
5236
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
5237
+ return "config must be a JSON object";
5238
+ }
5239
+ for (const key of Object.keys(config)) {
5240
+ if (!CONFIG_KEYS.includes(key)) {
5241
+ return `unknown key "${key}" (expected: ${CONFIG_KEYS.join(", ")})`;
5242
+ }
5243
+ }
5244
+ if (typeof config.point !== "object" || config.point === null) {
5245
+ return `"point" is required — the frozen grid point of a training run`;
5246
+ }
5247
+ for (const field of POINT_NUMBER_FIELDS) {
5248
+ if (typeof config.point[field] !== "number") {
5249
+ return `"point.${field}" must be a number, got ${typeof config.point[field]}`;
5250
+ }
5251
+ }
5252
+ if (typeof config.point.authorMetric !== "string") {
5253
+ return `"point.authorMetric" must be a string, got ${typeof config.point.authorMetric}`;
5254
+ }
5255
+ if (!Array.isArray(config.authorStats) || !config.authorStats.length) {
5256
+ return `"authorStats" is required — the frozen raw track record (non-empty array)`;
5257
+ }
5258
+ for (let i = 0; i < config.authorStats.length; i++) {
5259
+ const stat = config.authorStats[i];
5260
+ if (typeof stat !== "object" ||
5261
+ stat === null ||
5262
+ typeof stat.author !== "string" ||
5263
+ typeof stat.ideas !== "number" ||
5264
+ typeof stat.hits !== "number") {
5265
+ return `"authorStats[${i}]" must be { author: string, ideas: number, hits: number }`;
5266
+ }
5267
+ }
5268
+ return null;
5269
+ };
5270
+ /** Одноточечные оси, зеркалящие замороженную точку — сетка инертна. */
5271
+ const axesFromPoint = (point) => ({
5272
+ hardStopPercent: [point.hardStopPercent],
5273
+ trailingTakePercent: [point.trailingTakePercent],
5274
+ holdMinutes: [point.holdMinutes],
5275
+ minAuthorTrack: [point.minAuthorTrack],
5276
+ minAuthorHitRate: [point.minAuthorHitRate],
5277
+ profitLockPercent: [point.profitLockPercent],
5278
+ authorMetric: [point.authorMetric],
5279
+ });
5280
+ const pointLabel = (point) => `H=${point.hardStopPercent} TT=${point.trailingTakePercent} hold=${point.holdMinutes / 60}h ` +
5281
+ `track=${point.minAuthorTrack} rate=${point.minAuthorHitRate} ` +
5282
+ `lock=${point.profitLockPercent} metric=${point.authorMetric}`;
5283
+ const fmtRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
5284
+ const toMarkdown = (test) => {
5285
+ const lines = [];
5286
+ lines.push(`# Tune Report — ${test.symbol}`);
5287
+ lines.push("");
5288
+ lines.push(`ONE out-of-sample shot of a FROZEN training artifact: nothing is trained here — the point and the raw author track record come from the input config verbatim, bans are re-derived from the frozen numbers under the point's rule, unseen authors are banned by default.`);
5289
+ lines.push("");
5290
+ lines.push(`Frozen point: ${pointLabel(test.point)}`);
5291
+ lines.push("");
5292
+ lines.push(`## Out-of-sample (${test.ideasDirectional} directional ideas)`);
5293
+ lines.push("");
5294
+ lines.push(`| Metric | Value |`);
5295
+ lines.push(`| --- | --- |`);
5296
+ lines.push(`| Trades (skipped busy) | ${test.report.trades} (${test.report.skippedBusy}) |`);
5297
+ lines.push(`| PNL | ${test.report.totalPnlPercent.toFixed(2)}% |`);
5298
+ lines.push(`| Win rate | ${(test.report.winRate * 100).toFixed(0)}% |`);
5299
+ lines.push(`| Profit factor | ${fmtRatio(test.report.profitFactor)} |`);
5300
+ lines.push(`| Max series drawdown | ${test.report.maxSeriesDrawdownPercent.toFixed(2)}% |`);
5301
+ lines.push(`| Sharpe / Sortino | ${fmtRatio(test.report.sharpe)} / ${fmtRatio(test.report.sortino)} |`);
5302
+ lines.push(`| Recovery factor | ${fmtRatio(test.report.recoveryFactor)} |`);
5303
+ lines.push(`| Exits | ${Object.entries(test.report.exitReasons).map(([reason, count]) => `${reason}=${count}`).join(", ")} |`);
5304
+ lines.push("");
5305
+ lines.push(`## Test trades`);
5306
+ lines.push("");
5307
+ lines.push(`| Direction | Exit | PNL% | Hold | Entry (UTC) |`);
5308
+ lines.push(`| --- | --- | --- | --- | --- |`);
5309
+ for (const trade of test.trades) {
5310
+ lines.push(`| ${trade.direction} | ${trade.exitReason} | ${trade.pnlPercent.toFixed(2)} | ${trade.holdMinutesActual}m | ${new Date(trade.entryTimestamp).toISOString()} |`);
5311
+ }
5312
+ lines.push("");
5313
+ lines.push(`## Frozen author track record (re-derived bans under the point's rule)`);
5314
+ lines.push("");
5315
+ lines.push(`| Author | Ideas | Hits | Banned |`);
5316
+ lines.push(`| --- | --- | --- | --- |`);
5317
+ for (const stat of test.authorStats) {
5318
+ lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${stat.banned ? "yes" : ""} |`);
5319
+ }
5320
+ return lines.join("\n");
5321
+ };
5322
+ const main$4 = async () => {
5323
+ 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)))) {
5324
+ return;
5325
+ }
5326
+ const { values, positionals } = getArgs();
5327
+ if (!values.tune) {
5328
+ return;
5329
+ }
5330
+ const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
5331
+ if (!ideasPath) {
5332
+ console.error("Error: positional path to an ideas .jsonl file is required");
5333
+ process.exit(1);
5334
+ }
5335
+ // конфиг — позиционный JSON; без него подтягивается пустой объект
5336
+ // (и валидация честно скажет, что замороженной точки нет)
5337
+ const [configPath = null] = positionals.filter((value) => value.endsWith(".json") && !value.endsWith(".jsonl"));
5338
+ let config = {};
5339
+ if (configPath) {
5340
+ let content;
5341
+ try {
5342
+ content = await fs$1.readFile(path.resolve(configPath), "utf-8");
5343
+ }
5344
+ catch (error) {
5345
+ console.error(`Error: cannot read config file: ${configPath}`);
5346
+ process.exit(1);
5347
+ }
5348
+ try {
5349
+ config = JSON.parse(content);
5350
+ }
5351
+ catch {
5352
+ console.error(`Error: invalid JSON in config file: ${configPath}`);
5353
+ process.exit(1);
5354
+ }
5355
+ }
5356
+ {
5357
+ const problem = validateConfig(config);
5358
+ if (problem) {
5359
+ console.error(`Error: tune config does not match the structure — ${problem}`);
5360
+ console.error(`Expected shape: { "point": ISimulatorGridPoint, "authorStats": [{ "author", "ideas", "hits" }], "gridAxes"?, "reportOrder"? }`);
5361
+ process.exit(1);
5362
+ }
5363
+ }
5364
+ let ideas = [];
5365
+ {
5366
+ let content;
5367
+ try {
5368
+ content = await fs$1.readFile(path.resolve(ideasPath), "utf-8");
5369
+ }
5370
+ catch (error) {
5371
+ console.error(`Error: cannot read ideas file: ${ideasPath}`);
5372
+ process.exit(1);
5373
+ }
5374
+ const rows = content.split("\n").filter(Boolean);
5375
+ if (!rows.length) {
5376
+ console.error(`Error: ideas file is empty: ${ideasPath}`);
5377
+ process.exit(1);
5378
+ }
5379
+ for (let i = 0; i < rows.length; i++) {
5380
+ let idea;
5381
+ try {
5382
+ idea = JSON.parse(rows[i]);
5383
+ }
5384
+ catch {
5385
+ console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
5386
+ process.exit(1);
5387
+ }
5388
+ const problem = validateIdea(idea, i + 1);
5389
+ if (problem) {
5390
+ console.error(`Error: ideas file does not match the idea structure — ${problem}`);
5391
+ console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
5392
+ process.exit(1);
5393
+ }
5394
+ ideas.push(idea);
5395
+ }
5396
+ }
5397
+ {
5398
+ const cwd = process.cwd();
5399
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
5400
+ }
5401
+ await cli.configConnectionService.loadConfig("setup.config");
5402
+ {
5403
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
5404
+ try {
5405
+ if (typeof loader === "function") {
5406
+ await loader();
5407
+ }
5408
+ if (typeof loader?.loader === "function") {
5409
+ await loader.loader();
5410
+ }
5411
+ }
5412
+ catch (error) {
5413
+ console.error("Module loader failed", error);
5414
+ process.exit(-1);
5415
+ }
5416
+ }
5417
+ await cli.moduleConnectionService.loadModule("tune.module");
5418
+ {
5419
+ await cli.exchangeSchemaService.addSchema();
5420
+ await cli.symbolSchemaService.addSchema();
5421
+ }
5422
+ const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
5423
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
5424
+ if (values.verbose) {
5425
+ BacktestKit.overrideExchangeSchema({
5426
+ exchangeName,
5427
+ callbacks: {
5428
+ onCandleData(symbol, interval, since) {
5429
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
5430
+ },
5431
+ },
5432
+ });
5433
+ }
5434
+ const symbol = values.symbol || "BTCUSDT";
5435
+ // сетка для test() инертна (оценивается ровно замороженная точка):
5436
+ // без явных осей в конфиге они одноточечно зеркалят точку
5437
+ BacktestKit.addSimulatorSchema({
5438
+ simulatorName: SIMULATOR_NAME,
5439
+ exchangeName,
5440
+ gridAxes: config.gridAxes ?? axesFromPoint(config.point),
5441
+ ...(config.reportOrder ? { reportOrder: config.reportOrder } : {}),
5442
+ callbacks: {
5443
+ onProgress: (symbol, stage, processed, total) => {
5444
+ if (values.verbose) {
5445
+ console.log("onProgress", { symbol, stage, processed, total });
5446
+ }
5447
+ },
5448
+ onIdeas: (symbol, ideasTotal, ideasDirectional) => {
5449
+ if (values.verbose) {
5450
+ console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
5451
+ }
5452
+ },
5453
+ onProfiles: (symbol, profiles, truncatedCount) => {
5454
+ if (values.verbose) {
5455
+ console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
5456
+ }
5457
+ },
5458
+ onTestDone: (symbol, result) => {
5459
+ if (values.verbose) {
5460
+ console.log("onTestDone", {
5461
+ symbol,
5462
+ trades: result.report.trades,
5463
+ pnl: +result.report.totalPnlPercent.toFixed(2),
5464
+ });
5465
+ }
5466
+ },
5467
+ },
5468
+ });
5469
+ // только out-of-sample: обучения нет, точка и трек-рекорд приходят
5470
+ // из конфига verbatim — test() перевыводит баны под правило точки,
5471
+ // невиданные авторы забанены по умолчанию
5472
+ const test = await BacktestKit.Simulator.test({
5473
+ symbol,
5474
+ simulatorName: SIMULATOR_NAME,
5475
+ ideas,
5476
+ point: config.point,
5477
+ authorStats: config.authorStats,
5478
+ });
5479
+ const dumpName = values.output || `tune_${symbol}_${Date.now()}`;
5480
+ const dumpDir = path.join(process.cwd(), "dump");
5481
+ if (values.json) {
5482
+ const filePath = path.resolve(dumpDir, `${dumpName}.json`);
5483
+ await fs$1.mkdir(dumpDir, { recursive: true });
5484
+ await fs$1.writeFile(filePath, JSON.stringify(test, null, 2), "utf-8");
5485
+ console.log(`Saved: ${filePath}`);
5486
+ process.exit(0);
5487
+ }
5488
+ if (values.markdown) {
5489
+ const filePath = path.resolve(dumpDir, `${dumpName}.md`);
5490
+ await fs$1.mkdir(dumpDir, { recursive: true });
5491
+ await fs$1.writeFile(filePath, toMarkdown(test), "utf-8");
5492
+ console.log(`Saved: ${filePath}`);
5493
+ process.exit(0);
5494
+ }
5495
+ console.log(toMarkdown(test));
4840
5496
  process.exit(0);
4841
5497
  };
4842
5498
  main$4();
@@ -5037,7 +5693,8 @@ Modes:
5037
5693
  --dump Fetch and save raw OHLCV candles
5038
5694
  --pnldebug Simulate PnL per minute for a given entry price and direction
5039
5695
  --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
5696
+ --simulator <ideas.jsonl> [config.json] Feasibility probe over crowd ideas: is there a profitable corridor at all
5697
+ --tune <ideas.jsonl> <config.json> ONE out-of-sample shot of a frozen training artifact (point + author track)
5041
5698
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
5042
5699
  --init Scaffold a new project in the current directory
5043
5700
  --docker Scaffold a Docker workspace for running strategies in a container
@@ -5167,16 +5824,22 @@ Simulator flags (--simulator):
5167
5824
  --markdown Save summary report to ./dump/<output>.md
5168
5825
  --verbose Log every simulator lifecycle callback to the console
5169
5826
 
5170
- Positional: path to an ideas .jsonl file — one idea per line with the exact shape
5827
+ Positionals: path to an ideas .jsonl file — one idea per line with the exact shape
5171
5828
  { "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.
5829
+ "author": string } — and an OPTIONAL config .json with the shape
5830
+ { "gridAxes"?: ISimulatorGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }.
5831
+ Both files are validated BEFORE any work; a mismatch aborts the run with an error.
5832
+ No config -> an empty object is used and the engine defaults apply (the full
5833
+ default grid axes and reportOrder "sharpe" of the connection service).
5174
5834
 
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.
5835
+ A FEASIBILITY PROBE, not an out-of-sample shot (that is --tune): the grid from
5836
+ the config (or the engine default) answers one question — does the feed contain
5837
+ a profitable corridor at all. One candle pass per idea to the grid's longest
5838
+ hold, flood dedupe (one idea per author per direction per 8h), default-ban
5839
+ author filter graded inside each point's own hold window, time-based
5840
+ Sharpe/Sortino, per-metric buckets with their own ranking winners and ban
5841
+ dictionaries. Ideas of other symbols are filtered out — one shared feed serves
5842
+ any --symbol.
5180
5843
 
5181
5844
  No output flag → print the Markdown summary to stdout. With --verbose every
5182
5845
  simulator lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
@@ -5185,6 +5848,33 @@ Simulator flags (--simulator):
5185
5848
  Module file ./modules/simulator.module is loaded automatically if it exists
5186
5849
  (register your exchange there); without it CCXT Binance is used by default.
5187
5850
 
5851
+ Tune flags (--tune):
5852
+
5853
+ --symbol <string> Trading pair to test (default: BTCUSDT)
5854
+ --exchange <string> Exchange name (default: first registered)
5855
+ --output <string> Output file base name (default: tune_{SYMBOL}_{TIMESTAMP})
5856
+ --json Save the full ISimulatorTestResult to ./dump/<output>.json
5857
+ --markdown Save the out-of-sample report to ./dump/<output>.md
5858
+ --verbose Log simulator lifecycle callbacks to the console
5859
+
5860
+ Positionals: path to an ideas .jsonl file (same shape and validation as
5861
+ --simulator) and a config .json carrying the FROZEN training artifact:
5862
+ { "point": ISimulatorGridPoint, "authorStats": [{ "author", "ideas", "hits" }],
5863
+ "gridAxes"?, "reportOrder"? }. The point and authorStats are REQUIRED — without
5864
+ them there is nothing to test and the run aborts with an error. gridAxes are
5865
+ optional: by default they mirror the frozen point one value per axis (the grid
5866
+ is inert for a test).
5867
+
5868
+ ONE OUT-OF-SAMPLE SHOT, no training: the CLI never runs the sweep — pick your
5869
+ candidate elsewhere (a Simulator.run of your own), freeze its point and raw
5870
+ author track record into the config, and fire it once here via Simulator.test.
5871
+ Bans are re-derived from the frozen numbers under the point's rule; authors
5872
+ unseen in the config are banned by default. The report carries the result with
5873
+ the trade list and the frozen track record with re-derived ban flags.
5874
+
5875
+ Module file ./modules/tune.module is loaded automatically if it exists
5876
+ (register your exchange there); without it CCXT Binance is used by default.
5877
+
5188
5878
  Flush flags (--flush):
5189
5879
 
5190
5880
  One or more positional entry points. For each entry point the following
@@ -5218,7 +5908,8 @@ Module hooks (loaded automatically by each mode):
5218
5908
  modules/dump.module --dump Exchange schema for candle dumps
5219
5909
  modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
5220
5910
  modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
5221
- modules/simulator.module --simulator Exchange schema for crowd-ideas simulation
5911
+ modules/simulator.module --simulator Exchange schema for the crowd-ideas feasibility probe
5912
+ modules/tune.module --tune Exchange schema for the walk-forward parameter search
5222
5913
 
5223
5914
  --flush has no associated module. It only removes dump subdirectories.
5224
5915
 
@@ -5248,7 +5939,9 @@ Examples:
5248
5939
  node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
5249
5940
  node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
5250
5941
  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
5942
+ node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_probe ./assets/tv-ideas.normalized.jsonl ./assets/probe.config.json
5943
+ node ${ENTRY_PATH} --tune --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5944
+ node ${ENTRY_PATH} --tune --symbol BTCUSDT --markdown --output jun_2026_tune ./assets/tv-ideas.normalized.jsonl ./assets/tune.config.json
5252
5945
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
5253
5946
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
5254
5947
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -5262,7 +5955,7 @@ const main$1 = async () => {
5262
5955
  if (!values.help) {
5263
5956
  return;
5264
5957
  }
5265
- process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n\n`);
5958
+ process.stdout.write(`@backtest-kit/cli ${"17.0.0"}\n\n`);
5266
5959
  process.stdout.write(HELP_TEXT);
5267
5960
  process.exit(0);
5268
5961
  };
@@ -5276,7 +5969,7 @@ const main = async () => {
5276
5969
  if (!values.version) {
5277
5970
  return;
5278
5971
  }
5279
- process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n`);
5972
+ process.stdout.write(`@backtest-kit/cli ${"17.0.0"}\n`);
5280
5973
  process.exit(0);
5281
5974
  };
5282
5975
  main();