@backtest-kit/cli 16.6.0 → 17.7.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$l = () => {
236
+ const main$k = () => {
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$l();
243
+ main$k();
244
244
 
245
245
  const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
246
246
  function dumpStackTrace(error) {
@@ -891,18 +891,10 @@ const getArgs = functoolsKit.singleshot(() => {
891
891
  type: "boolean",
892
892
  default: false,
893
893
  },
894
- simulator: {
894
+ sweep: {
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
- },
906
898
  commit: {
907
899
  type: "string",
908
900
  default: "",
@@ -3687,14 +3679,14 @@ const cli = {
3687
3679
  };
3688
3680
  init();
3689
3681
 
3690
- const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "tune", "flush", "init", "docker", "help", "version"];
3682
+ const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "sweep", "flush", "init", "docker", "help", "version"];
3691
3683
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
3692
3684
  const HELP_TEXT$1 = `
3693
3685
  Example:
3694
3686
 
3695
3687
  node ${ENTRY_PATH$1} --help
3696
3688
  `.trimStart();
3697
- const main$k = async () => {
3689
+ const main$j = async () => {
3698
3690
  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)))) {
3699
3691
  return;
3700
3692
  }
@@ -3702,14 +3694,14 @@ const main$k = async () => {
3702
3694
  if (MODES.some((mode) => values[mode])) {
3703
3695
  return;
3704
3696
  }
3705
- process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
3697
+ process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n`);
3706
3698
  process.stdout.write("\n");
3707
3699
  process.stdout.write(`Run with --help to see available commands.\n`);
3708
3700
  process.stdout.write("\n");
3709
3701
  process.stdout.write(HELP_TEXT$1);
3710
3702
  process.exit(0);
3711
3703
  };
3712
- main$k();
3704
+ main$j();
3713
3705
 
3714
3706
  const notifyShutdown = functoolsKit.singleshot(async () => {
3715
3707
  console.log("Graceful shutdown initiated.");
@@ -3725,7 +3717,7 @@ const flush = async (entryPoint) => {
3725
3717
  console.log(`Removed: ${target}`);
3726
3718
  }
3727
3719
  };
3728
- const main$j = async () => {
3720
+ const main$i = async () => {
3729
3721
  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)))) {
3730
3722
  return;
3731
3723
  }
@@ -3742,7 +3734,7 @@ const main$j = async () => {
3742
3734
  }
3743
3735
  process.exit(0);
3744
3736
  };
3745
- main$j();
3737
+ main$i();
3746
3738
 
3747
3739
  const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
3748
3740
  process.off("SIGINT", BEFORE_EXIT_FN$6);
@@ -3765,7 +3757,7 @@ const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
3765
3757
  const listenGracefulShutdown$6 = functoolsKit.singleshot(() => {
3766
3758
  process.on("SIGINT", BEFORE_EXIT_FN$6);
3767
3759
  });
3768
- const main$i = async () => {
3760
+ const main$h = async () => {
3769
3761
  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)))) {
3770
3762
  return;
3771
3763
  }
@@ -3783,7 +3775,7 @@ const main$i = async () => {
3783
3775
  await cli.backtestMainService.connect();
3784
3776
  listenGracefulShutdown$6();
3785
3777
  };
3786
- main$i();
3778
+ main$h();
3787
3779
 
3788
3780
  const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3789
3781
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3802,7 +3794,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
3802
3794
  const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
3803
3795
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3804
3796
  });
3805
- const main$h = async () => {
3797
+ const main$g = async () => {
3806
3798
  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)))) {
3807
3799
  return;
3808
3800
  }
@@ -3821,7 +3813,7 @@ const main$h = async () => {
3821
3813
  listenGracefulShutdown$5();
3822
3814
  await cli.walkerMainService.connect();
3823
3815
  };
3824
- main$h();
3816
+ main$g();
3825
3817
 
3826
3818
  const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3827
3819
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3843,7 +3835,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
3843
3835
  const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
3844
3836
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3845
3837
  });
3846
- const main$g = async () => {
3838
+ const main$f = async () => {
3847
3839
  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)))) {
3848
3840
  return;
3849
3841
  }
@@ -3857,7 +3849,7 @@ const main$g = async () => {
3857
3849
  await cli.paperMainService.connect();
3858
3850
  listenGracefulShutdown$4();
3859
3851
  };
3860
- main$g();
3852
+ main$f();
3861
3853
 
3862
3854
  const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3863
3855
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3879,7 +3871,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
3879
3871
  const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
3880
3872
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3881
3873
  });
3882
- const main$f = async () => {
3874
+ const main$e = async () => {
3883
3875
  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)))) {
3884
3876
  return;
3885
3877
  }
@@ -3893,7 +3885,7 @@ const main$f = async () => {
3893
3885
  await cli.liveMainService.connect();
3894
3886
  listenGracefulShutdown$3();
3895
3887
  };
3896
- main$f();
3888
+ main$e();
3897
3889
 
3898
3890
  const stopBacktestList$1 = async () => {
3899
3891
  for (const item of await BacktestKit.Backtest.list()) {
@@ -3957,7 +3949,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
3957
3949
  const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
3958
3950
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3959
3951
  });
3960
- const main$e = async () => {
3952
+ const main$d = async () => {
3961
3953
  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)))) {
3962
3954
  return;
3963
3955
  }
@@ -4013,7 +4005,7 @@ const main$e = async () => {
4013
4005
  listenGracefulShutdown$2();
4014
4006
  await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
4015
4007
  };
4016
- main$e();
4008
+ main$d();
4017
4009
 
4018
4010
  const MODE_MODULE = {
4019
4011
  backtest: "backtest.module",
@@ -4116,7 +4108,7 @@ const createGracefulShutdown = (mode) => {
4116
4108
  process.on("SIGINT", handler);
4117
4109
  });
4118
4110
  };
4119
- const main$d = async () => {
4111
+ const main$c = async () => {
4120
4112
  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)))) {
4121
4113
  return;
4122
4114
  }
@@ -4192,7 +4184,7 @@ const main$d = async () => {
4192
4184
  }
4193
4185
  await getEntrySubject().next(absolutePath);
4194
4186
  };
4195
- main$d();
4187
+ main$c();
4196
4188
 
4197
4189
  const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
4198
4190
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -4203,7 +4195,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
4203
4195
  const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
4204
4196
  process.on("SIGINT", BEFORE_EXIT_FN$1);
4205
4197
  });
4206
- const main$c = async () => {
4198
+ const main$b = async () => {
4207
4199
  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)))) {
4208
4200
  return;
4209
4201
  }
@@ -4213,7 +4205,7 @@ const main$c = async () => {
4213
4205
  }
4214
4206
  listenGracefulShutdown$1();
4215
4207
  };
4216
- main$c();
4208
+ main$b();
4217
4209
 
4218
4210
  const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
4219
4211
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -4224,7 +4216,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
4224
4216
  const listenGracefulShutdown = functoolsKit.singleshot(() => {
4225
4217
  process.on("SIGINT", BEFORE_EXIT_FN);
4226
4218
  });
4227
- const main$b = async () => {
4219
+ const main$a = async () => {
4228
4220
  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)))) {
4229
4221
  return;
4230
4222
  }
@@ -4234,7 +4226,7 @@ const main$b = async () => {
4234
4226
  }
4235
4227
  listenGracefulShutdown();
4236
4228
  };
4237
- main$b();
4229
+ main$a();
4238
4230
 
4239
4231
  const EXTRACT_ROWS_FN = (plots, schema) => {
4240
4232
  const keys = Object.keys(schema);
@@ -4256,7 +4248,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
4256
4248
  }
4257
4249
  return rows;
4258
4250
  };
4259
- const main$a = async () => {
4251
+ const main$9 = async () => {
4260
4252
  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)))) {
4261
4253
  return;
4262
4254
  }
@@ -4356,9 +4348,9 @@ const main$a = async () => {
4356
4348
  console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
4357
4349
  process.exit(0);
4358
4350
  };
4359
- main$a();
4351
+ main$9();
4360
4352
 
4361
- const main$9 = async () => {
4353
+ const main$8 = async () => {
4362
4354
  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)))) {
4363
4355
  return;
4364
4356
  }
@@ -4421,9 +4413,9 @@ const main$9 = async () => {
4421
4413
  };
4422
4414
  process.on("SIGINT", beforeExit);
4423
4415
  };
4424
- main$9();
4416
+ main$8();
4425
4417
 
4426
- const main$8 = async () => {
4418
+ const main$7 = async () => {
4427
4419
  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)))) {
4428
4420
  return;
4429
4421
  }
@@ -4504,9 +4496,9 @@ const main$8 = async () => {
4504
4496
  console.log(JSON.stringify(candles, null, 2));
4505
4497
  process.exit(0);
4506
4498
  };
4507
- main$8();
4499
+ main$7();
4508
4500
 
4509
- const main$7 = async () => {
4501
+ const main$6 = async () => {
4510
4502
  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)))) {
4511
4503
  return;
4512
4504
  }
@@ -4620,7 +4612,7 @@ const main$7 = async () => {
4620
4612
  }
4621
4613
  process.exit(0);
4622
4614
  };
4623
- main$7();
4615
+ main$6();
4624
4616
 
4625
4617
  const getBroker = functoolsKit.singleshot(() => {
4626
4618
  // Reaches into BrokerAdapter internals on purpose: the public commit*
@@ -4729,7 +4721,7 @@ const ZERO_PNL = {
4729
4721
  pnlCost: 0,
4730
4722
  pnlEntries: 0,
4731
4723
  };
4732
- const main$6 = async () => {
4724
+ const main$5 = async () => {
4733
4725
  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
4726
  return;
4735
4727
  }
@@ -4923,11 +4915,23 @@ const main$6 = async () => {
4923
4915
  }
4924
4916
  process.exit(0);
4925
4917
  };
4926
- main$6();
4918
+ main$5();
4927
4919
 
4928
- const SIMULATOR_NAME$1 = "cli_simulator";
4929
- const IDEA_DIRECTIONS$1 = ["LONG", "SHORT", "NEUTRAL"];
4930
- const validateIdea$1 = (idea, line) => {
4920
+ const SIMULATOR_NAME = "cli_sweep";
4921
+ const CONFIG_KEYS = ["gridAxes", "reportOrder"];
4922
+ const validateConfig = (config) => {
4923
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
4924
+ return "config must be a JSON object";
4925
+ }
4926
+ for (const key of Object.keys(config)) {
4927
+ if (!CONFIG_KEYS.includes(key)) {
4928
+ return `unknown key "${key}" (expected: ${CONFIG_KEYS.join(", ")})`;
4929
+ }
4930
+ }
4931
+ return null;
4932
+ };
4933
+ const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
4934
+ const validateIdea = (idea, line) => {
4931
4935
  if (typeof idea !== "object" || idea === null) {
4932
4936
  return `line ${line}: not an object`;
4933
4937
  }
@@ -4940,59 +4944,66 @@ const validateIdea$1 = (idea, line) => {
4940
4944
  if (typeof idea.symbol !== "string" || !idea.symbol) {
4941
4945
  return `line ${line}: "symbol" must be a non-empty string`;
4942
4946
  }
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)}`;
4947
+ if (!IDEA_DIRECTIONS.includes(idea.direction)) {
4948
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
4945
4949
  }
4946
4950
  if (typeof idea.author !== "string" || !idea.author) {
4947
4951
  return `line ${line}: "author" must be a non-empty string`;
4948
4952
  }
4949
4953
  return null;
4950
4954
  };
4951
- const toMarkdown$1 = (result) => {
4952
- const profitable = result.reports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
4955
+ const fmtSimRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
4956
+ const toMarkdown = (result) => {
4957
+ const bucket = result.reports;
4958
+ const allReports = bucket.reports;
4959
+ const profitable = allReports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
4953
4960
  const lines = [];
4954
- lines.push(`# Simulator Report — ${result.symbol}`);
4961
+ lines.push(`# Sweep Report — ${result.symbol}`);
4955
4962
  lines.push("");
4956
4963
  lines.push(`| Metric | Value |`);
4957
4964
  lines.push(`| --- | --- |`);
4958
4965
  lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
4959
4966
  lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
4960
- lines.push(`| Authors allowed / banned | ${result.allowedAuthors.length} / ${result.bannedAuthors.length} |`);
4961
- lines.push(`| Profitable corridor | ${profitable} / ${result.reports.length} grid points |`);
4967
+ lines.push(`| Profitable corridor | ${profitable} / ${allReports.length} grid points |`);
4962
4968
  lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
4969
+ // единственная метрика — profit-before-stop: одна корзина с
4970
+ // победителями и author tracks
4963
4971
  lines.push("");
4964
- lines.push(`## Ranking winners`);
4972
+ lines.push(`## Grid winners`);
4965
4973
  lines.push("");
4966
- lines.push(`| Criterion | Stop% | Hold | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4974
+ lines.push(`| Criterion | Stop% | Trail% | Hold | Lock% | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4967
4975
  lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4968
- for (const best of result.best) {
4976
+ for (const best of bucket.best) {
4969
4977
  if (!best.report) {
4970
4978
  lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
4971
4979
  continue;
4972
4980
  }
4973
4981
  const { point } = best.report;
4974
- lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.holdMinutes / 60}h | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
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)} |`);
4976
- }
4982
+ lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.trailingTakePercent} | ${point.holdMinutes / 60}h | ${point.profitLockPercent} | ` +
4983
+ `${best.report.tradesList.length} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${fmtSimRatio(best.report.sharpe)} | ${fmtSimRatio(best.report.sortino)} |`);
4984
+ }
4985
+ // сырой author track: userspace решает, кому верить (порога/бана
4986
+ // нет). Топ по hitRate
4987
+ const tracks = [...bucket.tracks]
4988
+ .filter(({ ideas }) => ideas > 0)
4989
+ .sort((a, b) => b.hitRate - a.hitRate || b.ideas - a.ideas)
4990
+ .slice(0, 20);
4977
4991
  lines.push("");
4978
- lines.push(`## Allowed authors (whitelist of the sharpe winner rule)`);
4992
+ lines.push(`### Author tracks — raw ideas/hits/hitRate per rule (top ${tracks.length} by hitRate; full set in --json)`);
4979
4993
  lines.push("");
4980
- lines.push(`| Author | Ideas | Hits | HitRate |`);
4981
- lines.push(`| --- | --- | --- | --- |`);
4982
- // артефакт авторов живёт по-победительно в best[] — ран-левел
4983
- // authorStats в ISimulatorResult больше нет
4984
- const sharpeBest = result.best.find(({ criterion }) => criterion === "sharpe");
4985
- for (const stat of (sharpeBest?.authorStats ?? []).filter(({ banned }) => !banned)) {
4986
- lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
4994
+ lines.push(`| Author | Hold | Lock% | Stop% | Trail% | Ideas | Hits | HitRate |`);
4995
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- |`);
4996
+ for (const t of tracks) {
4997
+ lines.push(`| ${t.author} | ${t.holdMinutes / 60}h | ${t.profitLockPercent} | ${t.hardStopPercent} | ${t.trailingTakePercent} | ${t.ideas} | ${t.hits} | ${(t.hitRate * 100).toFixed(0)}% |`);
4987
4998
  }
4988
4999
  return lines.join("\n");
4989
5000
  };
4990
- const main$5 = async () => {
5001
+ const main$4 = async () => {
4991
5002
  if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
4992
5003
  return;
4993
5004
  }
4994
5005
  const { values, positionals } = getArgs();
4995
- if (!values.simulator) {
5006
+ if (!values.sweep) {
4996
5007
  return;
4997
5008
  }
4998
5009
  const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
@@ -5000,6 +5011,33 @@ const main$5 = async () => {
5000
5011
  console.error("Error: positional path to an ideas .jsonl file is required");
5001
5012
  process.exit(1);
5002
5013
  }
5014
+ // конфиг — позиционный JSON; без него подтягивается пустой объект
5015
+ // и схема живёт на дефолтах движка (оси, порядок отчёта)
5016
+ const [configPath = null] = positionals.filter((value) => value.endsWith(".json") && !value.endsWith(".jsonl"));
5017
+ let config = {};
5018
+ if (configPath) {
5019
+ let content;
5020
+ try {
5021
+ content = await fs$1.readFile(path.resolve(configPath), "utf-8");
5022
+ }
5023
+ catch (error) {
5024
+ console.error(`Error: cannot read config file: ${configPath}`);
5025
+ process.exit(1);
5026
+ }
5027
+ try {
5028
+ config = JSON.parse(content);
5029
+ }
5030
+ catch {
5031
+ console.error(`Error: invalid JSON in config file: ${configPath}`);
5032
+ process.exit(1);
5033
+ }
5034
+ const problem = validateConfig(config);
5035
+ if (problem) {
5036
+ console.error(`Error: sweep config does not match the structure — ${problem}`);
5037
+ console.error(`Expected shape: { "gridAxes"?: ISweepGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }`);
5038
+ process.exit(1);
5039
+ }
5040
+ }
5003
5041
  let ideas = [];
5004
5042
  {
5005
5043
  let content;
@@ -5024,7 +5062,7 @@ const main$5 = async () => {
5024
5062
  console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
5025
5063
  process.exit(1);
5026
5064
  }
5027
- const problem = validateIdea$1(idea, i + 1);
5065
+ const problem = validateIdea(idea, i + 1);
5028
5066
  if (problem) {
5029
5067
  console.error(`Error: ideas file does not match the idea structure — ${problem}`);
5030
5068
  console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
@@ -5053,7 +5091,7 @@ const main$5 = async () => {
5053
5091
  process.exit(-1);
5054
5092
  }
5055
5093
  }
5056
- await cli.moduleConnectionService.loadModule("simulator.module");
5094
+ await cli.moduleConnectionService.loadModule("sweep.module");
5057
5095
  {
5058
5096
  await cli.exchangeSchemaService.addSchema();
5059
5097
  await cli.symbolSchemaService.addSchema();
@@ -5071,31 +5109,13 @@ const main$5 = async () => {
5071
5109
  });
5072
5110
  }
5073
5111
  const symbol = values.symbol || "BTCUSDT";
5074
- // Проба осуществимости, НЕ поиск заработка (подбор параметров — у
5075
- // --tune): собирающая прибыль механика выключена, остаётся вопрос
5076
- // "есть ли прибыльный коридор" по стопу x холду x правилу бана —
5077
- // 48 точек, быстрый вердикт
5078
- BacktestKit.addSimulatorSchema({
5079
- simulatorName: SIMULATOR_NAME$1,
5112
+ // оси и порядок отчёта приходят позиционным конфигом потребителя;
5113
+ // пустой конфиг — дефолтная сетка движка из connection-сервиса
5114
+ BacktestKit.addSweepSchema({
5115
+ sweepName: SIMULATOR_NAME,
5080
5116
  exchangeName,
5081
- gridAxes: {
5082
- // грубая шкала катастрофы: коридор должен быть широким, не точкой
5083
- hardStopPercent: [2, 3, 5, 7],
5084
- // инертен: проба не собирает прибыль, выход — по времени или стопу
5085
- trailingTakePercent: [100],
5086
- holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
5087
- // одного проверенного автора достаточно — консенсус не перебираем
5088
- minIdeasAligned: [1],
5089
- // правило бана — единственная перебираемая "умность" пробы
5090
- minAuthorTrack: [3, 5],
5091
- minAuthorHitRate: [0.5, 0.6],
5092
- minAuthorWilson: [0],
5093
- minWeightAligned: [0],
5094
- profitLockPercent: [0],
5095
- authorMetric: ["close"],
5096
- banCriteria: ["sharpe", "pnl"],
5097
- },
5098
- reportOrder: "sharpe",
5117
+ ...(config.gridAxes ? { gridAxes: config.gridAxes } : {}),
5118
+ ...(config.reportOrder ? { reportOrder: config.reportOrder } : {}),
5099
5119
  callbacks: {
5100
5120
  onProgress: (symbol, stage, processed, total) => {
5101
5121
  if (values.verbose) {
@@ -5112,22 +5132,20 @@ const main$5 = async () => {
5112
5132
  console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
5113
5133
  }
5114
5134
  },
5115
- onAuthorsTrained: (symbol, stats, bannedIdeas) => {
5135
+ onAuthorsTrained: (symbol, tracks) => {
5116
5136
  if (values.verbose) {
5117
5137
  console.log("onAuthorsTrained", {
5118
5138
  symbol,
5119
- authors: stats.length,
5120
- banned: stats.filter(({ banned }) => banned).length,
5121
- bannedIdeas,
5139
+ authors: tracks.length,
5122
5140
  });
5123
5141
  }
5124
5142
  },
5125
- onGridPoint: (symbol, report) => {
5143
+ onGridPoint: (symbol, report, trades) => {
5126
5144
  if (values.verbose) {
5127
5145
  console.log("onGridPoint", {
5128
5146
  symbol,
5129
5147
  point: report.point,
5130
- trades: report.trades,
5148
+ trades: trades.length,
5131
5149
  pnl: +report.totalPnlPercent.toFixed(2),
5132
5150
  sharpe: +report.sharpe.toFixed(2),
5133
5151
  });
@@ -5147,20 +5165,19 @@ const main$5 = async () => {
5147
5165
  if (values.verbose) {
5148
5166
  console.log("onDone", {
5149
5167
  symbol,
5150
- reports: result.reports.length,
5151
- allowedAuthors: result.allowedAuthors.length,
5152
- bannedAuthors: result.bannedAuthors.length,
5168
+ reports: result.reports.reports.length,
5169
+ tracks: result.reports.tracks.length,
5153
5170
  });
5154
5171
  }
5155
5172
  },
5156
5173
  },
5157
5174
  });
5158
- const result = await BacktestKit.Simulator.run({
5175
+ const result = await BacktestKit.Sweep.run({
5159
5176
  symbol,
5160
- simulatorName: SIMULATOR_NAME$1,
5177
+ sweepName: SIMULATOR_NAME,
5161
5178
  ideas,
5162
5179
  });
5163
- const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
5180
+ const dumpName = values.output || `sweep_${symbol}_${Date.now()}`;
5164
5181
  const dumpDir = path.join(process.cwd(), "dump");
5165
5182
  if (values.json) {
5166
5183
  const filePath = path.resolve(dumpDir, `${dumpName}.json`);
@@ -5172,299 +5189,11 @@ const main$5 = async () => {
5172
5189
  if (values.markdown) {
5173
5190
  const filePath = path.resolve(dumpDir, `${dumpName}.md`);
5174
5191
  await fs$1.mkdir(dumpDir, { recursive: true });
5175
- await fs$1.writeFile(filePath, toMarkdown$1(result), "utf-8");
5192
+ await fs$1.writeFile(filePath, toMarkdown(result), "utf-8");
5176
5193
  console.log(`Saved: ${filePath}`);
5177
5194
  process.exit(0);
5178
5195
  }
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");
5464
- console.log(`Saved: ${filePath}`);
5465
- process.exit(0);
5466
- }
5467
- console.log(toMarkdown(train, test, trainSplit));
5196
+ console.log(toMarkdown(result));
5468
5197
  process.exit(0);
5469
5198
  };
5470
5199
  main$4();
@@ -5665,8 +5394,7 @@ Modes:
5665
5394
  --dump Fetch and save raw OHLCV candles
5666
5395
  --pnldebug Simulate PnL per minute for a given entry price and direction
5667
5396
  --brokerdebug Fire a single broker commit against the live broker adapter
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
5397
+ --sweep <ideas.jsonl> [config.json] Grid sweep over crowd ideas: profit-before-stop corridor + raw author tracks
5670
5398
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
5671
5399
  --init Scaffold a new project in the current directory
5672
5400
  --docker Scaffold a Docker workspace for running strategies in a container
@@ -5787,59 +5515,36 @@ Broker debug flags (--brokerdebug):
5787
5515
  Loads ./live.module, fetches the last candle for --symbol/--timeframe, and calls
5788
5516
  the selected broker commit with synthetic payload values derived from current price.
5789
5517
 
5790
- Simulator flags (--simulator):
5518
+ Sweep flags (--sweep):
5791
5519
 
5792
5520
  --symbol <string> Trading pair to simulate (default: BTCUSDT)
5793
5521
  --exchange <string> Exchange name (default: first registered)
5794
- --output <string> Output file base name (default: simulator_{SYMBOL}_{TIMESTAMP})
5795
- --json Save full ISimulatorResult to ./dump/<output>.json
5522
+ --output <string> Output file base name (default: sweep_{SYMBOL}_{TIMESTAMP})
5523
+ --json Save full ISweepResult to ./dump/<output>.json
5796
5524
  --markdown Save summary report to ./dump/<output>.md
5797
- --verbose Log every simulator lifecycle callback to the console
5525
+ --verbose Log every sweep lifecycle callback to the console
5798
5526
 
5799
- Positional: path to an ideas .jsonl file — one idea per line with the exact shape
5527
+ Positionals: path to an ideas .jsonl file — one idea per line with the exact shape
5800
5528
  { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL",
5801
- "author": string }. The file is validated BEFORE any work; a line that does not
5802
- match the structure aborts the run with an error.
5803
-
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.
5811
- Ideas of other symbols are filtered out — one shared feed serves any --symbol.
5529
+ "author": string } — and an OPTIONAL config .json with the shape
5530
+ { "gridAxes"?: ISweepGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }.
5531
+ Both files are validated BEFORE any work; a mismatch aborts the run with an error.
5532
+ No config -> an empty object is used and the engine defaults apply (the full
5533
+ default grid axes and reportOrder "sharpe" of the connection service).
5534
+
5535
+ A GRID SWEEP: the grid from the config (or the engine default) is evaluated
5536
+ per point. One candle pass per idea to the grid's longest hold, flood dedupe
5537
+ (one idea per author per direction per 8h), EVERY author traded (no ban — the
5538
+ engine reports the raw per-author track graded inside each point's own hold
5539
+ window; who to trust is userspace), time-based Sharpe/Sortino, per-metric
5540
+ buckets with their own ranking winners and author tracks. Ideas of other
5541
+ symbols are filtered out — one shared feed serves any --symbol.
5812
5542
 
5813
5543
  No output flag → print the Markdown summary to stdout. With --verbose every
5814
- simulator lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
5544
+ sweep lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
5815
5545
  onGridPoint, onRanking, onDone) is logged to the console as it fires.
5816
5546
 
5817
- Module file ./modules/simulator.module is loaded automatically if it exists
5818
- (register your exchange there); without it CCXT Binance is used by default.
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
5547
+ Module file ./modules/sweep.module is loaded automatically if it exists
5843
5548
  (register your exchange there); without it CCXT Binance is used by default.
5844
5549
 
5845
5550
  Flush flags (--flush):
@@ -5875,8 +5580,7 @@ Module hooks (loaded automatically by each mode):
5875
5580
  modules/dump.module --dump Exchange schema for candle dumps
5876
5581
  modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
5877
5582
  modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
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
5583
+ modules/sweep.module --sweep Exchange schema for the crowd-ideas feasibility probe
5880
5584
 
5881
5585
  --flush has no associated module. It only removes dump subdirectories.
5882
5586
 
@@ -5905,10 +5609,8 @@ Examples:
5905
5609
  node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
5906
5610
  node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
5907
5611
  node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
5908
- node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./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
5612
+ node ${ENTRY_PATH} --sweep --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5613
+ node ${ENTRY_PATH} --sweep --symbol BTCUSDT --json --output jun_2026_probe ./assets/tv-ideas.normalized.jsonl ./assets/probe.config.json
5912
5614
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
5913
5615
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
5914
5616
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -5922,7 +5624,7 @@ const main$1 = async () => {
5922
5624
  if (!values.help) {
5923
5625
  return;
5924
5626
  }
5925
- process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n\n`);
5627
+ process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n\n`);
5926
5628
  process.stdout.write(HELP_TEXT);
5927
5629
  process.exit(0);
5928
5630
  };
@@ -5936,7 +5638,7 @@ const main = async () => {
5936
5638
  if (!values.version) {
5937
5639
  return;
5938
5640
  }
5939
- process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
5641
+ process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n`);
5940
5642
  process.exit(0);
5941
5643
  };
5942
5644
  main();