@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.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import * as BacktestKit from 'backtest-kit';
3
- import { setConfig, listenExit, emitters, Notification, Cron, Recent, Storage, Markdown, Report, Dump, State, Memory, MarkdownWriter, ReportWriter, PersistSignalAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistPartialAdapter, PersistBreakevenAdapter, PersistCandleAdapter, PersistStorageAdapter, PersistNotificationAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistIntervalAdapter, PersistMemoryAdapter, PersistRecentAdapter, PersistStateAdapter, PersistSessionAdapter, PersistStrategyAdapter, Log, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, RecentLive, RecentBacktest, Broker, SessionLive, SessionBacktest, MemoryLive, MemoryBacktest, StateLive, StateBacktest, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, System, Cache, Interval, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenOrderFill, listenOrderReject, listenOrderContinue, listenOrderStop, listenSignalNotify, Exchange, addSimulatorSchema, Simulator } from 'backtest-kit';
4
- import { singleshot, sleep, getErrorMessage, errorData, BehaviorSubject, Subject, str, compose, createAwaiter, execpool, queued, randomString, TIMEOUT_SYMBOL, typo, retry, trycatch, ttl, memoize, isObject } from 'functools-kit';
3
+ import { setConfig, listenExit, emitters, Notification, Cron, Recent, Storage, Markdown, Report, Dump, State, Memory, MarkdownWriter, ReportWriter, PersistSignalAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistPartialAdapter, PersistBreakevenAdapter, PersistCandleAdapter, PersistStorageAdapter, PersistNotificationAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistIntervalAdapter, PersistMemoryAdapter, PersistRecentAdapter, PersistStateAdapter, PersistSessionAdapter, PersistStrategyAdapter, Log, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, RecentLive, RecentBacktest, Broker, SessionLive, SessionBacktest, MemoryLive, MemoryBacktest, StateLive, StateBacktest, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, System, Cache, Interval, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenOrderFill, listenOrderReject, listenOrderContinue, listenOrderStop, listenSignalNotify, Exchange, runInMockContext, addSimulatorSchema, Simulator } from 'backtest-kit';
4
+ import { singleshot, sleep, getErrorMessage, errorData, BehaviorSubject, Subject, str, compose, createAwaiter, execpool, queued, randomString, TIMEOUT_SYMBOL, typo, retry, trycatch, ttl, Source, memoize, isObject } from 'functools-kit';
5
5
  import { exec, spawn } from 'child_process';
6
6
  import fs, { constants, realpathSync } from 'fs';
7
7
  import * as stackTrace from 'stack-trace';
@@ -28,7 +28,7 @@ import Module, { createRequire } from 'module';
28
28
  import * as BacktestKitGraph from '@backtest-kit/graph';
29
29
  import * as BacktestKitOllama from '@backtest-kit/ollama';
30
30
  import * as BacktestKitPinets from '@backtest-kit/pinets';
31
- import { run as run$1, Code, toMarkdown as toMarkdown$1 } from '@backtest-kit/pinets';
31
+ import { run as run$1, Code, toMarkdown as toMarkdown$2 } from '@backtest-kit/pinets';
32
32
  import * as BacktestKitSignals from '@backtest-kit/signals';
33
33
  import open from 'open';
34
34
 
@@ -208,14 +208,14 @@ const getReadySubject = singleshot(() => {
208
208
  return new Subject();
209
209
  });
210
210
 
