@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.mjs CHANGED
@@ -1,6 +1,6 @@
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, runInMockContext, addSimulatorSchema, Simulator } 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, runInMockContext, addSweepSchema, Sweep } from 'backtest-kit';
4
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';
@@ -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$2 } from '@backtest-kit/pinets';
31
+ import { run as run$1, Code, toMarkdown as toMarkdown$1 } from '@backtest-kit/pinets';
32
32
  import * as BacktestKitSignals from '@backtest-kit/signals';
33
33
  import open from 'open';
34
34
 
@@ -208,14 +208,14 @@ const getReadySubject = singleshot(() => {
208
208
  return new Subject();
209
209
  });
210
210
 
211
- const main$l = () => {
211
+ const main$k = () => {
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$l();
218
+ main$k();
219
219
 
220
220
  const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
221
221
  function dumpStackTrace(error) {
@@ -866,18 +866,10 @@ const getArgs = singleshot(() => {
866
866
  type: "boolean",
867
867
  default: false,
868
868
  },
869
- simulator: {
869
+ sweep: {
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
- },
881
873
  commit: {
882
874
  type: "string",
883
875
  default: "",
@@ -3655,14 +3647,14 @@ const cli = {
3655
3647
  };
3656
3648
  init();
3657
3649
 
3658
- const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "tune", "flush", "init", "docker", "help", "version"];
3650
+ const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "sweep", "flush", "init", "docker", "help", "version"];
3659
3651
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
3660
3652
  const HELP_TEXT$1 = `
3661
3653
  Example:
3662
3654
 
3663
3655
  node ${ENTRY_PATH$1} --help
3664
3656
  `.trimStart();
3665
- const main$k = async () => {
3657
+ const main$j = async () => {
3666
3658
  if (!getEntry(import.meta.url)) {
3667
3659
  return;
3668
3660
  }
@@ -3670,14 +3662,14 @@ const main$k = async () => {
3670
3662
  if (MODES.some((mode) => values[mode])) {
3671
3663
  return;
3672
3664
  }
3673
- process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
3665
+ process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n`);
3674
3666
  process.stdout.write("\n");
3675
3667
  process.stdout.write(`Run with --help to see available commands.\n`);
3676
3668
  process.stdout.write("\n");
3677
3669
  process.stdout.write(HELP_TEXT$1);
3678
3670
  process.exit(0);
3679
3671
  };
3680
- main$k();
3672
+ main$j();
3681
3673
 
3682
3674
  const notifyShutdown = singleshot(async () => {
3683
3675
  console.log("Graceful shutdown initiated.");
@@ -3693,7 +3685,7 @@ const flush = async (entryPoint) => {
3693
3685
  console.log(`Removed: ${target}`);
3694
3686
  }
3695
3687
  };
3696
- const main$j = async () => {
3688
+ const main$i = async () => {
3697
3689
  if (!getEntry(import.meta.url)) {
3698
3690
  return;
3699
3691
  }
@@ -3710,7 +3702,7 @@ const main$j = async () => {
3710
3702
  }
3711
3703
  process.exit(0);
3712
3704
  };
3713
- main$j();
3705
+ main$i();
3714
3706
 
3715
3707
  const BEFORE_EXIT_FN$6 = singleshot(async () => {
3716
3708
  process.off("SIGINT", BEFORE_EXIT_FN$6);
@@ -3733,7 +3725,7 @@ const BEFORE_EXIT_FN$6 = singleshot(async () => {
3733
3725
  const listenGracefulShutdown$6 = singleshot(() => {
3734
3726
  process.on("SIGINT", BEFORE_EXIT_FN$6);
3735
3727
  });
3736
- const main$i = async () => {
3728
+ const main$h = async () => {
3737
3729
  if (!getEntry(import.meta.url)) {
3738
3730
  return;
3739
3731
  }
@@ -3751,7 +3743,7 @@ const main$i = async () => {
3751
3743
  await cli.backtestMainService.connect();
3752
3744
  listenGracefulShutdown$6();
3753
3745
  };
3754
- main$i();
3746
+ main$h();
3755
3747
 
3756
3748
  const BEFORE_EXIT_FN$5 = singleshot(async () => {
3757
3749
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3770,7 +3762,7 @@ const BEFORE_EXIT_FN$5 = singleshot(async () => {
3770
3762
  const listenGracefulShutdown$5 = singleshot(() => {
3771
3763
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3772
3764
  });
3773
- const main$h = async () => {
3765
+ const main$g = async () => {
3774
3766
  if (!getEntry(import.meta.url)) {
3775
3767
  return;
3776
3768
  }
@@ -3789,7 +3781,7 @@ const main$h = async () => {
3789
3781
  listenGracefulShutdown$5();
3790
3782
  await cli.walkerMainService.connect();
3791
3783
  };
3792
- main$h();
3784
+ main$g();
3793
3785
 
3794
3786
  const BEFORE_EXIT_FN$4 = singleshot(async () => {
3795
3787
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3811,7 +3803,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
3811
3803
  const listenGracefulShutdown$4 = singleshot(() => {
3812
3804
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3813
3805
  });
3814
- const main$g = async () => {
3806
+ const main$f = async () => {
3815
3807
  if (!getEntry(import.meta.url)) {
3816
3808
  return;
3817
3809
  }
@@ -3825,7 +3817,7 @@ const main$g = async () => {
3825
3817
  await cli.paperMainService.connect();
3826
3818
  listenGracefulShutdown$4();
3827
3819
  };
3828
- main$g();
3820
+ main$f();
3829
3821
 
3830
3822
  const BEFORE_EXIT_FN$3 = singleshot(async () => {
3831
3823
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3847,7 +3839,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
3847
3839
  const listenGracefulShutdown$3 = singleshot(() => {
3848
3840
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3849
3841
  });
3850
- const main$f = async () => {
3842
+ const main$e = async () => {
3851
3843
  if (!getEntry(import.meta.url)) {
3852
3844
  return;
3853
3845
  }
@@ -3861,7 +3853,7 @@ const main$f = async () => {
3861
3853
  await cli.liveMainService.connect();
3862
3854
  listenGracefulShutdown$3();
3863
3855
  };
3864
- main$f();
3856
+ main$e();
3865
3857
 
3866
3858
  const stopBacktestList$1 = async () => {
3867
3859
  for (const item of await Backtest.list()) {
@@ -3925,7 +3917,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
3925
3917
  const listenGracefulShutdown$2 = singleshot(() => {
3926
3918
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3927
3919
  });
3928
- const main$e = async () => {
3920
+ const main$d = async () => {
3929
3921
  if (!getEntry(import.meta.url)) {
3930
3922
  return;
3931
3923
  }
@@ -3981,7 +3973,7 @@ const main$e = async () => {
3981
3973
  listenGracefulShutdown$2();
3982
3974
  await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
3983
3975
  };
3984
- main$e();
3976
+ main$d();
3985
3977
 
3986
3978
  const MODE_MODULE = {
3987
3979
  backtest: "backtest.module",
@@ -4084,7 +4076,7 @@ const createGracefulShutdown = (mode) => {
4084
4076
  process.on("SIGINT", handler);
4085
4077
  });
4086
4078
  };
4087
- const main$d = async () => {
4079
+ const main$c = async () => {
4088
4080
  if (!getEntry(import.meta.url)) {
4089
4081
  return;
4090
4082
  }
@@ -4160,7 +4152,7 @@ const main$d = async () => {
4160
4152
  }
4161
4153
  await getEntrySubject().next(absolutePath);
4162
4154
  };
4163
- main$d();
4155
+ main$c();
4164
4156
 
4165
4157
  const BEFORE_EXIT_FN$1 = singleshot(async () => {
4166
4158
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -4171,7 +4163,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
4171
4163
  const listenGracefulShutdown$1 = singleshot(() => {
4172
4164
  process.on("SIGINT", BEFORE_EXIT_FN$1);
4173
4165
  });
4174
- const main$c = async () => {
4166
+ const main$b = async () => {
4175
4167
  if (!getEntry(import.meta.url)) {
4176
4168
  return;
4177
4169
  }
@@ -4181,7 +4173,7 @@ const main$c = async () => {
4181
4173
  }
4182
4174
  listenGracefulShutdown$1();
4183
4175
  };
4184
- main$c();
4176
+ main$b();
4185
4177
 
4186
4178
  const BEFORE_EXIT_FN = singleshot(async () => {
4187
4179
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -4192,7 +4184,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
4192
4184
  const listenGracefulShutdown = singleshot(() => {
4193
4185
  process.on("SIGINT", BEFORE_EXIT_FN);
4194
4186
  });
4195
- const main$b = async () => {
4187
+ const main$a = async () => {
4196
4188
  if (!getEntry(import.meta.url)) {
4197
4189
  return;
4198
4190
  }
@@ -4202,7 +4194,7 @@ const main$b = async () => {
4202
4194
  }
4203
4195
  listenGracefulShutdown();
4204
4196
  };
4205
- main$b();
4197
+ main$a();
4206
4198
 
4207
4199
  const EXTRACT_ROWS_FN = (plots, schema) => {
4208
4200
  const keys = Object.keys(schema);
@@ -4224,7 +4216,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
4224
4216
  }
4225
4217
  return rows;
4226
4218
  };
4227
- const main$a = async () => {
4219
+ const main$9 = async () => {
4228
4220
  if (!getEntry(import.meta.url)) {
4229
4221
  return;
4230
4222
  }
@@ -4316,17 +4308,17 @@ const main$a = async () => {
4316
4308
  if (values.markdown) {
4317
4309
  const filePath = resolve(dumpDir, `${dumpName}.md`);
4318
4310
  await mkdir(dumpDir, { recursive: true });
4319
- await writeFile(filePath, await toMarkdown$2(signalId, plots, signalSchema), "utf-8");
4311
+ await writeFile(filePath, await toMarkdown$1(signalId, plots, signalSchema), "utf-8");
4320
4312
  console.log(`Saved: ${filePath}`);
4321
4313
  process.exit(0);
4322
4314
  return;
4323
4315
  }
4324
- console.log(await toMarkdown$2(signalId, plots, signalSchema));
4316
+ console.log(await toMarkdown$1(signalId, plots, signalSchema));
4325
4317
  process.exit(0);
4326
4318
  };
4327
- main$a();
4319
+ main$9();
4328
4320
 
4329
- const main$9 = async () => {
4321
+ const main$8 = async () => {
4330
4322
  if (!getEntry(import.meta.url)) {
4331
4323
  return;
4332
4324
  }
@@ -4389,9 +4381,9 @@ const main$9 = async () => {
4389
4381
  };
4390
4382
  process.on("SIGINT", beforeExit);
4391
4383
  };
4392
- main$9();
4384
+ main$8();
4393
4385
 
4394
- const main$8 = async () => {
4386
+ const main$7 = async () => {
4395
4387
  if (!getEntry(import.meta.url)) {
4396
4388
  return;
4397
4389
  }
@@ -4472,9 +4464,9 @@ const main$8 = async () => {
4472
4464
  console.log(JSON.stringify(candles, null, 2));
4473
4465
  process.exit(0);
4474
4466
  };
4475
- main$8();
4467
+ main$7();
4476
4468
 
4477
- const main$7 = async () => {
4469
+ const main$6 = async () => {
4478
4470
  if (!getEntry(import.meta.url)) {
4479
4471
  return;
4480
4472
  }
@@ -4588,7 +4580,7 @@ const main$7 = async () => {
4588
4580
  }
4589
4581
  process.exit(0);
4590
4582
  };
4591
- main$7();
4583
+ main$6();
4592
4584
 
4593
4585
  const getBroker = singleshot(() => {
4594
4586
  // Reaches into BrokerAdapter internals on purpose: the public commit*
@@ -4697,7 +4689,7 @@ const ZERO_PNL = {
4697
4689
  pnlCost: 0,
4698
4690
  pnlEntries: 0,
4699
4691
  };
4700
- const main$6 = async () => {
4692
+ const main$5 = async () => {
4701
4693
  if (!getEntry(import.meta.url)) {
4702
4694
  return;
4703
4695
  }
@@ -4891,11 +4883,23 @@ const main$6 = async () => {
4891
4883
  }
4892
4884
  process.exit(0);
4893
4885
  };
4894
- main$6();
4886
+ main$5();
4895
4887
 
4896
- const SIMULATOR_NAME$1 = "cli_simulator";
4897
- const IDEA_DIRECTIONS$1 = ["LONG", "SHORT", "NEUTRAL"];
4898
- const validateIdea$1 = (idea, line) => {
4888
+ const SIMULATOR_NAME = "cli_sweep";
4889
+ const CONFIG_KEYS = ["gridAxes", "reportOrder"];
4890
+ const validateConfig = (config) => {
4891
+ if (typeof config !== "object" || config === null || Array.isArray(config)) {
4892
+ return "config must be a JSON object";
4893
+ }
4894
+ for (const key of Object.keys(config)) {
4895
+ if (!CONFIG_KEYS.includes(key)) {
4896
+ return `unknown key "${key}" (expected: ${CONFIG_KEYS.join(", ")})`;
4897
+ }
4898
+ }
4899
+ return null;
4900
+ };
4901
+ const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
4902
+ const validateIdea = (idea, line) => {
4899
4903
  if (typeof idea !== "object" || idea === null) {
4900
4904
  return `line ${line}: not an object`;
4901
4905
  }
@@ -4908,59 +4912,66 @@ const validateIdea$1 = (idea, line) => {
4908
4912
  if (typeof idea.symbol !== "string" || !idea.symbol) {
4909
4913
  return `line ${line}: "symbol" must be a non-empty string`;
4910
4914
  }
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)}`;
4915
+ if (!IDEA_DIRECTIONS.includes(idea.direction)) {
4916
+ return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
4913
4917
  }
4914
4918
  if (typeof idea.author !== "string" || !idea.author) {
4915
4919
  return `line ${line}: "author" must be a non-empty string`;
4916
4920
  }
4917
4921
  return null;
4918
4922
  };
4919
- const toMarkdown$1 = (result) => {
4920
- const profitable = result.reports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
4923
+ const fmtSimRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
4924
+ const toMarkdown = (result) => {
4925
+ const bucket = result.reports;
4926
+ const allReports = bucket.reports;
4927
+ const profitable = allReports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
4921
4928
  const lines = [];
4922
- lines.push(`# Simulator Report — ${result.symbol}`);
4929
+ lines.push(`# Sweep Report — ${result.symbol}`);
4923
4930
  lines.push("");
4924
4931
  lines.push(`| Metric | Value |`);
4925
4932
  lines.push(`| --- | --- |`);
4926
4933
  lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
4927
4934
  lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
4928
- lines.push(`| Authors allowed / banned | ${result.allowedAuthors.length} / ${result.bannedAuthors.length} |`);
4929
- lines.push(`| Profitable corridor | ${profitable} / ${result.reports.length} grid points |`);
4935
+ lines.push(`| Profitable corridor | ${profitable} / ${allReports.length} grid points |`);
4930
4936
  lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
4937
+ // единственная метрика — profit-before-stop: одна корзина с
4938
+ // победителями и author tracks
4931
4939
  lines.push("");
4932
- lines.push(`## Ranking winners`);
4940
+ lines.push(`## Grid winners`);
4933
4941
  lines.push("");
4934
- lines.push(`| Criterion | Stop% | Hold | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4942
+ lines.push(`| Criterion | Stop% | Trail% | Hold | Lock% | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4935
4943
  lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4936
- for (const best of result.best) {
4944
+ for (const best of bucket.best) {
4937
4945
  if (!best.report) {
4938
4946
  lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
4939
4947
  continue;
4940
4948
  }
4941
4949
  const { point } = best.report;
4942
- lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.holdMinutes / 60}h | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
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)} |`);
4944
- }
4950
+ lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.trailingTakePercent} | ${point.holdMinutes / 60}h | ${point.profitLockPercent} | ` +
4951
+ `${best.report.tradesList.length} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${fmtSimRatio(best.report.sharpe)} | ${fmtSimRatio(best.report.sortino)} |`);
4952
+ }
4953
+ // сырой author track: userspace решает, кому верить (порога/бана
4954
+ // нет). Топ по hitRate
4955
+ const tracks = [...bucket.tracks]
4956
+ .filter(({ ideas }) => ideas > 0)
4957
+ .sort((a, b) => b.hitRate - a.hitRate || b.ideas - a.ideas)
4958
+ .slice(0, 20);
4945
4959
  lines.push("");
4946
- lines.push(`## Allowed authors (whitelist of the sharpe winner rule)`);
4960
+ lines.push(`### Author tracks — raw ideas/hits/hitRate per rule (top ${tracks.length} by hitRate; full set in --json)`);
4947
4961
  lines.push("");
4948
- lines.push(`| Author | Ideas | Hits | HitRate |`);
4949
- lines.push(`| --- | --- | --- | --- |`);
4950
- // артефакт авторов живёт по-победительно в best[] — ран-левел
4951
- // authorStats в ISimulatorResult больше нет
4952
- const sharpeBest = result.best.find(({ criterion }) => criterion === "sharpe");
4953
- for (const stat of (sharpeBest?.authorStats ?? []).filter(({ banned }) => !banned)) {
4954
- lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
4962
+ lines.push(`| Author | Hold | Lock% | Stop% | Trail% | Ideas | Hits | HitRate |`);
4963
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- |`);
4964
+ for (const t of tracks) {
4965
+ lines.push(`| ${t.author} | ${t.holdMinutes / 60}h | ${t.profitLockPercent} | ${t.hardStopPercent} | ${t.trailingTakePercent} | ${t.ideas} | ${t.hits} | ${(t.hitRate * 100).toFixed(0)}% |`);
4955
4966
  }
4956
4967
  return lines.join("\n");
4957
4968
  };
4958
- const main$5 = async () => {
4969
+ const main$4 = async () => {
4959
4970
  if (!getEntry(import.meta.url)) {
4960
4971
  return;
4961
4972
  }
4962
4973
  const { values, positionals } = getArgs();
4963
- if (!values.simulator) {
4974
+ if (!values.sweep) {
4964
4975
  return;
4965
4976
  }
4966
4977
  const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
@@ -4968,6 +4979,33 @@ const main$5 = async () => {
4968
4979
  console.error("Error: positional path to an ideas .jsonl file is required");
4969
4980
  process.exit(1);
4970
4981
  }
4982
+ // конфиг — позиционный JSON; без него подтягивается пустой объект
4983
+ // и схема живёт на дефолтах движка (оси, порядок отчёта)
4984
+ const [configPath = null] = positionals.filter((value) => value.endsWith(".json") && !value.endsWith(".jsonl"));
4985
+ let config = {};
4986
+ if (configPath) {
4987
+ let content;
4988
+ try {
4989
+ content = await readFile(resolve(configPath), "utf-8");
4990
+ }
4991
+ catch (error) {
4992
+ console.error(`Error: cannot read config file: ${configPath}`);
4993
+ process.exit(1);
4994
+ }
4995
+ try {
4996
+ config = JSON.parse(content);
4997
+ }
4998
+ catch {
4999
+ console.error(`Error: invalid JSON in config file: ${configPath}`);
5000
+ process.exit(1);
5001
+ }
5002
+ const problem = validateConfig(config);
5003
+ if (problem) {
5004
+ console.error(`Error: sweep config does not match the structure — ${problem}`);
5005
+ console.error(`Expected shape: { "gridAxes"?: ISweepGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }`);
5006
+ process.exit(1);
5007
+ }
5008
+ }
4971
5009
  let ideas = [];
4972
5010
  {
4973
5011
  let content;
@@ -4992,7 +5030,7 @@ const main$5 = async () => {
4992
5030
  console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
4993
5031
  process.exit(1);
4994
5032
  }
4995
- const problem = validateIdea$1(idea, i + 1);
5033
+ const problem = validateIdea(idea, i + 1);
4996
5034
  if (problem) {
4997
5035
  console.error(`Error: ideas file does not match the idea structure — ${problem}`);
4998
5036
  console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
@@ -5021,7 +5059,7 @@ const main$5 = async () => {
5021
5059
  process.exit(-1);
5022
5060
  }
5023
5061
  }
5024
- await cli.moduleConnectionService.loadModule("simulator.module");
5062
+ await cli.moduleConnectionService.loadModule("sweep.module");
5025
5063
  {
5026
5064
  await cli.exchangeSchemaService.addSchema();
5027
5065
  await cli.symbolSchemaService.addSchema();
@@ -5039,31 +5077,13 @@ const main$5 = async () => {
5039
5077
  });
5040
5078
  }
5041
5079
  const symbol = values.symbol || "BTCUSDT";
5042
- // Проба осуществимости, НЕ поиск заработка (подбор параметров — у
5043
- // --tune): собирающая прибыль механика выключена, остаётся вопрос
5044
- // "есть ли прибыльный коридор" по стопу x холду x правилу бана —
5045
- // 48 точек, быстрый вердикт
5046
- addSimulatorSchema({
5047
- simulatorName: SIMULATOR_NAME$1,
5080
+ // оси и порядок отчёта приходят позиционным конфигом потребителя;
5081
+ // пустой конфиг — дефолтная сетка движка из connection-сервиса
5082
+ addSweepSchema({
5083
+ sweepName: SIMULATOR_NAME,
5048
5084
  exchangeName,
5049
- gridAxes: {
5050
- // грубая шкала катастрофы: коридор должен быть широким, не точкой
5051
- hardStopPercent: [2, 3, 5, 7],
5052
- // инертен: проба не собирает прибыль, выход — по времени или стопу
5053
- trailingTakePercent: [100],
5054
- holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
5055
- // одного проверенного автора достаточно — консенсус не перебираем
5056
- minIdeasAligned: [1],
5057
- // правило бана — единственная перебираемая "умность" пробы
5058
- minAuthorTrack: [3, 5],
5059
- minAuthorHitRate: [0.5, 0.6],
5060
- minAuthorWilson: [0],
5061
- minWeightAligned: [0],
5062
- profitLockPercent: [0],
5063
- authorMetric: ["close"],
5064
- banCriteria: ["sharpe", "pnl"],
5065
- },
5066
- reportOrder: "sharpe",
5085
+ ...(config.gridAxes ? { gridAxes: config.gridAxes } : {}),
5086
+ ...(config.reportOrder ? { reportOrder: config.reportOrder } : {}),
5067
5087
  callbacks: {
5068
5088
  onProgress: (symbol, stage, processed, total) => {
5069
5089
  if (values.verbose) {
@@ -5080,22 +5100,20 @@ const main$5 = async () => {
5080
5100
  console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
5081
5101
  }
5082
5102
  },
5083
- onAuthorsTrained: (symbol, stats, bannedIdeas) => {
5103
+ onAuthorsTrained: (symbol, tracks) => {
5084
5104
  if (values.verbose) {
5085
5105
  console.log("onAuthorsTrained", {
5086
5106
  symbol,
5087
- authors: stats.length,
5088
- banned: stats.filter(({ banned }) => banned).length,
5089
- bannedIdeas,
5107
+ authors: tracks.length,
5090
5108
  });
5091
5109
  }
5092
5110
  },
5093
- onGridPoint: (symbol, report) => {
5111
+ onGridPoint: (symbol, report, trades) => {
5094
5112
  if (values.verbose) {
5095
5113
  console.log("onGridPoint", {
5096
5114
  symbol,
5097
5115
  point: report.point,
5098
- trades: report.trades,
5116
+ trades: trades.length,
5099
5117
  pnl: +report.totalPnlPercent.toFixed(2),
5100
5118
  sharpe: +report.sharpe.toFixed(2),
5101
5119
  });
@@ -5115,20 +5133,19 @@ const main$5 = async () => {
5115
5133
  if (values.verbose) {
5116
5134
  console.log("onDone", {
5117
5135
  symbol,
5118
- reports: result.reports.length,
5119
- allowedAuthors: result.allowedAuthors.length,
5120
- bannedAuthors: result.bannedAuthors.length,
5136
+ reports: result.reports.reports.length,
5137
+ tracks: result.reports.tracks.length,
5121
5138
  });
5122
5139
  }
5123
5140
  },
5124
5141
  },
5125
5142
  });
5126
- const result = await Simulator.run({
5143
+ const result = await Sweep.run({
5127
5144
  symbol,
5128
- simulatorName: SIMULATOR_NAME$1,
5145
+ sweepName: SIMULATOR_NAME,
5129
5146
  ideas,
5130
5147
  });
5131
- const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
5148
+ const dumpName = values.output || `sweep_${symbol}_${Date.now()}`;
5132
5149
  const dumpDir = join(process.cwd(), "dump");
5133
5150
  if (values.json) {
5134
5151
  const filePath = resolve(dumpDir, `${dumpName}.json`);
@@ -5140,299 +5157,11 @@ const main$5 = async () => {
5140
5157
  if (values.markdown) {
5141
5158
  const filePath = resolve(dumpDir, `${dumpName}.md`);
5142
5159
  await mkdir(dumpDir, { recursive: true });
5143
- await writeFile(filePath, toMarkdown$1(result), "utf-8");
5160
+ await writeFile(filePath, toMarkdown(result), "utf-8");
5144
5161
  console.log(`Saved: ${filePath}`);
5145
5162
  process.exit(0);
5146
5163
  }
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");
5432
- console.log(`Saved: ${filePath}`);
5433
- process.exit(0);
5434
- }
5435
- console.log(toMarkdown(train, test, trainSplit));
5164
+ console.log(toMarkdown(result));
5436
5165
  process.exit(0);
5437
5166
  };
5438
5167
  main$4();
@@ -5633,8 +5362,7 @@ Modes:
5633
5362
  --dump Fetch and save raw OHLCV candles
5634
5363
  --pnldebug Simulate PnL per minute for a given entry price and direction
5635
5364
  --brokerdebug Fire a single broker commit against the live broker adapter
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
5365
+ --sweep <ideas.jsonl> [config.json] Grid sweep over crowd ideas: profit-before-stop corridor + raw author tracks
5638
5366
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
5639
5367
  --init Scaffold a new project in the current directory
5640
5368
  --docker Scaffold a Docker workspace for running strategies in a container
@@ -5755,59 +5483,36 @@ Broker debug flags (--brokerdebug):
5755
5483
  Loads ./live.module, fetches the last candle for --symbol/--timeframe, and calls
5756
5484
  the selected broker commit with synthetic payload values derived from current price.
5757
5485
 
5758
- Simulator flags (--simulator):
5486
+ Sweep flags (--sweep):
5759
5487
 
5760
5488
  --symbol <string> Trading pair to simulate (default: BTCUSDT)
5761
5489
  --exchange <string> Exchange name (default: first registered)
5762
- --output <string> Output file base name (default: simulator_{SYMBOL}_{TIMESTAMP})
5763
- --json Save full ISimulatorResult to ./dump/<output>.json
5490
+ --output <string> Output file base name (default: sweep_{SYMBOL}_{TIMESTAMP})
5491
+ --json Save full ISweepResult to ./dump/<output>.json
5764
5492
  --markdown Save summary report to ./dump/<output>.md
5765
- --verbose Log every simulator lifecycle callback to the console
5493
+ --verbose Log every sweep lifecycle callback to the console
5766
5494
 
5767
- Positional: path to an ideas .jsonl file — one idea per line with the exact shape
5495
+ Positionals: path to an ideas .jsonl file — one idea per line with the exact shape
5768
5496
  { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL",
5769
- "author": string }. The file is validated BEFORE any work; a line that does not
5770
- match the structure aborts the run with an error.
5771
-
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.
5779
- Ideas of other symbols are filtered out — one shared feed serves any --symbol.
5497
+ "author": string } — and an OPTIONAL config .json with the shape
5498
+ { "gridAxes"?: ISweepGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }.
5499
+ Both files are validated BEFORE any work; a mismatch aborts the run with an error.
5500
+ No config -> an empty object is used and the engine defaults apply (the full
5501
+ default grid axes and reportOrder "sharpe" of the connection service).
5502
+
5503
+ A GRID SWEEP: the grid from the config (or the engine default) is evaluated
5504
+ per point. One candle pass per idea to the grid's longest hold, flood dedupe
5505
+ (one idea per author per direction per 8h), EVERY author traded (no ban — the
5506
+ engine reports the raw per-author track graded inside each point's own hold
5507
+ window; who to trust is userspace), time-based Sharpe/Sortino, per-metric
5508
+ buckets with their own ranking winners and author tracks. Ideas of other
5509
+ symbols are filtered out — one shared feed serves any --symbol.
5780
5510
 
5781
5511
  No output flag → print the Markdown summary to stdout. With --verbose every
5782
- simulator lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
5512
+ sweep lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
5783
5513
  onGridPoint, onRanking, onDone) is logged to the console as it fires.
5784
5514
 
5785
- Module file ./modules/simulator.module is loaded automatically if it exists
5786
- (register your exchange there); without it CCXT Binance is used by default.
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
5515
+ Module file ./modules/sweep.module is loaded automatically if it exists
5811
5516
  (register your exchange there); without it CCXT Binance is used by default.
5812
5517
 
5813
5518
  Flush flags (--flush):
@@ -5843,8 +5548,7 @@ Module hooks (loaded automatically by each mode):
5843
5548
  modules/dump.module --dump Exchange schema for candle dumps
5844
5549
  modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
5845
5550
  modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
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
5551
+ modules/sweep.module --sweep Exchange schema for the crowd-ideas feasibility probe
5848
5552
 
5849
5553
  --flush has no associated module. It only removes dump subdirectories.
5850
5554
 
@@ -5873,10 +5577,8 @@ Examples:
5873
5577
  node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
5874
5578
  node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
5875
5579
  node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
5876
- node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./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
5580
+ node ${ENTRY_PATH} --sweep --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5581
+ node ${ENTRY_PATH} --sweep --symbol BTCUSDT --json --output jun_2026_probe ./assets/tv-ideas.normalized.jsonl ./assets/probe.config.json
5880
5582
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
5881
5583
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
5882
5584
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -5890,7 +5592,7 @@ const main$1 = async () => {
5890
5592
  if (!values.help) {
5891
5593
  return;
5892
5594
  }
5893
- process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n\n`);
5595
+ process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n\n`);
5894
5596
  process.stdout.write(HELP_TEXT);
5895
5597
  process.exit(0);
5896
5598
  };
@@ -5904,7 +5606,7 @@ const main = async () => {
5904
5606
  if (!values.version) {
5905
5607
  return;
5906
5608
  }
5907
- process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
5609
+ process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n`);
5908
5610
  process.exit(0);
5909
5611
  };
5910
5612
  main();