@backtest-kit/cli 16.5.0 → 16.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/index.cjs CHANGED
@@ -233,14 +233,14 @@ const getReadySubject = functoolsKit.singleshot(() => {
233
233
  return new functoolsKit.Subject();
234
234
  });
235
235
 
236
- const main$j = () => {
236
+ const main$l = () => {
237
237
  const entrySubject = getEntrySubject();
238
238
  entrySubject.subscribe(async (path) => {
239
239
  console.log("Running", path);
240
240
  await getReadySubject().next();
241
241
  });
242
242
  };
243
- main$j();
243
+ main$l();
244
244
 
245
245
  const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
246
246
  function dumpStackTrace(error) {
@@ -895,6 +895,14 @@ const getArgs = functoolsKit.singleshot(() => {
895
895
  type: "boolean",
896
896
  default: false,
897
897
  },
898
+ tune: {
899
+ type: "boolean",
900
+ default: false,
901
+ },
902
+ split: {
903
+ type: "string",
904
+ default: "",
905
+ },
898
906
  commit: {
899
907
  type: "string",
900
908
  default: "",
@@ -2727,8 +2735,11 @@ class TelegramLogicService {
2727
2735
  const unSignalNotify = BacktestKit.listenSignalNotify(async (event) => {
2728
2736
  await this.notifySignalInfo(event);
2729
2737
  });
2738
+ const unGc = functoolsKit.Source.fromInterval(90000).connect(() => {
2739
+ this.notifyOrderContinue.gc();
2740
+ });
2730
2741
  const unConnect = () => this.connect.clear();
2731
- const unListen = functoolsKit.compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unSignalNotify(), () => unConnect());
2742
+ const unListen = functoolsKit.compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unGc(), () => unSignalNotify(), () => unConnect());
2732
2743
  return () => {
2733
2744
  STOP_BOT_FN();
2734
2745
  unListen();
@@ -3676,14 +3687,14 @@ const cli = {
3676
3687
  };
3677
3688
  init();
3678
3689
 
3679
- const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "flush", "init", "docker", "help", "version"];
3690
+ const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "tune", "flush", "init", "docker", "help", "version"];
3680
3691
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
3681
3692
  const HELP_TEXT$1 = `
3682
3693
  Example:
3683
3694
 
3684
3695
  node ${ENTRY_PATH$1} --help
3685
3696
  `.trimStart();
3686
- const main$i = async () => {
3697
+ const main$k = async () => {
3687
3698
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
3688
3699
  return;
3689
3700
  }
@@ -3691,14 +3702,14 @@ const main$i = async () => {
3691
3702
  if (MODES.some((mode) => values[mode])) {
3692
3703
  return;
3693
3704
  }
3694
- process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n`);
3705
+ process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
3695
3706
  process.stdout.write("\n");
3696
3707
  process.stdout.write(`Run with --help to see available commands.\n`);
3697
3708
  process.stdout.write("\n");
3698
3709
  process.stdout.write(HELP_TEXT$1);
3699
3710
  process.exit(0);
3700
3711
  };
3701
- main$i();
3712
+ main$k();
3702
3713
 
3703
3714
  const notifyShutdown = functoolsKit.singleshot(async () => {
3704
3715
  console.log("Graceful shutdown initiated.");
@@ -3714,7 +3725,7 @@ const flush = async (entryPoint) => {
3714
3725
  console.log(`Removed: ${target}`);
3715
3726
  }
3716
3727
  };
3717
- const main$h = async () => {
3728
+ const main$j = async () => {
3718
3729
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
3719
3730
  return;
3720
3731
  }
@@ -3731,7 +3742,7 @@ const main$h = async () => {
3731
3742
  }
3732
3743
  process.exit(0);
3733
3744
  };
3734
- main$h();
3745
+ main$j();
3735
3746
 
3736
3747
  const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
3737
3748
  process.off("SIGINT", BEFORE_EXIT_FN$6);
@@ -3754,7 +3765,7 @@ const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
3754
3765
  const listenGracefulShutdown$6 = functoolsKit.singleshot(() => {
3755
3766
  process.on("SIGINT", BEFORE_EXIT_FN$6);
3756
3767
  });
3757
- const main$g = async () => {
3768
+ const main$i = async () => {
3758
3769
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
3759
3770
  return;
3760
3771
  }
@@ -3772,7 +3783,7 @@ const main$g = async () => {
3772
3783
  await cli.backtestMainService.connect();
3773
3784
  listenGracefulShutdown$6();
3774
3785
  };
3775
- main$g();
3786
+ main$i();
3776
3787
 
3777
3788
  const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3778
3789
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3791,7 +3802,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3791
3802
  const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
3792
3803
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3793
3804
  });
3794
- const main$f = async () => {
3805
+ const main$h = async () => {
3795
3806
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
3796
3807
  return;
3797
3808
  }
@@ -3810,7 +3821,7 @@ const main$f = async () => {
3810
3821
  listenGracefulShutdown$5();
3811
3822
  await cli.walkerMainService.connect();
3812
3823
  };
3813
- main$f();
3824
+ main$h();
3814
3825
 
3815
3826
  const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3816
3827
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3832,7 +3843,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3832
3843
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
3833
3844
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3834
3845
  });
3835
- const main$e = async () => {
3846
+ const main$g = async () => {
3836
3847
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
3837
3848
  return;
3838
3849
  }
@@ -3846,7 +3857,7 @@ const main$e = async () => {
3846
3857
  await cli.paperMainService.connect();
3847
3858
  listenGracefulShutdown$4();
3848
3859
  };
3849
- main$e();
3860
+ main$g();
3850
3861
 
3851
3862
  const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3852
3863
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3868,7 +3879,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3868
3879
  const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
3869
3880
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3870
3881
  });
3871
- const main$d = async () => {
3882
+ const main$f = async () => {
3872
3883
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
3873
3884
  return;
3874
3885
  }
@@ -3882,7 +3893,7 @@ const main$d = async () => {
3882
3893
  await cli.liveMainService.connect();
3883
3894
  listenGracefulShutdown$3();
3884
3895
  };
3885
- main$d();
3896
+ main$f();
3886
3897
 
3887
3898
  const stopBacktestList$1 = async () => {
3888
3899
  for (const item of await BacktestKit.Backtest.list()) {
@@ -3946,7 +3957,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
3946
3957
  const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
3947
3958
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3948
3959
  });
3949
- const main$c = async () => {
3960
+ const main$e = async () => {
3950
3961
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
3951
3962
  return;
3952
3963
  }
@@ -4002,7 +4013,7 @@ const main$c = async () => {
4002
4013
  listenGracefulShutdown$2();
4003
4014
  await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
4004
4015
  };
4005
- main$c();
4016
+ main$e();
4006
4017
 
4007
4018
  const MODE_MODULE = {
4008
4019
  backtest: "backtest.module",
@@ -4105,7 +4116,7 @@ const createGracefulShutdown = (mode) => {
4105
4116
  process.on("SIGINT", handler);
4106
4117
  });
4107
4118
  };
4108
- const main$b = async () => {
4119
+ const main$d = async () => {
4109
4120
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
4110
4121
  return;
4111
4122
  }
@@ -4181,7 +4192,7 @@ const main$b = async () => {
4181
4192
  }
4182
4193
  await getEntrySubject().next(absolutePath);
4183
4194
  };
4184
- main$b();
4195
+ main$d();
4185
4196
 
4186
4197
  const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
4187
4198
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -4192,7 +4203,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
4192
4203
  const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
4193
4204
  process.on("SIGINT", BEFORE_EXIT_FN$1);
4194
4205
  });
4195
- const main$a = async () => {
4206
+ const main$c = async () => {
4196
4207
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
4197
4208
  return;
4198
4209
  }
@@ -4202,7 +4213,7 @@ const main$a = async () => {
4202
4213
  }
4203
4214
  listenGracefulShutdown$1();
4204
4215
  };
4205
- main$a();
4216
+ main$c();
4206
4217
 
4207
4218
  const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
4208
4219
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -4213,7 +4224,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
4213
4224
  const listenGracefulShutdown = functoolsKit.singleshot(() => {
4214
4225
  process.on("SIGINT", BEFORE_EXIT_FN);
4215
4226
  });
4216
- const main$9 = async () => {
4227
+ const main$b = async () => {
4217
4228
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
4218
4229
  return;
4219
4230
  }
@@ -4223,7 +4234,7 @@ const main$9 = async () => {
4223
4234
  }
4224
4235
  listenGracefulShutdown();
4225
4236
  };
4226
- main$9();
4237
+ main$b();
4227
4238
 
4228
4239
  const EXTRACT_ROWS_FN = (plots, schema) => {
4229
4240
  const keys = Object.keys(schema);
@@ -4245,7 +4256,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
4245
4256
  }
4246
4257
  return rows;
4247
4258
  };
4248
- const main$8 = async () => {
4259
+ const main$a = async () => {
4249
4260
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
4250
4261
  return;
4251
4262
  }
@@ -4345,9 +4356,9 @@ const main$8 = async () => {
4345
4356
  console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
4346
4357
  process.exit(0);
4347
4358
  };
4348
- main$8();
4359
+ main$a();
4349
4360
 
4350
- const main$7 = async () => {
4361
+ const main$9 = async () => {
4351
4362
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
4352
4363
  return;
4353
4364
  }
@@ -4410,9 +4421,9 @@ const main$7 = async () => {
4410
4421
  };
4411
4422
  process.on("SIGINT", beforeExit);
4412
4423
  };
4413
- main$7();
4424
+ main$9();
4414
4425
 
4415
- const main$6 = async () => {
4426
+ const main$8 = async () => {
4416
4427
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
4417
4428
  return;
4418
4429
  }
@@ -4493,9 +4504,9 @@ const main$6 = async () => {
4493
4504
  console.log(JSON.stringify(candles, null, 2));
4494
4505
  process.exit(0);
4495
4506
  };
4496
- main$6();
4507
+ main$8();
4497
4508
 
4498
- const main$5 = async () => {
4509
+ const main$7 = async () => {
4499
4510
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
4500
4511
  return;
4501
4512
  }
@@ -4609,11 +4620,314 @@ const main$5 = async () => {
4609
4620
  }
4610
4621
  process.exit(0);
4611
4622
  };
4612
- main$5();
4623
+ main$7();
4613
4624
 
4614
- const SIMULATOR_NAME = "cli_simulator";
4615
- const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
4616
- const validateIdea = (idea, line) => {
4625
+ const getBroker = functoolsKit.singleshot(() => {
4626
+ // Reaches into BrokerAdapter internals on purpose: the public commit*
4627
+ // methods skip when payload.backtest=true and require enable(), while
4628
+ // brokerdebug must fire exactly one commit at the raw adapter proxy.
4629
+ const broker = BacktestKit.Broker["getInstance"]();
4630
+ if (!broker) {
4631
+ throw new Error("Broker instance is not initialized.");
4632
+ }
4633
+ return broker;
4634
+ });
4635
+ /** Called when a new signal is opened (position entry confirmed). */
4636
+ const commitOrderOpen = functoolsKit.trycatch(async (payload) => {
4637
+ const broker = getBroker();
4638
+ return await broker.onOrderOpenCommit(payload);
4639
+ }, {
4640
+ fallback: (error) => {
4641
+ console.log(error);
4642
+ process.exit(-1);
4643
+ },
4644
+ });
4645
+ /** Called when a new signal is closed (take-profit, stop-loss, or manual close). */
4646
+ const commitOrderClose = functoolsKit.trycatch(async (payload) => {
4647
+ const broker = getBroker();
4648
+ return await broker.onOrderCloseCommit(payload);
4649
+ }, {
4650
+ fallback: (error) => {
4651
+ console.log(error);
4652
+ process.exit(-1);
4653
+ },
4654
+ });
4655
+ /** Called when a partial profit close is committed. */
4656
+ const commitPartialProfit = functoolsKit.trycatch(async (payload) => {
4657
+ const broker = getBroker();
4658
+ return await broker.onPartialProfitCommit(payload);
4659
+ }, {
4660
+ fallback: (error) => {
4661
+ console.log(error);
4662
+ process.exit(-1);
4663
+ },
4664
+ });
4665
+ /** Called when a partial loss close is committed. */
4666
+ const commitPartialLoss = functoolsKit.trycatch(async (payload) => {
4667
+ const broker = getBroker();
4668
+ return await broker.onPartialLossCommit(payload);
4669
+ }, {
4670
+ fallback: (error) => {
4671
+ console.log(error);
4672
+ process.exit(-1);
4673
+ },
4674
+ });
4675
+ /** Called when a trailing stop update is committed. */
4676
+ const commitTrailingStop = functoolsKit.trycatch(async (payload) => {
4677
+ const broker = getBroker();
4678
+ return await broker.onTrailingStopCommit(payload);
4679
+ }, {
4680
+ fallback: (error) => {
4681
+ console.log(error);
4682
+ process.exit(-1);
4683
+ },
4684
+ });
4685
+ /** Called when a trailing take-profit update is committed. */
4686
+ const commitTrailingTake = functoolsKit.trycatch(async (payload) => {
4687
+ const broker = getBroker();
4688
+ return await broker.onTrailingTakeCommit(payload);
4689
+ }, {
4690
+ fallback: (error) => {
4691
+ console.log(error);
4692
+ process.exit(-1);
4693
+ },
4694
+ });
4695
+ /** Called when a breakeven stop is committed (stop loss moved to entry price). */
4696
+ const commitBreakeven = functoolsKit.trycatch(async (payload) => {
4697
+ const broker = getBroker();
4698
+ return await broker.onBreakevenCommit(payload);
4699
+ }, {
4700
+ fallback: (error) => {
4701
+ console.log(error);
4702
+ process.exit(-1);
4703
+ },
4704
+ });
4705
+ /** Called when a DCA (average-buy) entry is committed. */
4706
+ const commitAverageBuy = functoolsKit.trycatch(async (payload) => {
4707
+ const broker = getBroker();
4708
+ return await broker.onAverageBuyCommit(payload);
4709
+ }, {
4710
+ fallback: (error) => {
4711
+ console.log(error);
4712
+ process.exit(-1);
4713
+ },
4714
+ });
4715
+ const COMMITS = [
4716
+ "signal-open",
4717
+ "signal-close",
4718
+ "partial-profit",
4719
+ "partial-loss",
4720
+ "average-buy",
4721
+ "trailing-stop",
4722
+ "trailing-take",
4723
+ "breakeven",
4724
+ ];
4725
+ const ZERO_PNL = {
4726
+ pnlPercentage: 0,
4727
+ priceOpen: 0,
4728
+ priceClose: 0,
4729
+ pnlCost: 0,
4730
+ pnlEntries: 0,
4731
+ };
4732
+ const main$6 = async () => {
4733
+ if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
4734
+ return;
4735
+ }
4736
+ const { values } = getArgs();
4737
+ if (!values.brokerdebug) {
4738
+ return;
4739
+ }
4740
+ {
4741
+ const cwd = process.cwd();
4742
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
4743
+ }
4744
+ await cli.configConnectionService.loadConfig("setup.config");
4745
+ {
4746
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
4747
+ try {
4748
+ if (typeof loader === "function") {
4749
+ await loader();
4750
+ }
4751
+ if (typeof loader?.loader === "function") {
4752
+ await loader.loader();
4753
+ }
4754
+ }
4755
+ catch (error) {
4756
+ console.error("Module loader failed", error);
4757
+ process.exit(-1);
4758
+ }
4759
+ }
4760
+ await cli.moduleConnectionService.loadModule("brokerdebug.module");
4761
+ const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
4762
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
4763
+ const symbol = values.symbol || "BTCUSDT";
4764
+ const commit = values.commit || "signal-open";
4765
+ if (!COMMITS.includes(commit)) {
4766
+ console.error(`Unknown --commit value: ${commit}`);
4767
+ console.error(`Available: ${COMMITS.join(", ")}`);
4768
+ process.exit(1);
4769
+ }
4770
+ const candles = await BacktestKit.Exchange.getCandles(symbol, "1m", 5, { exchangeName });
4771
+ const lastCandle = candles[candles.length - 1];
4772
+ const currentPrice = lastCandle.close;
4773
+ const priceTakeProfit = currentPrice * 1.02;
4774
+ const priceStopLoss = currentPrice * 0.98;
4775
+ const context = {
4776
+ strategyName: "broker-test",
4777
+ exchangeName: exchangeName,
4778
+ };
4779
+ const signalId = `broker-debug-${Date.now()}`;
4780
+ const when = BacktestKit.alignToInterval(new Date(), "1m");
4781
+ {
4782
+ console.log(`symbol=${symbol} price=${currentPrice} commit=${commit}`);
4783
+ console.time(commit);
4784
+ }
4785
+ const run = async () => {
4786
+ if (commit === "signal-open") {
4787
+ await commitOrderOpen({
4788
+ type: "active",
4789
+ symbol,
4790
+ signalId,
4791
+ cost: 100,
4792
+ position: "long",
4793
+ priceOpen: currentPrice,
4794
+ priceTakeProfit,
4795
+ priceStopLoss,
4796
+ pnl: ZERO_PNL,
4797
+ peakProfit: ZERO_PNL,
4798
+ maxDrawdown: ZERO_PNL,
4799
+ attempt: 0,
4800
+ context,
4801
+ when,
4802
+ backtest: true,
4803
+ });
4804
+ }
4805
+ if (commit === "signal-close") {
4806
+ await commitOrderClose({
4807
+ symbol,
4808
+ signalId,
4809
+ cost: 100,
4810
+ position: "long",
4811
+ currentPrice,
4812
+ priceOpen: currentPrice,
4813
+ priceTakeProfit,
4814
+ priceStopLoss,
4815
+ totalEntries: 1,
4816
+ totalPartials: 0,
4817
+ attempt: 0,
4818
+ pnl: ZERO_PNL,
4819
+ peakProfit: ZERO_PNL,
4820
+ maxDrawdown: ZERO_PNL,
4821
+ context,
4822
+ when,
4823
+ backtest: true,
4824
+ });
4825
+ }
4826
+ if (commit === "partial-profit") {
4827
+ await commitPartialProfit({
4828
+ symbol,
4829
+ signalId,
4830
+ percentToClose: 50,
4831
+ cost: 50,
4832
+ currentPrice,
4833
+ position: "long",
4834
+ priceTakeProfit,
4835
+ priceStopLoss,
4836
+ context,
4837
+ when,
4838
+ backtest: true,
4839
+ });
4840
+ }
4841
+ if (commit === "partial-loss") {
4842
+ await commitPartialLoss({
4843
+ symbol,
4844
+ signalId,
4845
+ percentToClose: 50,
4846
+ cost: 50,
4847
+ currentPrice,
4848
+ position: "long",
4849
+ priceTakeProfit,
4850
+ priceStopLoss,
4851
+ context,
4852
+ when,
4853
+ backtest: true,
4854
+ });
4855
+ }
4856
+ if (commit === "average-buy") {
4857
+ await commitAverageBuy({
4858
+ symbol,
4859
+ signalId,
4860
+ currentPrice,
4861
+ cost: 100,
4862
+ position: "long",
4863
+ priceTakeProfit,
4864
+ priceStopLoss,
4865
+ context,
4866
+ when,
4867
+ backtest: true,
4868
+ });
4869
+ }
4870
+ if (commit === "trailing-stop") {
4871
+ await commitTrailingStop({
4872
+ symbol,
4873
+ signalId,
4874
+ percentShift: -50,
4875
+ currentPrice,
4876
+ newStopLossPrice: priceStopLoss + (currentPrice - priceStopLoss) * 0.5,
4877
+ takeProfitPrice: priceTakeProfit,
4878
+ position: "long",
4879
+ context,
4880
+ when,
4881
+ backtest: true,
4882
+ });
4883
+ }
4884
+ if (commit === "trailing-take") {
4885
+ await commitTrailingTake({
4886
+ symbol,
4887
+ signalId,
4888
+ percentShift: 50,
4889
+ currentPrice,
4890
+ newTakeProfitPrice: priceTakeProfit + (priceTakeProfit - currentPrice) * 0.5,
4891
+ takeProfitPrice: priceTakeProfit,
4892
+ position: "long",
4893
+ context,
4894
+ when,
4895
+ backtest: true,
4896
+ });
4897
+ }
4898
+ if (commit === "breakeven") {
4899
+ await commitBreakeven({
4900
+ symbol,
4901
+ signalId,
4902
+ currentPrice,
4903
+ newStopLossPrice: currentPrice,
4904
+ newTakeProfitPrice: priceTakeProfit,
4905
+ position: "long",
4906
+ context,
4907
+ when,
4908
+ backtest: true,
4909
+ });
4910
+ }
4911
+ };
4912
+ await BacktestKit.runInMockContext(run, {
4913
+ when,
4914
+ strategyName: context.strategyName,
4915
+ exchangeName: context.exchangeName,
4916
+ frameName: "",
4917
+ symbol,
4918
+ backtest: true,
4919
+ });
4920
+ {
4921
+ console.log(`${commit} OK`);
4922
+ console.timeEnd(commit);
4923
+ }
4924
+ process.exit(0);
4925
+ };
4926
+ main$6();
4927
+
4928
+ const SIMULATOR_NAME$1 = "cli_simulator";
4929
+ const IDEA_DIRECTIONS$1 = ["LONG", "SHORT", "NEUTRAL"];
4930
+ const validateIdea$1 = (idea, line) => {
4617
4931
  if (typeof idea !== "object" || idea === null) {
4618
4932
  return `line ${line}: not an object`;
4619
4933
  }
@@ -4626,15 +4940,16 @@ const validateIdea = (idea, line) => {
4626
4940
  if (typeof idea.symbol !== "string" || !idea.symbol) {
4627
4941
  return `line ${line}: "symbol" must be a non-empty string`;
4628
4942
  }
4629
- if (!IDEA_DIRECTIONS.includes(idea.direction)) {
4630
- return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
4943
+ if (!IDEA_DIRECTIONS$1.includes(idea.direction)) {
4944
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS$1.join("|")}, got ${JSON.stringify(idea.direction)}`;
4631
4945
  }
4632
4946
  if (typeof idea.author !== "string" || !idea.author) {
4633
4947
  return `line ${line}: "author" must be a non-empty string`;
4634
4948
  }
4635
4949
  return null;
4636
4950
  };
4637
- const toMarkdown = (result) => {
4951
+ const toMarkdown$1 = (result) => {
4952
+ const profitable = result.reports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
4638
4953
  const lines = [];
4639
4954
  lines.push(`# Simulator Report — ${result.symbol}`);
4640
4955
  lines.push("");
@@ -4643,33 +4958,36 @@ const toMarkdown = (result) => {
4643
4958
  lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
4644
4959
  lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
4645
4960
  lines.push(`| Authors allowed / banned | ${result.allowedAuthors.length} / ${result.bannedAuthors.length} |`);
4646
- lines.push(`| Grid points | ${result.reports.length} |`);
4961
+ lines.push(`| Profitable corridor | ${profitable} / ${result.reports.length} grid points |`);
4647
4962
  lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
4648
4963
  lines.push("");
4649
4964
  lines.push(`## Ranking winners`);
4650
4965
  lines.push("");
4651
- lines.push(`| Criterion | Stop% | Trailing% | Hold | Aligned | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4652
- lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4966
+ lines.push(`| Criterion | Stop% | Hold | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4967
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4653
4968
  for (const best of result.best) {
4654
4969
  if (!best.report) {
4655
- lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — | — | — |`);
4970
+ lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
4656
4971
  continue;
4657
4972
  }
4658
4973
  const { point } = best.report;
4659
- lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.trailingTakePercent} | ${point.holdMinutes / 60}h | ${point.minIdeasAligned} | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
4974
+ lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.holdMinutes / 60}h | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
4660
4975
  `${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
4661
4976
  }
4662
4977
  lines.push("");
4663
- lines.push(`## Allowed authors (production whitelist)`);
4978
+ lines.push(`## Allowed authors (whitelist of the sharpe winner rule)`);
4664
4979
  lines.push("");
4665
4980
  lines.push(`| Author | Ideas | Hits | HitRate |`);
4666
4981
  lines.push(`| --- | --- | --- | --- |`);
4667
- for (const stat of result.authorStats.filter(({ banned }) => !banned)) {
4982
+ // артефакт авторов живёт по-победительно в best[] — ран-левел
4983
+ // authorStats в ISimulatorResult больше нет
4984
+ const sharpeBest = result.best.find(({ criterion }) => criterion === "sharpe");
4985
+ for (const stat of (sharpeBest?.authorStats ?? []).filter(({ banned }) => !banned)) {
4668
4986
  lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
4669
4987
  }
4670
4988
  return lines.join("\n");
4671
4989
  };
4672
- const main$4 = async () => {
4990
+ const main$5 = async () => {
4673
4991
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
4674
4992
  return;
4675
4993
  }
@@ -4706,7 +5024,7 @@ const main$4 = async () => {
4706
5024
  console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
4707
5025
  process.exit(1);
4708
5026
  }
4709
- const problem = validateIdea(idea, i + 1);
5027
+ const problem = validateIdea$1(idea, i + 1);
4710
5028
  if (problem) {
4711
5029
  console.error(`Error: ideas file does not match the idea structure — ${problem}`);
4712
5030
  console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
@@ -4742,20 +5060,42 @@ const main$4 = async () => {
4742
5060
  }
4743
5061
  const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
4744
5062
  const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
5063
+ if (values.verbose) {
5064
+ BacktestKit.overrideExchangeSchema({
5065
+ exchangeName,
5066
+ callbacks: {
5067
+ onCandleData(symbol, interval, since) {
5068
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
5069
+ },
5070
+ },
5071
+ });
5072
+ }
4745
5073
  const symbol = values.symbol || "BTCUSDT";
5074
+ // Проба осуществимости, НЕ поиск заработка (подбор параметров — у
5075
+ // --tune): собирающая прибыль механика выключена, остаётся вопрос
5076
+ // "есть ли прибыльный коридор" по стопу x холду x правилу бана —
5077
+ // 48 точек, быстрый вердикт
4746
5078
  BacktestKit.addSimulatorSchema({
4747
- simulatorName: SIMULATOR_NAME,
5079
+ simulatorName: SIMULATOR_NAME$1,
4748
5080
  exchangeName,
4749
5081
  gridAxes: {
4750
- hardStopPercent: [1, 1.5, 2, 2.5, 3, 4, 5, 7],
4751
- trailingTakePercent: [0.5, 1, 1.5, 2, 3, 4],
5082
+ // грубая шкала катастрофы: коридор должен быть широким, не точкой
5083
+ hardStopPercent: [2, 3, 5, 7],
5084
+ // инертен: проба не собирает прибыль, выход — по времени или стопу
5085
+ trailingTakePercent: [100],
4752
5086
  holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
4753
- minIdeasAligned: [1, 2, 3],
4754
- minAuthorTrack: [2, 3, 5],
5087
+ // одного проверенного автора достаточно — консенсус не перебираем
5088
+ minIdeasAligned: [1],
5089
+ // правило бана — единственная перебираемая "умность" пробы
5090
+ minAuthorTrack: [3, 5],
4755
5091
  minAuthorHitRate: [0.5, 0.6],
4756
- minWeightAligned: [0, 0.6, 1.2],
4757
- profitLockPercent: [0, 1.5, 2.5],
5092
+ minAuthorWilson: [0],
5093
+ minWeightAligned: [0],
5094
+ profitLockPercent: [0],
5095
+ authorMetric: ["close"],
5096
+ banCriteria: ["sharpe", "pnl"],
4758
5097
  },
5098
+ reportOrder: "sharpe",
4759
5099
  callbacks: {
4760
5100
  onProgress: (symbol, stage, processed, total) => {
4761
5101
  if (values.verbose) {
@@ -4817,7 +5157,7 @@ const main$4 = async () => {
4817
5157
  });
4818
5158
  const result = await BacktestKit.Simulator.run({
4819
5159
  symbol,
4820
- simulatorName: SIMULATOR_NAME,
5160
+ simulatorName: SIMULATOR_NAME$1,
4821
5161
  ideas,
4822
5162
  });
4823
5163
  const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
@@ -4832,11 +5172,299 @@ const main$4 = async () => {
4832
5172
  if (values.markdown) {
4833
5173
  const filePath = path.resolve(dumpDir, `${dumpName}.md`);
4834
5174
  await fs$1.mkdir(dumpDir, { recursive: true });
4835
- await fs$1.writeFile(filePath, toMarkdown(result), "utf-8");
5175
+ await fs$1.writeFile(filePath, toMarkdown$1(result), "utf-8");
5176
+ console.log(`Saved: ${filePath}`);
5177
+ process.exit(0);
5178
+ }
5179
+ console.log(toMarkdown$1(result));
5180
+ process.exit(0);
5181
+ };
5182
+ main$5();
5183
+
5184
+ const SIMULATOR_NAME = "cli_tune";
5185
+ /** Доля времени фида, уходящая в обучение; хвост — один OOS-выстрел. */
5186
+ const DEFAULT_TRAIN_SPLIT = 0.7;
5187
+ const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
5188
+ const validateIdea = (idea, line) => {
5189
+ if (typeof idea !== "object" || idea === null) {
5190
+ return `line ${line}: not an object`;
5191
+ }
5192
+ if (typeof idea.id !== "number") {
5193
+ return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
5194
+ }
5195
+ if (typeof idea.ts !== "number") {
5196
+ return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
5197
+ }
5198
+ if (typeof idea.symbol !== "string" || !idea.symbol) {
5199
+ return `line ${line}: "symbol" must be a non-empty string`;
5200
+ }
5201
+ if (!IDEA_DIRECTIONS.includes(idea.direction)) {
5202
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
5203
+ }
5204
+ if (typeof idea.author !== "string" || !idea.author) {
5205
+ return `line ${line}: "author" must be a non-empty string`;
5206
+ }
5207
+ return null;
5208
+ };
5209
+ const pointLabel = (point) => `H=${point.hardStopPercent} TT=${point.trailingTakePercent} hold=${point.holdMinutes / 60}h N=${point.minIdeasAligned} ` +
5210
+ `track=${point.minAuthorTrack} rate=${point.minAuthorHitRate} wilson=${point.minAuthorWilson} W=${point.minWeightAligned} ` +
5211
+ `lock=${point.profitLockPercent} metric=${point.authorMetric}`;
5212
+ const fmtRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
5213
+ const toMarkdown = (train, test, trainSplit) => {
5214
+ const lines = [];
5215
+ lines.push(`# Tune Report — ${train.symbol}`);
5216
+ lines.push("");
5217
+ lines.push(`Walk-forward: train on the first ${Math.round(trainSplit * 100)}% of the feed time range, ONE out-of-sample shot of the frozen sharpe winner on the tail.`);
5218
+ lines.push("");
5219
+ lines.push(`## Train (${train.ideasDirectional} directional ideas, ${train.reports.length} grid points)`);
5220
+ lines.push("");
5221
+ lines.push(`| Criterion | Point | Trades | PNL% | WinRate | DD% | Sharpe | Sortino |`);
5222
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- |`);
5223
+ for (const best of train.best) {
5224
+ if (!best.report) {
5225
+ lines.push(`| ${best.criterion} | — | — | — | — | — | — | — |`);
5226
+ continue;
5227
+ }
5228
+ lines.push(`| ${best.criterion} | ${pointLabel(best.report.point)} | ${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ` +
5229
+ `${(best.report.winRate * 100).toFixed(0)}% | ${best.report.maxSeriesDrawdownPercent.toFixed(2)} | ${fmtRatio(best.report.sharpe)} | ${fmtRatio(best.report.sortino)} |`);
5230
+ }
5231
+ lines.push("");
5232
+ lines.push(`## Out-of-sample (frozen sharpe winner, ${test.ideasDirectional} tail ideas)`);
5233
+ lines.push("");
5234
+ lines.push(`Frozen point: ${pointLabel(test.point)}`);
5235
+ lines.push("");
5236
+ lines.push(`| Metric | Value |`);
5237
+ lines.push(`| --- | --- |`);
5238
+ lines.push(`| Trades (skipped busy) | ${test.report.trades} (${test.report.skippedBusy}) |`);
5239
+ lines.push(`| PNL | ${test.report.totalPnlPercent.toFixed(2)}% |`);
5240
+ lines.push(`| Win rate | ${(test.report.winRate * 100).toFixed(0)}% |`);
5241
+ lines.push(`| Profit factor | ${fmtRatio(test.report.profitFactor)} |`);
5242
+ lines.push(`| Max series drawdown | ${test.report.maxSeriesDrawdownPercent.toFixed(2)}% |`);
5243
+ lines.push(`| Sharpe / Sortino | ${fmtRatio(test.report.sharpe)} / ${fmtRatio(test.report.sortino)} |`);
5244
+ lines.push(`| Recovery factor | ${fmtRatio(test.report.recoveryFactor)} |`);
5245
+ lines.push(`| Exits | ${Object.entries(test.report.exitReasons).map(([reason, count]) => `${reason}=${count}`).join(", ")} |`);
5246
+ lines.push("");
5247
+ lines.push(`## Test trades`);
5248
+ lines.push("");
5249
+ lines.push(`| Direction | Exit | PNL% | Hold | Entry (UTC) |`);
5250
+ lines.push(`| --- | --- | --- | --- | --- |`);
5251
+ for (const trade of test.trades) {
5252
+ lines.push(`| ${trade.direction} | ${trade.exitReason} | ${trade.pnlPercent.toFixed(2)} | ${trade.holdMinutesActual}m | ${new Date(trade.entryTimestamp).toISOString()} |`);
5253
+ }
5254
+ lines.push("");
5255
+ lines.push(`## Frozen author track record (train range, sharpe winner rule)`);
5256
+ lines.push("");
5257
+ lines.push(`Freeze these raw numbers for production \`Simulator.test\` — banned flags and vote weights are re-derived from them under the frozen point's rule; an author absent from the list is banned by default.`);
5258
+ lines.push("");
5259
+ lines.push(`| Author | Ideas | Hits | Banned |`);
5260
+ lines.push(`| --- | --- | --- | --- |`);
5261
+ for (const stat of test.authorStats) {
5262
+ lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${stat.banned ? "yes" : ""} |`);
5263
+ }
5264
+ return lines.join("\n");
5265
+ };
5266
+ const main$4 = async () => {
5267
+ if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
5268
+ return;
5269
+ }
5270
+ const { values, positionals } = getArgs();
5271
+ if (!values.tune) {
5272
+ return;
5273
+ }
5274
+ const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
5275
+ if (!ideasPath) {
5276
+ console.error("Error: positional path to an ideas .jsonl file is required");
5277
+ process.exit(1);
5278
+ }
5279
+ let ideas = [];
5280
+ {
5281
+ let content;
5282
+ try {
5283
+ content = await fs$1.readFile(path.resolve(ideasPath), "utf-8");
5284
+ }
5285
+ catch (error) {
5286
+ console.error(`Error: cannot read ideas file: ${ideasPath}`);
5287
+ process.exit(1);
5288
+ }
5289
+ const rows = content.split("\n").filter(Boolean);
5290
+ if (!rows.length) {
5291
+ console.error(`Error: ideas file is empty: ${ideasPath}`);
5292
+ process.exit(1);
5293
+ }
5294
+ for (let i = 0; i < rows.length; i++) {
5295
+ let idea;
5296
+ try {
5297
+ idea = JSON.parse(rows[i]);
5298
+ }
5299
+ catch {
5300
+ console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
5301
+ process.exit(1);
5302
+ }
5303
+ const problem = validateIdea(idea, i + 1);
5304
+ if (problem) {
5305
+ console.error(`Error: ideas file does not match the idea structure — ${problem}`);
5306
+ console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
5307
+ process.exit(1);
5308
+ }
5309
+ ideas.push(idea);
5310
+ }
5311
+ }
5312
+ const trainSplit = values.split
5313
+ ? parseFloat(values.split)
5314
+ : DEFAULT_TRAIN_SPLIT;
5315
+ if (!Number.isFinite(trainSplit) || trainSplit <= 0 || trainSplit >= 1) {
5316
+ console.error(`Error: --split must be a fraction between 0 and 1, got ${values.split}`);
5317
+ process.exit(1);
5318
+ }
5319
+ {
5320
+ const cwd = process.cwd();
5321
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
5322
+ }
5323
+ await cli.configConnectionService.loadConfig("setup.config");
5324
+ {
5325
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
5326
+ try {
5327
+ if (typeof loader === "function") {
5328
+ await loader();
5329
+ }
5330
+ if (typeof loader?.loader === "function") {
5331
+ await loader.loader();
5332
+ }
5333
+ }
5334
+ catch (error) {
5335
+ console.error("Module loader failed", error);
5336
+ process.exit(-1);
5337
+ }
5338
+ }
5339
+ await cli.moduleConnectionService.loadModule("tune.module");
5340
+ {
5341
+ await cli.exchangeSchemaService.addSchema();
5342
+ await cli.symbolSchemaService.addSchema();
5343
+ }
5344
+ const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
5345
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
5346
+ if (values.verbose) {
5347
+ BacktestKit.overrideExchangeSchema({
5348
+ exchangeName,
5349
+ callbacks: {
5350
+ onCandleData(symbol, interval, since) {
5351
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
5352
+ },
5353
+ },
5354
+ });
5355
+ }
5356
+ const symbol = values.symbol || "BTCUSDT";
5357
+ // Подбор параметров: полная сетка с собирающей прибыль механикой —
5358
+ // замок, трейлинг, взвешенный консенсус, Вильсон, обе метрики бана.
5359
+ // Честность обеспечивается сплитом: обучение видит только голову,
5360
+ // хвост тратится на один выстрел замороженного sharpe-победителя
5361
+ BacktestKit.addSimulatorSchema({
5362
+ simulatorName: SIMULATOR_NAME,
5363
+ exchangeName,
5364
+ gridAxes: {
5365
+ // стопы < 2% сидят внутри медианного шейкаута и не выигрывают
5366
+ hardStopPercent: [2, 2.5, 3, 4, 5, 7],
5367
+ trailingTakePercent: [1, 1.5, 2, 3, 4],
5368
+ holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
5369
+ minIdeasAligned: [1, 2],
5370
+ minAuthorTrack: [2, 3, 5],
5371
+ minAuthorHitRate: [0.5, 0.6],
5372
+ minAuthorWilson: [0, 0.6],
5373
+ minWeightAligned: [0, 0.6, 1.2],
5374
+ profitLockPercent: [0, 1.5, 2.5],
5375
+ authorMetric: ["close", "reach"],
5376
+ banCriteria: ["sharpe"],
5377
+ },
5378
+ reportOrder: "sharpe",
5379
+ callbacks: {
5380
+ onProgress: (symbol, stage, processed, total) => {
5381
+ if (values.verbose) {
5382
+ console.log("onProgress", { symbol, stage, processed, total });
5383
+ }
5384
+ },
5385
+ onIdeas: (symbol, ideasTotal, ideasDirectional) => {
5386
+ if (values.verbose) {
5387
+ console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
5388
+ }
5389
+ },
5390
+ onProfiles: (symbol, profiles, truncatedCount) => {
5391
+ if (values.verbose) {
5392
+ console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
5393
+ }
5394
+ },
5395
+ onAuthorsTrained: (symbol, stats, bannedIdeas) => {
5396
+ if (values.verbose) {
5397
+ console.log("onAuthorsTrained", {
5398
+ symbol,
5399
+ authors: stats.length,
5400
+ banned: stats.filter(({ banned }) => banned).length,
5401
+ bannedIdeas,
5402
+ });
5403
+ }
5404
+ },
5405
+ onRanking: (symbol, criterion, _sorted, best) => {
5406
+ if (values.verbose) {
5407
+ console.log("onRanking", {
5408
+ symbol,
5409
+ criterion,
5410
+ point: best.report?.point ?? null,
5411
+ pnl: best.report ? +best.report.totalPnlPercent.toFixed(2) : null,
5412
+ });
5413
+ }
5414
+ },
5415
+ onTestDone: (symbol, result) => {
5416
+ if (values.verbose) {
5417
+ console.log("onTestDone", {
5418
+ symbol,
5419
+ trades: result.report.trades,
5420
+ pnl: +result.report.totalPnlPercent.toFixed(2),
5421
+ });
5422
+ }
5423
+ },
5424
+ },
5425
+ });
5426
+ // обучение видит ТОЛЬКО голову ленты; хвост — один OOS-выстрел
5427
+ const sorted = [...ideas].sort((a, b) => a.ts - b.ts);
5428
+ const cutoff = sorted[0].ts + (sorted[sorted.length - 1].ts - sorted[0].ts) * trainSplit;
5429
+ const trainIdeas = sorted.filter(({ ts }) => ts < cutoff);
5430
+ const testIdeas = sorted.filter(({ ts }) => ts >= cutoff);
5431
+ const train = await BacktestKit.Simulator.run({
5432
+ symbol,
5433
+ simulatorName: SIMULATOR_NAME,
5434
+ ideas: trainIdeas,
5435
+ });
5436
+ const sharpeBest = train.best.find(({ criterion }) => criterion === "sharpe");
5437
+ if (!sharpeBest?.report) {
5438
+ console.error("Error: training produced no sharpe winner — nothing to freeze for the out-of-sample shot");
5439
+ process.exit(1);
5440
+ }
5441
+ // заморозка: точка и трек-рекорд sharpe-победителя как есть — из
5442
+ // замороженных статов test() читает только author/ideas/hits и сам
5443
+ // перевыводит баны и веса под правило точки
5444
+ const test = await BacktestKit.Simulator.test({
5445
+ symbol,
5446
+ simulatorName: SIMULATOR_NAME,
5447
+ ideas: testIdeas,
5448
+ point: sharpeBest.report.point,
5449
+ authorStats: sharpeBest.authorStats,
5450
+ });
5451
+ const dumpName = values.output || `tune_${symbol}_${Date.now()}`;
5452
+ const dumpDir = path.join(process.cwd(), "dump");
5453
+ if (values.json) {
5454
+ const filePath = path.resolve(dumpDir, `${dumpName}.json`);
5455
+ await fs$1.mkdir(dumpDir, { recursive: true });
5456
+ await fs$1.writeFile(filePath, JSON.stringify({ trainSplit, train, test }, null, 2), "utf-8");
5457
+ console.log(`Saved: ${filePath}`);
5458
+ process.exit(0);
5459
+ }
5460
+ if (values.markdown) {
5461
+ const filePath = path.resolve(dumpDir, `${dumpName}.md`);
5462
+ await fs$1.mkdir(dumpDir, { recursive: true });
5463
+ await fs$1.writeFile(filePath, toMarkdown(train, test, trainSplit), "utf-8");
4836
5464
  console.log(`Saved: ${filePath}`);
4837
5465
  process.exit(0);
4838
5466
  }
4839
- console.log(toMarkdown(result));
5467
+ console.log(toMarkdown(train, test, trainSplit));
4840
5468
  process.exit(0);
4841
5469
  };
4842
5470
  main$4();
@@ -5037,7 +5665,8 @@ Modes:
5037
5665
  --dump Fetch and save raw OHLCV candles
5038
5666
  --pnldebug Simulate PnL per minute for a given entry price and direction
5039
5667
  --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
5668
+ --simulator <ideas.jsonl> Feasibility probe over crowd ideas: is there a profitable corridor at all
5669
+ --tune <ideas.jsonl> Walk-forward parameter search: train on the head, one frozen shot on the tail
5041
5670
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
5042
5671
  --init Scaffold a new project in the current directory
5043
5672
  --docker Scaffold a Docker workspace for running strategies in a container
@@ -5172,10 +5801,13 @@ Simulator flags (--simulator):
5172
5801
  "author": string }. The file is validated BEFORE any work; a line that does not
5173
5802
  match the structure aborts the run with an error.
5174
5803
 
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.
5804
+ A FEASIBILITY PROBE, not a parameter search (that is --tune): the profit-harvesting
5805
+ machinery is off (no profit lock, inert trailing, no weighted consensus), and a
5806
+ fast 48-point grid of hard stop x hold x ban rule answers one question — does the
5807
+ feed contain a profitable corridor at all. One 5-day candle pass per idea, flood
5808
+ dedupe (one idea per author per direction per 8h), default-ban author filter,
5809
+ time-based Sharpe/Sortino, four ranking winners. The report carries the corridor
5810
+ share and the whitelist of the sharpe winner's ban rule as evidence.
5179
5811
  Ideas of other symbols are filtered out — one shared feed serves any --symbol.
5180
5812
 
5181
5813
  No output flag → print the Markdown summary to stdout. With --verbose every
@@ -5185,6 +5817,31 @@ Simulator flags (--simulator):
5185
5817
  Module file ./modules/simulator.module is loaded automatically if it exists
5186
5818
  (register your exchange there); without it CCXT Binance is used by default.
5187
5819
 
5820
+ Tune flags (--tune):
5821
+
5822
+ --symbol <string> Trading pair to tune (default: BTCUSDT)
5823
+ --exchange <string> Exchange name (default: first registered)
5824
+ --split <string> Train share of the feed time range, 0..1 (default: 0.7)
5825
+ --output <string> Output file base name (default: tune_{SYMBOL}_{TIMESTAMP})
5826
+ --json Save { trainSplit, train, test } to ./dump/<output>.json
5827
+ --markdown Save walk-forward report to ./dump/<output>.md
5828
+ --verbose Log simulator lifecycle callbacks to the console
5829
+
5830
+ Positional: path to an ideas .jsonl file — same shape and validation as --simulator.
5831
+
5832
+ The PARAMETER SEARCH counterpart of the --simulator probe: the full grid with the
5833
+ profit-harvesting machinery on — profit lock, trailing take, weighted consensus,
5834
+ Wilson-bound ban, both author-hit metrics (close/reach). Honesty is structural:
5835
+ training sees ONLY the head of the feed (--split of its time range), then the
5836
+ sharpe winner is frozen — point and raw author track record — and fired exactly
5837
+ once on the tail via Simulator.test. Nothing is trained on the tail; authors
5838
+ unseen in training are banned by default. The report carries the train winners,
5839
+ the out-of-sample result with the trade list, and the frozen author track record
5840
+ to hardcode for production Simulator.test.
5841
+
5842
+ Module file ./modules/tune.module is loaded automatically if it exists
5843
+ (register your exchange there); without it CCXT Binance is used by default.
5844
+
5188
5845
  Flush flags (--flush):
5189
5846
 
5190
5847
  One or more positional entry points. For each entry point the following
@@ -5218,7 +5875,8 @@ Module hooks (loaded automatically by each mode):
5218
5875
  modules/dump.module --dump Exchange schema for candle dumps
5219
5876
  modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
5220
5877
  modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
5221
- modules/simulator.module --simulator Exchange schema for crowd-ideas simulation
5878
+ modules/simulator.module --simulator Exchange schema for the crowd-ideas feasibility probe
5879
+ modules/tune.module --tune Exchange schema for the walk-forward parameter search
5222
5880
 
5223
5881
  --flush has no associated module. It only removes dump subdirectories.
5224
5882
 
@@ -5248,7 +5906,9 @@ Examples:
5248
5906
  node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
5249
5907
  node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
5250
5908
  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
5909
+ node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_probe ./assets/tv-ideas.normalized.jsonl
5910
+ node ${ENTRY_PATH} --tune --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5911
+ node ${ENTRY_PATH} --tune --symbol BTCUSDT --split 0.7 --markdown --output jun_2026_tune ./assets/tv-ideas.normalized.jsonl
5252
5912
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
5253
5913
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
5254
5914
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -5262,7 +5922,7 @@ const main$1 = async () => {
5262
5922
  if (!values.help) {
5263
5923
  return;
5264
5924
  }
5265
- process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n\n`);
5925
+ process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n\n`);
5266
5926
  process.stdout.write(HELP_TEXT);
5267
5927
  process.exit(0);
5268
5928
  };
@@ -5276,7 +5936,7 @@ const main = async () => {
5276
5936
  if (!values.version) {
5277
5937
  return;
5278
5938
  }
5279
- process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n`);
5939
+ process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
5280
5940
  process.exit(0);
5281
5941
  };
5282
5942
  main();