211
- const main$j = () => {
211
+ const main$l = () => {
212
212
  const entrySubject = getEntrySubject();
213
213
  entrySubject.subscribe(async (path) => {
214
214
  console.log("Running", path);
215
215
  await getReadySubject().next();
216
216
  });
217
217
  };
218
- main$j();
218
+ main$l();
219
219
 
220
220
  const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
221
221
  function dumpStackTrace(error) {
@@ -870,6 +870,14 @@ const getArgs = singleshot(() => {
870
870
  type: "boolean",
871
871
  default: false,
872
872
  },
873
+ tune: {
874
+ type: "boolean",
875
+ default: false,
876
+ },
877
+ split: {
878
+ type: "string",
879
+ default: "",
880
+ },
873
881
  commit: {
874
882
  type: "string",
875
883
  default: "",
@@ -2702,8 +2710,11 @@ class TelegramLogicService {
2702
2710
  const unSignalNotify = listenSignalNotify(async (event) => {
2703
2711
  await this.notifySignalInfo(event);
2704
2712
  });
2713
+ const unGc = Source.fromInterval(90000).connect(() => {
2714
+ this.notifyOrderContinue.gc();
2715
+ });
2705
2716
  const unConnect = () => this.connect.clear();
2706
- const unListen = compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unSignalNotify(), () => unConnect());
2717
+ const unListen = compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unGc(), () => unSignalNotify(), () => unConnect());
2707
2718
  return () => {
2708
2719
  STOP_BOT_FN();
2709
2720
  unListen();
@@ -3644,14 +3655,14 @@ const cli = {
3644
3655
  };
3645
3656
  init();
3646
3657
 
3647
- const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "flush", "init", "docker", "help", "version"];
3658
+ const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "tune", "flush", "init", "docker", "help", "version"];
3648
3659
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
3649
3660
  const HELP_TEXT$1 = `
3650
3661
  Example:
3651
3662
 
3652
3663
  node ${ENTRY_PATH$1} --help
3653
3664
  `.trimStart();
3654
- const main$i = async () => {
3665
+ const main$k = async () => {
3655
3666
  if (!getEntry(import.meta.url)) {
3656
3667
  return;
3657
3668
  }
@@ -3659,14 +3670,14 @@ const main$i = async () => {
3659
3670
  if (MODES.some((mode) => values[mode])) {
3660
3671
  return;
3661
3672
  }
3662
- process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n`);
3673
+ process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
3663
3674
  process.stdout.write("\n");
3664
3675
  process.stdout.write(`Run with --help to see available commands.\n`);
3665
3676
  process.stdout.write("\n");
3666
3677
  process.stdout.write(HELP_TEXT$1);
3667
3678
  process.exit(0);
3668
3679
  };
3669
- main$i();
3680
+ main$k();
3670
3681
 
3671
3682
  const notifyShutdown = singleshot(async () => {
3672
3683
  console.log("Graceful shutdown initiated.");
@@ -3682,7 +3693,7 @@ const flush = async (entryPoint) => {
3682
3693
  console.log(`Removed: ${target}`);
3683
3694
  }
3684
3695
  };
3685
- const main$h = async () => {
3696
+ const main$j = async () => {
3686
3697
  if (!getEntry(import.meta.url)) {
3687
3698
  return;
3688
3699
  }
@@ -3699,7 +3710,7 @@ const main$h = async () => {
3699
3710
  }
3700
3711
  process.exit(0);
3701
3712
  };
3702
- main$h();
3713
+ main$j();
3703
3714
 
3704
3715
  const BEFORE_EXIT_FN$6 = singleshot(async () => {
3705
3716
  process.off("SIGINT", BEFORE_EXIT_FN$6);
@@ -3722,7 +3733,7 @@ const BEFORE_EXIT_FN$6 = singleshot(async () => {
3722
3733
  const listenGracefulShutdown$6 = singleshot(() => {
3723
3734
  process.on("SIGINT", BEFORE_EXIT_FN$6);
3724
3735
  });
3725
- const main$g = async () => {
3736
+ const main$i = async () => {
3726
3737
  if (!getEntry(import.meta.url)) {
3727
3738
  return;
3728
3739
  }
@@ -3740,7 +3751,7 @@ const main$g = async () => {
3740
3751
  await cli.backtestMainService.connect();
3741
3752
  listenGracefulShutdown$6();
3742
3753
  };
3743
- main$g();
3754
+ main$i();
3744
3755
 
3745
3756
  const BEFORE_EXIT_FN$5 = singleshot(async () => {
3746
3757
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3759,7 +3770,7 @@ const BEFORE_EXIT_FN$5 = singleshot(async () => {
3759
3770
  const listenGracefulShutdown$5 = singleshot(() => {
3760
3771
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3761
3772
  });
3762
- const main$f = async () => {
3773
+ const main$h = async () => {
3763
3774
  if (!getEntry(import.meta.url)) {
3764
3775
  return;
3765
3776
  }
@@ -3778,7 +3789,7 @@ const main$f = async () => {
3778
3789
  listenGracefulShutdown$5();
3779
3790
  await cli.walkerMainService.connect();
3780
3791
  };
3781
- main$f();
3792
+ main$h();
3782
3793
 
3783
3794
  const BEFORE_EXIT_FN$4 = singleshot(async () => {
3784
3795
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3800,7 +3811,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
3800
3811
  const listenGracefulShutdown$4 = singleshot(() => {
3801
3812
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3802
3813
  });
3803
- const main$e = async () => {
3814
+ const main$g = async () => {
3804
3815
  if (!getEntry(import.meta.url)) {
3805
3816
  return;
3806
3817
  }
@@ -3814,7 +3825,7 @@ const main$e = async () => {
3814
3825
  await cli.paperMainService.connect();
3815
3826
  listenGracefulShutdown$4();
3816
3827
  };
3817
- main$e();
3828
+ main$g();
3818
3829
 
3819
3830
  const BEFORE_EXIT_FN$3 = singleshot(async () => {
3820
3831
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3836,7 +3847,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
3836
3847
  const listenGracefulShutdown$3 = singleshot(() => {
3837
3848
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3838
3849
  });
3839
- const main$d = async () => {
3850
+ const main$f = async () => {
3840
3851
  if (!getEntry(import.meta.url)) {
3841
3852
  return;
3842
3853
  }
@@ -3850,7 +3861,7 @@ const main$d = async () => {
3850
3861
  await cli.liveMainService.connect();
3851
3862
  listenGracefulShutdown$3();
3852
3863
  };
3853
- main$d();
3864
+ main$f();
3854
3865
 
3855
3866
  const stopBacktestList$1 = async () => {
3856
3867
  for (const item of await Backtest.list()) {
@@ -3914,7 +3925,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
3914
3925
  const listenGracefulShutdown$2 = singleshot(() => {
3915
3926
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3916
3927
  });
3917
- const main$c = async () => {
3928
+ const main$e = async () => {
3918
3929
  if (!getEntry(import.meta.url)) {
3919
3930
  return;
3920
3931
  }
@@ -3970,7 +3981,7 @@ const main$c = async () => {
3970
3981
  listenGracefulShutdown$2();
3971
3982
  await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
3972
3983
  };
3973
- main$c();
3984
+ main$e();
3974
3985
 
3975
3986
  const MODE_MODULE = {
3976
3987
  backtest: "backtest.module",
@@ -4073,7 +4084,7 @@ const createGracefulShutdown = (mode) => {
4073
4084
  process.on("SIGINT", handler);
4074
4085
  });
4075
4086
  };
4076
- const main$b = async () => {
4087
+ const main$d = async () => {
4077
4088
  if (!getEntry(import.meta.url)) {
4078
4089
  return;
4079
4090
  }
@@ -4149,7 +4160,7 @@ const main$b = async () => {
4149
4160
  }
4150
4161
  await getEntrySubject().next(absolutePath);
4151
4162
  };
4152
- main$b();
4163
+ main$d();
4153
4164
 
4154
4165
  const BEFORE_EXIT_FN$1 = singleshot(async () => {
4155
4166
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -4160,7 +4171,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
4160
4171
  const listenGracefulShutdown$1 = singleshot(() => {
4161
4172
  process.on("SIGINT", BEFORE_EXIT_FN$1);
4162
4173
  });
4163
- const main$a = async () => {
4174
+ const main$c = async () => {
4164
4175
  if (!getEntry(import.meta.url)) {
4165
4176
  return;
4166
4177
  }
@@ -4170,7 +4181,7 @@ const main$a = async () => {
4170
4181
  }
4171
4182
  listenGracefulShutdown$1();
4172
4183
  };
4173
- main$a();
4184
+ main$c();
4174
4185
 
4175
4186
  const BEFORE_EXIT_FN = singleshot(async () => {
4176
4187
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -4181,7 +4192,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
4181
4192
  const listenGracefulShutdown = singleshot(() => {
4182
4193
  process.on("SIGINT", BEFORE_EXIT_FN);
4183
4194
  });
4184
- const main$9 = async () => {
4195
+ const main$b = async () => {
4185
4196
  if (!getEntry(import.meta.url)) {
4186
4197
  return;
4187
4198
  }
@@ -4191,7 +4202,7 @@ const main$9 = async () => {
4191
4202
  }
4192
4203
  listenGracefulShutdown();
4193
4204
  };
4194
- main$9();
4205
+ main$b();
4195
4206
 
4196
4207
  const EXTRACT_ROWS_FN = (plots, schema) => {
4197
4208
  const keys = Object.keys(schema);
@@ -4213,7 +4224,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
4213
4224
  }
4214
4225
  return rows;
4215
4226
  };
4216
- const main$8 = async () => {
4227
+ const main$a = async () => {
4217
4228
  if (!getEntry(import.meta.url)) {
4218
4229
  return;
4219
4230
  }
@@ -4305,17 +4316,17 @@ const main$8 = async () => {
4305
4316
  if (values.markdown) {
4306
4317
  const filePath = resolve(dumpDir, `${dumpName}.md`);
4307
4318
  await mkdir(dumpDir, { recursive: true });
4308
- await writeFile(filePath, await toMarkdown$1(signalId, plots, signalSchema), "utf-8");
4319
+ await writeFile(filePath, await toMarkdown$2(signalId, plots, signalSchema), "utf-8");
4309
4320
  console.log(`Saved: ${filePath}`);
4310
4321
  process.exit(0);
4311
4322
  return;
4312
4323
  }
4313
- console.log(await toMarkdown$1(signalId, plots, signalSchema));
4324
+ console.log(await toMarkdown$2(signalId, plots, signalSchema));
4314
4325
  process.exit(0);
4315
4326
  };
4316
- main$8();
4327
+ main$a();
4317
4328
 
4318
- const main$7 = async () => {
4329
+ const main$9 = async () => {
4319
4330
  if (!getEntry(import.meta.url)) {
4320
4331
  return;
4321
4332
  }
@@ -4378,9 +4389,9 @@ const main$7 = async () => {
4378
4389
  };
4379
4390
  process.on("SIGINT", beforeExit);
4380
4391
  };
4381
- main$7();
4392
+ main$9();
4382
4393
 
4383
- const main$6 = async () => {
4394
+ const main$8 = async () => {
4384
4395
  if (!getEntry(import.meta.url)) {
4385
4396
  return;
4386
4397
  }
@@ -4461,9 +4472,9 @@ const main$6 = async () => {
4461
4472
  console.log(JSON.stringify(candles, null, 2));
4462
4473
  process.exit(0);
4463
4474
  };
4464
- main$6();
4475
+ main$8();
4465
4476
 
4466
- const main$5 = async () => {
4477
+ const main$7 = async () => {
4467
4478
  if (!getEntry(import.meta.url)) {
4468
4479
  return;
4469
4480
  }
@@ -4577,11 +4588,314 @@ const main$5 = async () => {
4577
4588
  }
4578
4589
  process.exit(0);
4579
4590
  };
4580
- main$5();
4591
+ main$7();
4581
4592
 
4582
- const SIMULATOR_NAME = "cli_simulator";
4583
- const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
4584
- const validateIdea = (idea, line) => {
4593
+ const getBroker = singleshot(() => {
4594
+ // Reaches into BrokerAdapter internals on purpose: the public commit*
4595
+ // methods skip when payload.backtest=true and require enable(), while
4596
+ // brokerdebug must fire exactly one commit at the raw adapter proxy.
4597
+ const broker = Broker["getInstance"]();
4598
+ if (!broker) {
4599
+ throw new Error("Broker instance is not initialized.");
4600
+ }
4601
+ return broker;
4602
+ });
4603
+ /** Called when a new signal is opened (position entry confirmed). */
4604
+ const commitOrderOpen = trycatch(async (payload) => {
4605
+ const broker = getBroker();
4606
+ return await broker.onOrderOpenCommit(payload);
4607
+ }, {
4608
+ fallback: (error) => {
4609
+ console.log(error);
4610
+ process.exit(-1);
4611
+ },
4612
+ });
4613
+ /** Called when a new signal is closed (take-profit, stop-loss, or manual close). */
4614
+ const commitOrderClose = trycatch(async (payload) => {
4615
+ const broker = getBroker();
4616
+ return await broker.onOrderCloseCommit(payload);
4617
+ }, {
4618
+ fallback: (error) => {
4619
+ console.log(error);
4620
+ process.exit(-1);
4621
+ },
4622
+ });
4623
+ /** Called when a partial profit close is committed. */
4624
+ const commitPartialProfit = trycatch(async (payload) => {
4625
+ const broker = getBroker();
4626
+ return await broker.onPartialProfitCommit(payload);
4627
+ }, {
4628
+ fallback: (error) => {
4629
+ console.log(error);
4630
+ process.exit(-1);
4631
+ },
4632
+ });
4633
+ /** Called when a partial loss close is committed. */
4634
+ const commitPartialLoss = trycatch(async (payload) => {
4635
+ const broker = getBroker();
4636
+ return await broker.onPartialLossCommit(payload);
4637
+ }, {
4638
+ fallback: (error) => {
4639
+ console.log(error);
4640
+ process.exit(-1);
4641
+ },
4642
+ });
4643
+ /** Called when a trailing stop update is committed. */
4644
+ const commitTrailingStop = trycatch(async (payload) => {
4645
+ const broker = getBroker();
4646
+ return await broker.onTrailingStopCommit(payload);
4647
+ }, {
4648
+ fallback: (error) => {
4649
+ console.log(error);
4650
+ process.exit(-1);
4651
+ },
4652
+ });
4653
+ /** Called when a trailing take-profit update is committed. */
4654
+ const commitTrailingTake = trycatch(async (payload) => {
4655
+ const broker = getBroker();
4656
+ return await broker.onTrailingTakeCommit(payload);
4657
+ }, {
4658
+ fallback: (error) => {
4659
+ console.log(error);
4660
+ process.exit(-1);
4661
+ },
4662
+ });
4663
+ /** Called when a breakeven stop is committed (stop loss moved to entry price). */
4664
+ const commitBreakeven = trycatch(async (payload) => {
4665
+ const broker = getBroker();
4666
+ return await broker.onBreakevenCommit(payload);
4667
+ }, {
4668
+ fallback: (error) => {
4669
+ console.log(error);
4670
+ process.exit(-1);
4671
+ },
4672
+ });
4673
+ /** Called when a DCA (average-buy) entry is committed. */
4674
+ const commitAverageBuy = trycatch(async (payload) => {
4675
+ const broker = getBroker();
4676
+ return await broker.onAverageBuyCommit(payload);
4677
+ }, {
4678
+ fallback: (error) => {
4679
+ console.log(error);
4680
+ process.exit(-1);
4681
+ },
4682
+ });
4683
+ const COMMITS = [
4684
+ "signal-open",
4685
+ "signal-close",
4686
+ "partial-profit",
4687
+ "partial-loss",
4688
+ "average-buy",
4689
+ "trailing-stop",
4690
+ "trailing-take",
4691
+ "breakeven",
4692
+ ];
4693
+ const ZERO_PNL = {
4694
+ pnlPercentage: 0,
4695
+ priceOpen: 0,
4696
+ priceClose: 0,
4697
+ pnlCost: 0,
4698
+ pnlEntries: 0,
4699
+ };
4700
+ const main$6 = async () => {
4701
+ if (!getEntry(import.meta.url)) {
4702
+ return;
4703
+ }
4704
+ const { values } = getArgs();
4705
+ if (!values.brokerdebug) {
4706
+ return;
4707
+ }
4708
+ {
4709
+ const cwd = process.cwd();
4710
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
4711
+ }
4712
+ await cli.configConnectionService.loadConfig("setup.config");
4713
+ {
4714
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
4715
+ try {
4716
+ if (typeof loader === "function") {
4717
+ await loader();
4718
+ }
4719
+ if (typeof loader?.loader === "function") {
4720
+ await loader.loader();
4721
+ }
4722
+ }
4723
+ catch (error) {
4724
+ console.error("Module loader failed", error);
4725
+ process.exit(-1);
4726
+ }
4727
+ }
4728
+ await cli.moduleConnectionService.loadModule("brokerdebug.module");
4729
+ const [defaultExchangeName = null] = await listExchangeSchema();
4730
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
4731
+ const symbol = values.symbol || "BTCUSDT";
4732
+ const commit = values.commit || "signal-open";
4733
+ if (!COMMITS.includes(commit)) {
4734
+ console.error(`Unknown --commit value: ${commit}`);
4735
+ console.error(`Available: ${COMMITS.join(", ")}`);
4736
+ process.exit(1);
4737
+ }
4738
+ const candles = await Exchange.getCandles(symbol, "1m", 5, { exchangeName });
4739
+ const lastCandle = candles[candles.length - 1];
4740
+ const currentPrice = lastCandle.close;
4741
+ const priceTakeProfit = currentPrice * 1.02;
4742
+ const priceStopLoss = currentPrice * 0.98;
4743
+ const context = {
4744
+ strategyName: "broker-test",
4745
+ exchangeName: exchangeName,
4746
+ };
4747
+ const signalId = `broker-debug-${Date.now()}`;
4748
+ const when = alignToInterval(new Date(), "1m");
4749
+ {
4750
+ console.log(`symbol=${symbol} price=${currentPrice} commit=${commit}`);
4751
+ console.time(commit);
4752
+ }
4753
+ const run = async () => {
4754
+ if (commit === "signal-open") {
4755
+ await commitOrderOpen({
4756
+ type: "active",
4757
+ symbol,
4758
+ signalId,
4759
+ cost: 100,
4760
+ position: "long",
4761
+ priceOpen: currentPrice,
4762
+ priceTakeProfit,
4763
+ priceStopLoss,
4764
+ pnl: ZERO_PNL,
4765
+ peakProfit: ZERO_PNL,
4766
+ maxDrawdown: ZERO_PNL,
4767
+ attempt: 0,
4768
+ context,
4769
+ when,
4770
+ backtest: true,
4771
+ });
4772
+ }
4773
+ if (commit === "signal-close") {
4774
+ await commitOrderClose({
4775
+ symbol,
4776
+ signalId,
4777
+ cost: 100,
4778
+ position: "long",
4779
+ currentPrice,
4780
+ priceOpen: currentPrice,
4781
+ priceTakeProfit,
4782
+ priceStopLoss,
4783
+ totalEntries: 1,
4784
+ totalPartials: 0,
4785
+ attempt: 0,
4786
+ pnl: ZERO_PNL,
4787
+ peakProfit: ZERO_PNL,
4788
+ maxDrawdown: ZERO_PNL,
4789
+ context,
4790
+ when,
4791
+ backtest: true,
4792
+ });
4793
+ }
4794
+ if (commit === "partial-profit") {
4795
+ await commitPartialProfit({
4796
+ symbol,
4797
+ signalId,
4798
+ percentToClose: 50,
4799
+ cost: 50,
4800
+ currentPrice,
4801
+ position: "long",
4802
+ priceTakeProfit,
4803
+ priceStopLoss,
4804
+ context,
4805
+ when,
4806
+ backtest: true,
4807
+ });
4808
+ }
4809
+ if (commit === "partial-loss") {
4810
+ await commitPartialLoss({
4811
+ symbol,
4812
+ signalId,
4813
+ percentToClose: 50,
4814
+ cost: 50,
4815
+ currentPrice,
4816
+ position: "long",
4817
+ priceTakeProfit,
4818
+ priceStopLoss,
4819
+ context,
4820
+ when,
4821
+ backtest: true,
4822
+ });
4823
+ }
4824
+ if (commit === "average-buy") {
4825
+ await commitAverageBuy({
4826
+ symbol,
4827
+ signalId,
4828
+ currentPrice,
4829
+ cost: 100,
4830
+ position: "long",
4831
+ priceTakeProfit,
4832
+ priceStopLoss,
4833
+ context,
4834
+ when,
4835
+ backtest: true,
4836
+ });
4837
+ }
4838
+ if (commit === "trailing-stop") {
4839
+ await commitTrailingStop({
4840
+ symbol,
4841
+ signalId,
4842
+ percentShift: -50,
4843
+ currentPrice,
4844
+ newStopLossPrice: priceStopLoss + (currentPrice - priceStopLoss) * 0.5,
4845
+ takeProfitPrice: priceTakeProfit,
4846
+ position: "long",
4847
+ context,
4848
+ when,
4849
+ backtest: true,
4850
+ });
4851
+ }
4852
+ if (commit === "trailing-take") {
4853
+ await commitTrailingTake({
4854
+ symbol,
4855
+ signalId,
4856
+ percentShift: 50,
4857
+ currentPrice,
4858
+ newTakeProfitPrice: priceTakeProfit + (priceTakeProfit - currentPrice) * 0.5,
4859
+ takeProfitPrice: priceTakeProfit,
4860
+ position: "long",
4861
+ context,
4862
+ when,
4863
+ backtest: true,
4864
+ });
4865
+ }
4866
+ if (commit === "breakeven") {
4867
+ await commitBreakeven({
4868
+ symbol,
4869
+ signalId,
4870
+ currentPrice,
4871
+ newStopLossPrice: currentPrice,
4872
+ newTakeProfitPrice: priceTakeProfit,
4873
+ position: "long",
4874
+ context,
4875
+ when,
4876
+ backtest: true,
4877
+ });
4878
+ }
4879
+ };
4880
+ await runInMockContext(run, {
4881
+ when,
4882
+ strategyName: context.strategyName,
4883
+ exchangeName: context.exchangeName,
4884
+ frameName: "",
4885
+ symbol,
4886
+ backtest: true,
4887
+ });
4888
+ {
4889
+ console.log(`${commit} OK`);
4890
+ console.timeEnd(commit);
4891
+ }
4892
+ process.exit(0);
4893
+ };
4894
+ main$6();
4895
+
4896
+ const SIMULATOR_NAME$1 = "cli_simulator";
4897
+ const IDEA_DIRECTIONS$1 = ["LONG", "SHORT", "NEUTRAL"];
4898
+ const validateIdea$1 = (idea, line) => {
4585
4899
  if (typeof idea !== "object" || idea === null) {
4586
4900
  return `line ${line}: not an object`;
4587
4901
  }
@@ -4594,15 +4908,16 @@ const validateIdea = (idea, line) => {
4594
4908
  if (typeof idea.symbol !== "string" || !idea.symbol) {
4595
4909
  return `line ${line}: "symbol" must be a non-empty string`;
4596
4910
  }
4597
- if (!IDEA_DIRECTIONS.includes(idea.direction)) {
4598
- return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
4911
+ if (!IDEA_DIRECTIONS$1.includes(idea.direction)) {
4912
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS$1.join("|")}, got ${JSON.stringify(idea.direction)}`;
4599
4913
  }
4600
4914
  if (typeof idea.author !== "string" || !idea.author) {
4601
4915
  return `line ${line}: "author" must be a non-empty string`;
4602
4916
  }
4603
4917
  return null;
4604
4918
  };
4605
- const toMarkdown = (result) => {
4919
+ const toMarkdown$1 = (result) => {
4920
+ const profitable = result.reports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
4606
4921
  const lines = [];
4607
4922
  lines.push(`# Simulator Report — ${result.symbol}`);
4608
4923
  lines.push("");
@@ -4611,33 +4926,36 @@ const toMarkdown = (result) => {
4611
4926
  lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
4612
4927
  lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
4613
4928
  lines.push(`| Authors allowed / banned | ${result.allowedAuthors.length} / ${result.bannedAuthors.length} |`);
4614
- lines.push(`| Grid points | ${result.reports.length} |`);
4929
+ lines.push(`| Profitable corridor | ${profitable} / ${result.reports.length} grid points |`);
4615
4930
  lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
4616
4931
  lines.push("");
4617
4932
  lines.push(`## Ranking winners`);
4618
4933
  lines.push("");
4619
- lines.push(`| Criterion | Stop% | Trailing% | Hold | Aligned | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4620
- lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4934
+ lines.push(`| Criterion | Stop% | Hold | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4935
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4621
4936
  for (const best of result.best) {
4622
4937
  if (!best.report) {
4623
- lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — | — | — |`);
4938
+ lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
4624
4939
  continue;
4625
4940
  }
4626
4941
  const { point } = best.report;
4627
- lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.trailingTakePercent} | ${point.holdMinutes / 60}h | ${point.minIdeasAligned} | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
4942
+ lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.holdMinutes / 60}h | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
4628
4943
  `${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
4629
4944
  }
4630
4945
  lines.push("");
4631
- lines.push(`## Allowed authors (production whitelist)`);
4946
+ lines.push(`## Allowed authors (whitelist of the sharpe winner rule)`);
4632
4947
  lines.push("");
4633
4948
  lines.push(`| Author | Ideas | Hits | HitRate |`);
4634
4949
  lines.push(`| --- | --- | --- | --- |`);
4635
- for (const stat of result.authorStats.filter(({ banned }) => !banned)) {
4950
+ // артефакт авторов живёт по-победительно в best[] — ран-левел
4951
+ // authorStats в ISimulatorResult больше нет
4952
+ const sharpeBest = result.best.find(({ criterion }) => criterion === "sharpe");
4953
+ for (const stat of (sharpeBest?.authorStats ?? []).filter(({ banned }) => !banned)) {
4636
4954
  lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
4637
4955
  }
4638
4956
  return lines.join("\n");
4639
4957
  };
4640
- const main$4 = async () => {
4958
+ const main$5 = async () => {
4641
4959
  if (!getEntry(import.meta.url)) {
4642
4960
  return;
4643
4961
  }
@@ -4674,7 +4992,7 @@ const main$4 = async () => {
4674
4992
  console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
4675
4993
  process.exit(1);
4676
4994
  }
4677
- const problem = validateIdea(idea, i + 1);
4995
+ const problem = validateIdea$1(idea, i + 1);
4678
4996
  if (problem) {
4679
4997
  console.error(`Error: ideas file does not match the idea structure — ${problem}`);
4680
4998
  console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
@@ -4710,20 +5028,42 @@ const main$4 = async () => {
4710
5028
  }
4711
5029
  const [defaultExchangeName = null] = await listExchangeSchema();
4712
5030
  const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
5031
+ if (values.verbose) {
5032
+ overrideExchangeSchema({
5033
+ exchangeName,
5034
+ callbacks: {
5035
+ onCandleData(symbol, interval, since) {
5036
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
5037
+ },
5038
+ },
5039
+ });
5040
+ }
4713
5041
  const symbol = values.symbol || "BTCUSDT";
5042
+ // Проба осуществимости, НЕ поиск заработка (подбор параметров — у
5043
+ // --tune): собирающая прибыль механика выключена, остаётся вопрос
5044
+ // "есть ли прибыльный коридор" по стопу x холду x правилу бана —
5045
+ // 48 точек, быстрый вердикт
4714
5046
  addSimulatorSchema({
4715
- simulatorName: SIMULATOR_NAME,
5047
+ simulatorName: SIMULATOR_NAME$1,
4716
5048
  exchangeName,
4717
5049
  gridAxes: {
4718
- hardStopPercent: [1, 1.5, 2, 2.5, 3, 4, 5, 7],
4719
- trailingTakePercent: [0.5, 1, 1.5, 2, 3, 4],
5050
+ // грубая шкала катастрофы: коридор должен быть широким, не точкой
5051
+ hardStopPercent: [2, 3, 5, 7],
5052
+ // инертен: проба не собирает прибыль, выход — по времени или стопу
5053
+ trailingTakePercent: [100],
4720
5054
  holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
4721
- minIdeasAligned: [1, 2, 3],
4722
- minAuthorTrack: [2, 3, 5],
5055
+ // одного проверенного автора достаточно — консенсус не перебираем
5056
+ minIdeasAligned: [1],
5057
+ // правило бана — единственная перебираемая "умность" пробы
5058
+ minAuthorTrack: [3, 5],
4723
5059
  minAuthorHitRate: [0.5, 0.6],
4724
- minWeightAligned: [0, 0.6, 1.2],
4725
- profitLockPercent: [0, 1.5, 2.5],
5060
+ minAuthorWilson: [0],
5061
+ minWeightAligned: [0],
5062
+ profitLockPercent: [0],
5063
+ authorMetric: ["close"],
5064
+ banCriteria: ["sharpe", "pnl"],
4726
5065
  },
5066
+ reportOrder: "sharpe",
4727
5067
  callbacks: {
4728
5068
  onProgress: (symbol, stage, processed, total) => {
4729
5069
  if (values.verbose) {
@@ -4785,7 +5125,7 @@ const main$4 = async () => {
4785
5125
  });
4786
5126
  const result = await Simulator.run({
4787
5127
  symbol,
4788
- simulatorName: SIMULATOR_NAME,
5128
+ simulatorName: SIMULATOR_NAME$1,
4789
5129
  ideas,
4790
5130
  });
4791
5131
  const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
@@ -4800,11 +5140,299 @@ const main$4 = async () => {
4800
5140
  if (values.markdown) {
4801
5141
  const filePath = resolve(dumpDir, `${dumpName}.md`);
4802
5142
  await mkdir(dumpDir, { recursive: true });
4803
- await writeFile(filePath, toMarkdown(result), "utf-8");
5143
+ await writeFile(filePath, toMarkdown$1(result), "utf-8");
5144
+ console.log(`Saved: ${filePath}`);
5145
+ process.exit(0);
5146
+ }
5147
+ console.log(toMarkdown$1(result));
5148
+ process.exit(0);
5149
+ };
5150
+ main$5();
5151
+
5152
+ const SIMULATOR_NAME = "cli_tune";
5153
+ /** Доля времени фида, уходящая в обучение; хвост — один OOS-выстрел. */
5154
+ const DEFAULT_TRAIN_SPLIT = 0.7;
5155
+ const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
5156
+ const validateIdea = (idea, line) => {
5157
+ if (typeof idea !== "object" || idea === null) {
5158
+ return `line ${line}: not an object`;
5159
+ }
5160
+ if (typeof idea.id !== "number") {
5161
+ return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
5162
+ }
5163
+ if (typeof idea.ts !== "number") {
5164
+ return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
5165
+ }
5166
+ if (typeof idea.symbol !== "string" || !idea.symbol) {
5167
+ return `line ${line}: "symbol" must be a non-empty string`;
5168
+ }
5169
+ if (!IDEA_DIRECTIONS.includes(idea.direction)) {
5170
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
5171
+ }
5172
+ if (typeof idea.author !== "string" || !idea.author) {
5173
+ return `line ${line}: "author" must be a non-empty string`;
5174
+ }
5175
+ return null;
5176
+ };
5177
+ const pointLabel = (point) => `H=${point.hardStopPercent} TT=${point.trailingTakePercent} hold=${point.holdMinutes / 60}h N=${point.minIdeasAligned} ` +
5178
+ `track=${point.minAuthorTrack} rate=${point.minAuthorHitRate} wilson=${point.minAuthorWilson} W=${point.minWeightAligned} ` +
5179
+ `lock=${point.profitLockPercent} metric=${point.authorMetric}`;
5180
+ const fmtRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
5181
+ const toMarkdown = (train, test, trainSplit) => {
5182
+ const lines = [];
5183
+ lines.push(`# Tune Report — ${train.symbol}`);
5184
+ lines.push("");
5185
+ lines.push(`Walk-forward: train on the first ${Math.round(trainSplit * 100)}% of the feed time range, ONE out-of-sample shot of the frozen sharpe winner on the tail.`);
5186
+ lines.push("");
5187
+ lines.push(`## Train (${train.ideasDirectional} directional ideas, ${train.reports.length} grid points)`);
5188
+ lines.push("");
5189
+ lines.push(`| Criterion | Point | Trades | PNL% | WinRate | DD% | Sharpe | Sortino |`);
5190
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- |`);
5191
+ for (const best of train.best) {
5192
+ if (!best.report) {
5193
+ lines.push(`| ${best.criterion} | — | — | — | — | — | — | — |`);
5194
+ continue;
5195
+ }
5196
+ lines.push(`| ${best.criterion} | ${pointLabel(best.report.point)} | ${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ` +
5197
+ `${(best.report.winRate * 100).toFixed(0)}% | ${best.report.maxSeriesDrawdownPercent.toFixed(2)} | ${fmtRatio(best.report.sharpe)} | ${fmtRatio(best.report.sortino)} |`);
5198
+ }
5199
+ lines.push("");
5200
+ lines.push(`## Out-of-sample (frozen sharpe winner, ${test.ideasDirectional} tail ideas)`);
5201
+ lines.push("");
5202
+ lines.push(`Frozen point: ${pointLabel(test.point)}`);
5203
+ lines.push("");
5204
+ lines.push(`| Metric | Value |`);
5205
+ lines.push(`| --- | --- |`);
5206
+ lines.push(`| Trades (skipped busy) | ${test.report.trades} (${test.report.skippedBusy}) |`);
5207
+ lines.push(`| PNL | ${test.report.totalPnlPercent.toFixed(2)}% |`);
5208
+ lines.push(`| Win rate | ${(test.report.winRate * 100).toFixed(0)}% |`);
5209
+ lines.push(`| Profit factor | ${fmtRatio(test.report.profitFactor)} |`);
5210
+ lines.push(`| Max series drawdown | ${test.report.maxSeriesDrawdownPercent.toFixed(2)}% |`);
5211
+ lines.push(`| Sharpe / Sortino | ${fmtRatio(test.report.sharpe)} / ${fmtRatio(test.report.sortino)} |`);
5212
+ lines.push(`| Recovery factor | ${fmtRatio(test.report.recoveryFactor)} |`);
5213
+ lines.push(`| Exits | ${Object.entries(test.report.exitReasons).map(([reason, count]) => `${reason}=${count}`).join(", ")} |`);
5214
+ lines.push("");
5215
+ lines.push(`## Test trades`);
5216
+ lines.push("");
5217
+ lines.push(`| Direction | Exit | PNL% | Hold | Entry (UTC) |`);
5218
+ lines.push(`| --- | --- | --- | --- | --- |`);
5219
+ for (const trade of test.trades) {
5220
+ lines.push(`| ${trade.direction} | ${trade.exitReason} | ${trade.pnlPercent.toFixed(2)} | ${trade.holdMinutesActual}m | ${new Date(trade.entryTimestamp).toISOString()} |`);
5221
+ }
5222
+ lines.push("");
5223
+ lines.push(`## Frozen author track record (train range, sharpe winner rule)`);
5224
+ lines.push("");
5225
+ lines.push(`Freeze these raw numbers for production \`Simulator.test\` — banned flags and vote weights are re-derived from them under the frozen point's rule; an author absent from the list is banned by default.`);
5226
+ lines.push("");
5227
+ lines.push(`| Author | Ideas | Hits | Banned |`);
5228
+ lines.push(`| --- | --- | --- | --- |`);
5229
+ for (const stat of test.authorStats) {
5230
+ lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${stat.banned ? "yes" : ""} |`);
5231
+ }
5232
+ return lines.join("\n");
5233
+ };
5234
+ const main$4 = async () => {
5235
+ if (!getEntry(import.meta.url)) {
5236
+ return;
5237
+ }
5238
+ const { values, positionals } = getArgs();
5239
+ if (!values.tune) {
5240
+ return;
5241
+ }
5242
+ const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
5243
+ if (!ideasPath) {
5244
+ console.error("Error: positional path to an ideas .jsonl file is required");
5245
+ process.exit(1);
5246
+ }
5247
+ let ideas = [];
5248
+ {
5249
+ let content;
5250
+ try {
5251
+ content = await readFile(resolve(ideasPath), "utf-8");
5252
+ }
5253
+ catch (error) {
5254
+ console.error(`Error: cannot read ideas file: ${ideasPath}`);
5255
+ process.exit(1);
5256
+ }
5257
+ const rows = content.split("\n").filter(Boolean);
5258
+ if (!rows.length) {
5259
+ console.error(`Error: ideas file is empty: ${ideasPath}`);
5260
+ process.exit(1);
5261
+ }
5262
+ for (let i = 0; i < rows.length; i++) {
5263
+ let idea;
5264
+ try {
5265
+ idea = JSON.parse(rows[i]);
5266
+ }
5267
+ catch {
5268
+ console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
5269
+ process.exit(1);
5270
+ }
5271
+ const problem = validateIdea(idea, i + 1);
5272
+ if (problem) {
5273
+ console.error(`Error: ideas file does not match the idea structure — ${problem}`);
5274
+ console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
5275
+ process.exit(1);
5276
+ }
5277
+ ideas.push(idea);
5278
+ }
5279
+ }
5280
+ const trainSplit = values.split
5281
+ ? parseFloat(values.split)
5282
+ : DEFAULT_TRAIN_SPLIT;
5283
+ if (!Number.isFinite(trainSplit) || trainSplit <= 0 || trainSplit >= 1) {
5284
+ console.error(`Error: --split must be a fraction between 0 and 1, got ${values.split}`);
5285
+ process.exit(1);
5286
+ }
5287
+ {
5288
+ const cwd = process.cwd();
5289
+ dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
5290
+ }
5291
+ await cli.configConnectionService.loadConfig("setup.config");
5292
+ {
5293
+ const loader = await cli.configConnectionService.loadConfig("loader.config");
5294
+ try {
5295
+ if (typeof loader === "function") {
5296
+ await loader();
5297
+ }
5298
+ if (typeof loader?.loader === "function") {
5299
+ await loader.loader();
5300
+ }
5301
+ }
5302
+ catch (error) {
5303
+ console.error("Module loader failed", error);
5304
+ process.exit(-1);
5305
+ }
5306
+ }
5307
+ await cli.moduleConnectionService.loadModule("tune.module");
5308
+ {
5309
+ await cli.exchangeSchemaService.addSchema();
5310
+ await cli.symbolSchemaService.addSchema();
5311
+ }
5312
+ const [defaultExchangeName = null] = await listExchangeSchema();
5313
+ const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
5314
+ if (values.verbose) {
5315
+ overrideExchangeSchema({
5316
+ exchangeName,
5317
+ callbacks: {
5318
+ onCandleData(symbol, interval, since) {
5319
+ console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
5320
+ },
5321
+ },
5322
+ });
5323
+ }
5324
+ const symbol = values.symbol || "BTCUSDT";
5325
+ // Подбор параметров: полная сетка с собирающей прибыль механикой —
5326
+ // замок, трейлинг, взвешенный консенсус, Вильсон, обе метрики бана.
5327
+ // Честность обеспечивается сплитом: обучение видит только голову,
5328
+ // хвост тратится на один выстрел замороженного sharpe-победителя
5329
+ addSimulatorSchema({
5330
+ simulatorName: SIMULATOR_NAME,
5331
+ exchangeName,
5332
+ gridAxes: {
5333
+ // стопы < 2% сидят внутри медианного шейкаута и не выигрывают
5334
+ hardStopPercent: [2, 2.5, 3, 4, 5, 7],
5335
+ trailingTakePercent: [1, 1.5, 2, 3, 4],
5336
+ holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
5337
+ minIdeasAligned: [1, 2],
5338
+ minAuthorTrack: [2, 3, 5],
5339
+ minAuthorHitRate: [0.5, 0.6],
5340
+ minAuthorWilson: [0, 0.6],
5341
+ minWeightAligned: [0, 0.6, 1.2],
5342
+ profitLockPercent: [0, 1.5, 2.5],
5343
+ authorMetric: ["close", "reach"],
5344
+ banCriteria: ["sharpe"],
5345
+ },
5346
+ reportOrder: "sharpe",
5347
+ callbacks: {
5348
+ onProgress: (symbol, stage, processed, total) => {
5349
+ if (values.verbose) {
5350
+ console.log("onProgress", { symbol, stage, processed, total });
5351
+ }
5352
+ },
5353
+ onIdeas: (symbol, ideasTotal, ideasDirectional) => {
5354
+ if (values.verbose) {
5355
+ console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
5356
+ }
5357
+ },
5358
+ onProfiles: (symbol, profiles, truncatedCount) => {
5359
+ if (values.verbose) {
5360
+ console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
5361
+ }
5362
+ },
5363
+ onAuthorsTrained: (symbol, stats, bannedIdeas) => {
5364
+ if (values.verbose) {
5365
+ console.log("onAuthorsTrained", {
5366
+ symbol,
5367
+ authors: stats.length,
5368
+ banned: stats.filter(({ banned }) => banned).length,
5369
+ bannedIdeas,
5370
+ });
5371
+ }
5372
+ },
5373
+ onRanking: (symbol, criterion, _sorted, best) => {
5374
+ if (values.verbose) {
5375
+ console.log("onRanking", {
5376
+ symbol,
5377
+ criterion,
5378
+ point: best.report?.point ?? null,
5379
+ pnl: best.report ? +best.report.totalPnlPercent.toFixed(2) : null,
5380
+ });
5381
+ }
5382
+ },
5383
+ onTestDone: (symbol, result) => {
5384
+ if (values.verbose) {
5385
+ console.log("onTestDone", {
5386
+ symbol,
5387
+ trades: result.report.trades,
5388
+ pnl: +result.report.totalPnlPercent.toFixed(2),
5389
+ });
5390
+ }
5391
+ },
5392
+ },
5393
+ });
5394
+ // обучение видит ТОЛЬКО голову ленты; хвост — один OOS-выстрел
5395
+ const sorted = [...ideas].sort((a, b) => a.ts - b.ts);
5396
+ const cutoff = sorted[0].ts + (sorted[sorted.length - 1].ts - sorted[0].ts) * trainSplit;
5397
+ const trainIdeas = sorted.filter(({ ts }) => ts < cutoff);
5398
+ const testIdeas = sorted.filter(({ ts }) => ts >= cutoff);
5399
+ const train = await Simulator.run({
5400
+ symbol,
5401
+ simulatorName: SIMULATOR_NAME,
5402
+ ideas: trainIdeas,
5403
+ });
5404
+ const sharpeBest = train.best.find(({ criterion }) => criterion === "sharpe");
5405
+ if (!sharpeBest?.report) {
5406
+ console.error("Error: training produced no sharpe winner — nothing to freeze for the out-of-sample shot");
5407
+ process.exit(1);
5408
+ }
5409
+ // заморозка: точка и трек-рекорд sharpe-победителя как есть — из
5410
+ // замороженных статов test() читает только author/ideas/hits и сам
5411
+ // перевыводит баны и веса под правило точки
5412
+ const test = await Simulator.test({
5413
+ symbol,
5414
+ simulatorName: SIMULATOR_NAME,
5415
+ ideas: testIdeas,
5416
+ point: sharpeBest.report.point,
5417
+ authorStats: sharpeBest.authorStats,
5418
+ });
5419
+ const dumpName = values.output || `tune_${symbol}_${Date.now()}`;
5420
+ const dumpDir = join(process.cwd(), "dump");
5421
+ if (values.json) {
5422
+ const filePath = resolve(dumpDir, `${dumpName}.json`);
5423
+ await mkdir(dumpDir, { recursive: true });
5424
+ await writeFile(filePath, JSON.stringify({ trainSplit, train, test }, null, 2), "utf-8");
5425
+ console.log(`Saved: ${filePath}`);
5426
+ process.exit(0);
5427
+ }
5428
+ if (values.markdown) {
5429
+ const filePath = resolve(dumpDir, `${dumpName}.md`);
5430
+ await mkdir(dumpDir, { recursive: true });
5431
+ await writeFile(filePath, toMarkdown(train, test, trainSplit), "utf-8");
4804
5432
  console.log(`Saved: ${filePath}`);
4805
5433
  process.exit(0);
4806
5434
  }
4807
- console.log(toMarkdown(result));
5435
+ console.log(toMarkdown(train, test, trainSplit));
4808
5436
  process.exit(0);
4809
5437
  };
4810
5438
  main$4();
@@ -5005,7 +5633,8 @@ Modes:
5005
5633
  --dump Fetch and save raw OHLCV candles
5006
5634
  --pnldebug Simulate PnL per minute for a given entry price and direction
5007
5635
  --brokerdebug Fire a single broker commit against the live broker adapter
5008
- --simulator <ideas.jsonl> Sweep exit/entry parameters over crowd ideas, train author whitelist
5636
+ --simulator <ideas.jsonl> Feasibility probe over crowd ideas: is there a profitable corridor at all
5637
+ --tune <ideas.jsonl> Walk-forward parameter search: train on the head, one frozen shot on the tail
5009
5638
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
5010
5639
  --init Scaffold a new project in the current directory
5011
5640
  --docker Scaffold a Docker workspace for running strategies in a container
@@ -5140,10 +5769,13 @@ Simulator flags (--simulator):
5140
5769
  "author": string }. The file is validated BEFORE any work; a line that does not
5141
5770
  match the structure aborts the run with an error.
5142
5771
 
5143
- Runs the backtest-kit Simulator entity: one 5-day candle pass per idea, flood
5144
- dedupe (one idea per author per direction per 8h), default-ban author filter
5145
- (ban thresholds are grid axes), grid evaluation with time-based Sharpe/Sortino,
5146
- four ranking winners. The report includes the production author whitelist.
5772
+ A FEASIBILITY PROBE, not a parameter search (that is --tune): the profit-harvesting
5773
+ machinery is off (no profit lock, inert trailing, no weighted consensus), and a
5774
+ fast 48-point grid of hard stop x hold x ban rule answers one question — does the
5775
+ feed contain a profitable corridor at all. One 5-day candle pass per idea, flood
5776
+ dedupe (one idea per author per direction per 8h), default-ban author filter,
5777
+ time-based Sharpe/Sortino, four ranking winners. The report carries the corridor
5778
+ share and the whitelist of the sharpe winner's ban rule as evidence.
5147
5779
  Ideas of other symbols are filtered out — one shared feed serves any --symbol.
5148
5780
 
5149
5781
  No output flag → print the Markdown summary to stdout. With --verbose every
@@ -5153,6 +5785,31 @@ Simulator flags (--simulator):
5153
5785
  Module file ./modules/simulator.module is loaded automatically if it exists
5154
5786
  (register your exchange there); without it CCXT Binance is used by default.
5155
5787
 
5788
+ Tune flags (--tune):
5789
+
5790
+ --symbol <string> Trading pair to tune (default: BTCUSDT)
5791
+ --exchange <string> Exchange name (default: first registered)
5792
+ --split <string> Train share of the feed time range, 0..1 (default: 0.7)
5793
+ --output <string> Output file base name (default: tune_{SYMBOL}_{TIMESTAMP})
5794
+ --json Save { trainSplit, train, test } to ./dump/<output>.json
5795
+ --markdown Save walk-forward report to ./dump/<output>.md
5796
+ --verbose Log simulator lifecycle callbacks to the console
5797
+
5798
+ Positional: path to an ideas .jsonl file — same shape and validation as --simulator.
5799
+
5800
+ The PARAMETER SEARCH counterpart of the --simulator probe: the full grid with the
5801
+ profit-harvesting machinery on — profit lock, trailing take, weighted consensus,
5802
+ Wilson-bound ban, both author-hit metrics (close/reach). Honesty is structural:
5803
+ training sees ONLY the head of the feed (--split of its time range), then the
5804
+ sharpe winner is frozen — point and raw author track record — and fired exactly
5805
+ once on the tail via Simulator.test. Nothing is trained on the tail; authors
5806
+ unseen in training are banned by default. The report carries the train winners,
5807
+ the out-of-sample result with the trade list, and the frozen author track record
5808
+ to hardcode for production Simulator.test.
5809
+
5810
+ Module file ./modules/tune.module is loaded automatically if it exists
5811
+ (register your exchange there); without it CCXT Binance is used by default.
5812
+
5156
5813
  Flush flags (--flush):
5157
5814
 
5158
5815
  One or more positional entry points. For each entry point the following
@@ -5186,7 +5843,8 @@ Module hooks (loaded automatically by each mode):
5186
5843
  modules/dump.module --dump Exchange schema for candle dumps
5187
5844
  modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
5188
5845
  modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
5189
- modules/simulator.module --simulator Exchange schema for crowd-ideas simulation
5846
+ modules/simulator.module --simulator Exchange schema for the crowd-ideas feasibility probe
5847
+ modules/tune.module --tune Exchange schema for the walk-forward parameter search
5190
5848
 
5191
5849
  --flush has no associated module. It only removes dump subdirectories.
5192
5850
 
@@ -5216,7 +5874,9 @@ Examples:
5216
5874
  node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
5217
5875
  node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
5218
5876
  node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5219
- node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_sweep ./assets/tv-ideas.normalized.jsonl
5877
+ node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_probe ./assets/tv-ideas.normalized.jsonl
5878
+ node ${ENTRY_PATH} --tune --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5879
+ node ${ENTRY_PATH} --tune --symbol BTCUSDT --split 0.7 --markdown --output jun_2026_tune ./assets/tv-ideas.normalized.jsonl
5220
5880
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
5221
5881
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
5222
5882
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -5230,7 +5890,7 @@ const main$1 = async () => {
5230
5890
  if (!values.help) {
5231
5891
  return;
5232
5892
  }
5233
- process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n\n`);
5893
+ process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n\n`);
5234
5894
  process.stdout.write(HELP_TEXT);
5235
5895
  process.exit(0);
5236
5896
  };
@@ -5244,7 +5904,7 @@ const main = async () => {
5244
5904
  if (!values.version) {
5245
5905
  return;
5246
5906
  }
5247
- process.stdout.write(`@backtest-kit/cli ${"16.5.0"}\n`);
5907
+ process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
5248
5908
  process.exit(0);
5249
5909
  };
5250
5910
  main();