@backtest-kit/cli 17.0.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,11 +866,7 @@ const getArgs = singleshot(() => {
866
866
  type: "boolean",
867
867
  default: false,
868
868
  },
869
- simulator: {
870
- type: "boolean",
871
- default: false,
872
- },
873
- tune: {
869
+ sweep: {
874
870
  type: "boolean",
875
871
  default: false,
876
872
  },
@@ -3651,14 +3647,14 @@ const cli = {
3651
3647
  };
3652
3648
  init();
3653
3649
 
3654
- 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"];
3655
3651
  const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
3656
3652
  const HELP_TEXT$1 = `
3657
3653
  Example:
3658
3654
 
3659
3655
  node ${ENTRY_PATH$1} --help
3660
3656
  `.trimStart();
3661
- const main$k = async () => {
3657
+ const main$j = async () => {
3662
3658
  if (!getEntry(import.meta.url)) {
3663
3659
  return;
3664
3660
  }
@@ -3666,14 +3662,14 @@ const main$k = async () => {
3666
3662
  if (MODES.some((mode) => values[mode])) {
3667
3663
  return;
3668
3664
  }
3669
- process.stdout.write(`@backtest-kit/cli ${"17.0.0"}\n`);
3665
+ process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n`);
3670
3666
  process.stdout.write("\n");
3671
3667
  process.stdout.write(`Run with --help to see available commands.\n`);
3672
3668
  process.stdout.write("\n");
3673
3669
  process.stdout.write(HELP_TEXT$1);
3674
3670
  process.exit(0);
3675
3671
  };
3676
- main$k();
3672
+ main$j();
3677
3673
 
3678
3674
  const notifyShutdown = singleshot(async () => {
3679
3675
  console.log("Graceful shutdown initiated.");
@@ -3689,7 +3685,7 @@ const flush = async (entryPoint) => {
3689
3685
  console.log(`Removed: ${target}`);
3690
3686
  }
3691
3687
  };
3692
- const main$j = async () => {
3688
+ const main$i = async () => {
3693
3689
  if (!getEntry(import.meta.url)) {
3694
3690
  return;
3695
3691
  }
@@ -3706,7 +3702,7 @@ const main$j = async () => {
3706
3702
  }
3707
3703
  process.exit(0);
3708
3704
  };
3709
- main$j();
3705
+ main$i();
3710
3706
 
3711
3707
  const BEFORE_EXIT_FN$6 = singleshot(async () => {
3712
3708
  process.off("SIGINT", BEFORE_EXIT_FN$6);
@@ -3729,7 +3725,7 @@ const BEFORE_EXIT_FN$6 = singleshot(async () => {
3729
3725
  const listenGracefulShutdown$6 = singleshot(() => {
3730
3726
  process.on("SIGINT", BEFORE_EXIT_FN$6);
3731
3727
  });
3732
- const main$i = async () => {
3728
+ const main$h = async () => {
3733
3729
  if (!getEntry(import.meta.url)) {
3734
3730
  return;
3735
3731
  }
@@ -3747,7 +3743,7 @@ const main$i = async () => {
3747
3743
  await cli.backtestMainService.connect();
3748
3744
  listenGracefulShutdown$6();
3749
3745
  };
3750
- main$i();
3746
+ main$h();
3751
3747
 
3752
3748
  const BEFORE_EXIT_FN$5 = singleshot(async () => {
3753
3749
  process.off("SIGINT", BEFORE_EXIT_FN$5);
@@ -3766,7 +3762,7 @@ const BEFORE_EXIT_FN$5 = singleshot(async () => {
3766
3762
  const listenGracefulShutdown$5 = singleshot(() => {
3767
3763
  process.on("SIGINT", BEFORE_EXIT_FN$5);
3768
3764
  });
3769
- const main$h = async () => {
3765
+ const main$g = async () => {
3770
3766
  if (!getEntry(import.meta.url)) {
3771
3767
  return;
3772
3768
  }
@@ -3785,7 +3781,7 @@ const main$h = async () => {
3785
3781
  listenGracefulShutdown$5();
3786
3782
  await cli.walkerMainService.connect();
3787
3783
  };
3788
- main$h();
3784
+ main$g();
3789
3785
 
3790
3786
  const BEFORE_EXIT_FN$4 = singleshot(async () => {
3791
3787
  process.off("SIGINT", BEFORE_EXIT_FN$4);
@@ -3807,7 +3803,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
3807
3803
  const listenGracefulShutdown$4 = singleshot(() => {
3808
3804
  process.on("SIGINT", BEFORE_EXIT_FN$4);
3809
3805
  });
3810
- const main$g = async () => {
3806
+ const main$f = async () => {
3811
3807
  if (!getEntry(import.meta.url)) {
3812
3808
  return;
3813
3809
  }
@@ -3821,7 +3817,7 @@ const main$g = async () => {
3821
3817
  await cli.paperMainService.connect();
3822
3818
  listenGracefulShutdown$4();
3823
3819
  };
3824
- main$g();
3820
+ main$f();
3825
3821
 
3826
3822
  const BEFORE_EXIT_FN$3 = singleshot(async () => {
3827
3823
  process.off("SIGINT", BEFORE_EXIT_FN$3);
@@ -3843,7 +3839,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
3843
3839
  const listenGracefulShutdown$3 = singleshot(() => {
3844
3840
  process.on("SIGINT", BEFORE_EXIT_FN$3);
3845
3841
  });
3846
- const main$f = async () => {
3842
+ const main$e = async () => {
3847
3843
  if (!getEntry(import.meta.url)) {
3848
3844
  return;
3849
3845
  }
@@ -3857,7 +3853,7 @@ const main$f = async () => {
3857
3853
  await cli.liveMainService.connect();
3858
3854
  listenGracefulShutdown$3();
3859
3855
  };
3860
- main$f();
3856
+ main$e();
3861
3857
 
3862
3858
  const stopBacktestList$1 = async () => {
3863
3859
  for (const item of await Backtest.list()) {
@@ -3921,7 +3917,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
3921
3917
  const listenGracefulShutdown$2 = singleshot(() => {
3922
3918
  process.on("SIGINT", BEFORE_EXIT_FN$2);
3923
3919
  });
3924
- const main$e = async () => {
3920
+ const main$d = async () => {
3925
3921
  if (!getEntry(import.meta.url)) {
3926
3922
  return;
3927
3923
  }
@@ -3977,7 +3973,7 @@ const main$e = async () => {
3977
3973
  listenGracefulShutdown$2();
3978
3974
  await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
3979
3975
  };
3980
- main$e();
3976
+ main$d();
3981
3977
 
3982
3978
  const MODE_MODULE = {
3983
3979
  backtest: "backtest.module",
@@ -4080,7 +4076,7 @@ const createGracefulShutdown = (mode) => {
4080
4076
  process.on("SIGINT", handler);
4081
4077
  });
4082
4078
  };
4083
- const main$d = async () => {
4079
+ const main$c = async () => {
4084
4080
  if (!getEntry(import.meta.url)) {
4085
4081
  return;
4086
4082
  }
@@ -4156,7 +4152,7 @@ const main$d = async () => {
4156
4152
  }
4157
4153
  await getEntrySubject().next(absolutePath);
4158
4154
  };
4159
- main$d();
4155
+ main$c();
4160
4156
 
4161
4157
  const BEFORE_EXIT_FN$1 = singleshot(async () => {
4162
4158
  process.off("SIGINT", BEFORE_EXIT_FN$1);
@@ -4167,7 +4163,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
4167
4163
  const listenGracefulShutdown$1 = singleshot(() => {
4168
4164
  process.on("SIGINT", BEFORE_EXIT_FN$1);
4169
4165
  });
4170
- const main$c = async () => {
4166
+ const main$b = async () => {
4171
4167
  if (!getEntry(import.meta.url)) {
4172
4168
  return;
4173
4169
  }
@@ -4177,7 +4173,7 @@ const main$c = async () => {
4177
4173
  }
4178
4174
  listenGracefulShutdown$1();
4179
4175
  };
4180
- main$c();
4176
+ main$b();
4181
4177
 
4182
4178
  const BEFORE_EXIT_FN = singleshot(async () => {
4183
4179
  process.off("SIGINT", BEFORE_EXIT_FN);
@@ -4188,7 +4184,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
4188
4184
  const listenGracefulShutdown = singleshot(() => {
4189
4185
  process.on("SIGINT", BEFORE_EXIT_FN);
4190
4186
  });
4191
- const main$b = async () => {
4187
+ const main$a = async () => {
4192
4188
  if (!getEntry(import.meta.url)) {
4193
4189
  return;
4194
4190
  }
@@ -4198,7 +4194,7 @@ const main$b = async () => {
4198
4194
  }
4199
4195
  listenGracefulShutdown();
4200
4196
  };
4201
- main$b();
4197
+ main$a();
4202
4198
 
4203
4199
  const EXTRACT_ROWS_FN = (plots, schema) => {
4204
4200
  const keys = Object.keys(schema);
@@ -4220,7 +4216,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
4220
4216
  }
4221
4217
  return rows;
4222
4218
  };
4223
- const main$a = async () => {
4219
+ const main$9 = async () => {
4224
4220
  if (!getEntry(import.meta.url)) {
4225
4221
  return;
4226
4222
  }
@@ -4312,17 +4308,17 @@ const main$a = async () => {
4312
4308
  if (values.markdown) {
4313
4309
  const filePath = resolve(dumpDir, `${dumpName}.md`);
4314
4310
  await mkdir(dumpDir, { recursive: true });
4315
- await writeFile(filePath, await toMarkdown$2(signalId, plots, signalSchema), "utf-8");
4311
+ await writeFile(filePath, await toMarkdown$1(signalId, plots, signalSchema), "utf-8");
4316
4312
  console.log(`Saved: ${filePath}`);
4317
4313
  process.exit(0);
4318
4314
  return;
4319
4315
  }
4320
- console.log(await toMarkdown$2(signalId, plots, signalSchema));
4316
+ console.log(await toMarkdown$1(signalId, plots, signalSchema));
4321
4317
  process.exit(0);
4322
4318
  };
4323
- main$a();
4319
+ main$9();
4324
4320
 
4325
- const main$9 = async () => {
4321
+ const main$8 = async () => {
4326
4322
  if (!getEntry(import.meta.url)) {
4327
4323
  return;
4328
4324
  }
@@ -4385,9 +4381,9 @@ const main$9 = async () => {
4385
4381
  };
4386
4382
  process.on("SIGINT", beforeExit);
4387
4383
  };
4388
- main$9();
4384
+ main$8();
4389
4385
 
4390
- const main$8 = async () => {
4386
+ const main$7 = async () => {
4391
4387
  if (!getEntry(import.meta.url)) {
4392
4388
  return;
4393
4389
  }
@@ -4468,9 +4464,9 @@ const main$8 = async () => {
4468
4464
  console.log(JSON.stringify(candles, null, 2));
4469
4465
  process.exit(0);
4470
4466
  };
4471
- main$8();
4467
+ main$7();
4472
4468
 
4473
- const main$7 = async () => {
4469
+ const main$6 = async () => {
4474
4470
  if (!getEntry(import.meta.url)) {
4475
4471
  return;
4476
4472
  }
@@ -4584,7 +4580,7 @@ const main$7 = async () => {
4584
4580
  }
4585
4581
  process.exit(0);
4586
4582
  };
4587
- main$7();
4583
+ main$6();
4588
4584
 
4589
4585
  const getBroker = singleshot(() => {
4590
4586
  // Reaches into BrokerAdapter internals on purpose: the public commit*
@@ -4693,7 +4689,7 @@ const ZERO_PNL = {
4693
4689
  pnlCost: 0,
4694
4690
  pnlEntries: 0,
4695
4691
  };
4696
- const main$6 = async () => {
4692
+ const main$5 = async () => {
4697
4693
  if (!getEntry(import.meta.url)) {
4698
4694
  return;
4699
4695
  }
@@ -4887,23 +4883,23 @@ const main$6 = async () => {
4887
4883
  }
4888
4884
  process.exit(0);
4889
4885
  };
4890
- main$6();
4886
+ main$5();
4891
4887
 
4892
- const SIMULATOR_NAME$1 = "cli_simulator";
4893
- const CONFIG_KEYS$1 = ["gridAxes", "reportOrder"];
4894
- const validateConfig$1 = (config) => {
4888
+ const SIMULATOR_NAME = "cli_sweep";
4889
+ const CONFIG_KEYS = ["gridAxes", "reportOrder"];
4890
+ const validateConfig = (config) => {
4895
4891
  if (typeof config !== "object" || config === null || Array.isArray(config)) {
4896
4892
  return "config must be a JSON object";
4897
4893
  }
4898
4894
  for (const key of Object.keys(config)) {
4899
- if (!CONFIG_KEYS$1.includes(key)) {
4900
- return `unknown key "${key}" (expected: ${CONFIG_KEYS$1.join(", ")})`;
4895
+ if (!CONFIG_KEYS.includes(key)) {
4896
+ return `unknown key "${key}" (expected: ${CONFIG_KEYS.join(", ")})`;
4901
4897
  }
4902
4898
  }
4903
4899
  return null;
4904
4900
  };
4905
- const IDEA_DIRECTIONS$1 = ["LONG", "SHORT", "NEUTRAL"];
4906
- const validateIdea$1 = (idea, line) => {
4901
+ const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
4902
+ const validateIdea = (idea, line) => {
4907
4903
  if (typeof idea !== "object" || idea === null) {
4908
4904
  return `line ${line}: not an object`;
4909
4905
  }
@@ -4916,20 +4912,21 @@ const validateIdea$1 = (idea, line) => {
4916
4912
  if (typeof idea.symbol !== "string" || !idea.symbol) {
4917
4913
  return `line ${line}: "symbol" must be a non-empty string`;
4918
4914
  }
4919
- if (!IDEA_DIRECTIONS$1.includes(idea.direction)) {
4920
- 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)}`;
4921
4917
  }
4922
4918
  if (typeof idea.author !== "string" || !idea.author) {
4923
4919
  return `line ${line}: "author" must be a non-empty string`;
4924
4920
  }
4925
4921
  return null;
4926
4922
  };
4927
- const toMarkdown$1 = (result) => {
4928
- const buckets = Object.entries(result.reports).filter(([, bucket]) => bucket.reports.length > 0);
4929
- const allReports = buckets.flatMap(([, bucket]) => bucket.reports);
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;
4930
4927
  const profitable = allReports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
4931
4928
  const lines = [];
4932
- lines.push(`# Simulator Report — ${result.symbol}`);
4929
+ lines.push(`# Sweep Report — ${result.symbol}`);
4933
4930
  lines.push("");
4934
4931
  lines.push(`| Metric | Value |`);
4935
4932
  lines.push(`| --- | --- |`);
@@ -4937,41 +4934,44 @@ const toMarkdown$1 = (result) => {
4937
4934
  lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
4938
4935
  lines.push(`| Profitable corridor | ${profitable} / ${allReports.length} grid points |`);
4939
4936
  lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
4940
- // каждая метрика — самодостаточная корзина: свои победители и
4941
- // свои словари банов, между собой не склеиваются
4942
- for (const [metric, bucket] of buckets) {
4943
- lines.push("");
4944
- lines.push(`## Metric: ${metric}`);
4945
- lines.push("");
4946
- lines.push(`| Criterion | Stop% | Hold | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4947
- lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4948
- for (const best of bucket.best) {
4949
- if (!best.report) {
4950
- lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
4951
- continue;
4952
- }
4953
- const { point } = best.report;
4954
- lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.holdMinutes / 60}h | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
4955
- `${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
4956
- }
4957
- const sharpeBest = bucket.best.find(({ criterion }) => criterion === "sharpe");
4958
- lines.push("");
4959
- lines.push(`### Allowed authors (sharpe winner rule)`);
4960
- lines.push("");
4961
- lines.push(`| Author | Ideas | Hits | HitRate |`);
4962
- lines.push(`| --- | --- | --- | --- |`);
4963
- for (const stat of (sharpeBest?.authorStats ?? []).filter(({ banned }) => !banned)) {
4964
- lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
4937
+ // единственная метрика — profit-before-stop: одна корзина с
4938
+ // победителями и author tracks
4939
+ lines.push("");
4940
+ lines.push(`## Grid winners`);
4941
+ lines.push("");
4942
+ lines.push(`| Criterion | Stop% | Trail% | Hold | Lock% | Trades | PNL% | WinRate | Sharpe | Sortino |`);
4943
+ lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
4944
+ for (const best of bucket.best) {
4945
+ if (!best.report) {
4946
+ lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
4947
+ continue;
4965
4948
  }
4949
+ const { point } = best.report;
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);
4959
+ lines.push("");
4960
+ lines.push(`### Author tracks — raw ideas/hits/hitRate per rule (top ${tracks.length} by hitRate; full set in --json)`);
4961
+ lines.push("");
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)}% |`);
4966
4966
  }
4967
4967
  return lines.join("\n");
4968
4968
  };
4969
- const main$5 = async () => {
4969
+ const main$4 = async () => {
4970
4970
  if (!getEntry(import.meta.url)) {
4971
4971
  return;
4972
4972
  }
4973
4973
  const { values, positionals } = getArgs();
4974
- if (!values.simulator) {
4974
+ if (!values.sweep) {
4975
4975
  return;
4976
4976
  }
4977
4977
  const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
@@ -4999,10 +4999,10 @@ const main$5 = async () => {
4999
4999
  console.error(`Error: invalid JSON in config file: ${configPath}`);
5000
5000
  process.exit(1);
5001
5001
  }
5002
- const problem = validateConfig$1(config);
5002
+ const problem = validateConfig(config);
5003
5003
  if (problem) {
5004
- console.error(`Error: simulator config does not match the structure — ${problem}`);
5005
- console.error(`Expected shape: { "gridAxes"?: ISimulatorGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }`);
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
5006
  process.exit(1);
5007
5007
  }
5008
5008
  }
@@ -5030,7 +5030,7 @@ const main$5 = async () => {
5030
5030
  console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
5031
5031
  process.exit(1);
5032
5032
  }
5033
- const problem = validateIdea$1(idea, i + 1);
5033
+ const problem = validateIdea(idea, i + 1);
5034
5034
  if (problem) {
5035
5035
  console.error(`Error: ideas file does not match the idea structure — ${problem}`);
5036
5036
  console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
@@ -5059,7 +5059,7 @@ const main$5 = async () => {
5059
5059
  process.exit(-1);
5060
5060
  }
5061
5061
  }
5062
- await cli.moduleConnectionService.loadModule("simulator.module");
5062
+ await cli.moduleConnectionService.loadModule("sweep.module");
5063
5063
  {
5064
5064
  await cli.exchangeSchemaService.addSchema();
5065
5065
  await cli.symbolSchemaService.addSchema();
@@ -5079,8 +5079,8 @@ const main$5 = async () => {
5079
5079
  const symbol = values.symbol || "BTCUSDT";
5080
5080
  // оси и порядок отчёта приходят позиционным конфигом потребителя;
5081
5081
  // пустой конфиг — дефолтная сетка движка из connection-сервиса
5082
- addSimulatorSchema({
5083
- simulatorName: SIMULATOR_NAME$1,
5082
+ addSweepSchema({
5083
+ sweepName: SIMULATOR_NAME,
5084
5084
  exchangeName,
5085
5085
  ...(config.gridAxes ? { gridAxes: config.gridAxes } : {}),
5086
5086
  ...(config.reportOrder ? { reportOrder: config.reportOrder } : {}),
@@ -5100,22 +5100,20 @@ const main$5 = async () => {
5100
5100
  console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
5101
5101
  }
5102
5102
  },
5103
- onAuthorsTrained: (symbol, stats, bannedIdeas) => {
5103
+ onAuthorsTrained: (symbol, tracks) => {
5104
5104
  if (values.verbose) {
5105
5105
  console.log("onAuthorsTrained", {
5106
5106
  symbol,
5107
- authors: stats.length,
5108
- banned: stats.filter(({ banned }) => banned).length,
5109
- bannedIdeas,
5107
+ authors: tracks.length,
5110
5108
  });
5111
5109
  }
5112
5110
  },
5113
- onGridPoint: (symbol, report) => {
5111
+ onGridPoint: (symbol, report, trades) => {
5114
5112
  if (values.verbose) {
5115
5113
  console.log("onGridPoint", {
5116
5114
  symbol,
5117
5115
  point: report.point,
5118
- trades: report.trades,
5116
+ trades: trades.length,
5119
5117
  pnl: +report.totalPnlPercent.toFixed(2),
5120
5118
  sharpe: +report.sharpe.toFixed(2),
5121
5119
  });
@@ -5135,19 +5133,19 @@ const main$5 = async () => {
5135
5133
  if (values.verbose) {
5136
5134
  console.log("onDone", {
5137
5135
  symbol,
5138
- reports: Object.values(result.reports).flatMap((bucket) => bucket.reports).length,
5139
- bans: Object.values(result.reports).flatMap((bucket) => bucket.bans).length,
5136
+ reports: result.reports.reports.length,
5137
+ tracks: result.reports.tracks.length,
5140
5138
  });
5141
5139
  }
5142
5140
  },
5143
5141
  },
5144
5142
  });
5145
- const result = await Simulator.run({
5143
+ const result = await Sweep.run({
5146
5144
  symbol,
5147
- simulatorName: SIMULATOR_NAME$1,
5145
+ sweepName: SIMULATOR_NAME,
5148
5146
  ideas,
5149
5147
  });
5150
- const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
5148
+ const dumpName = values.output || `sweep_${symbol}_${Date.now()}`;
5151
5149
  const dumpDir = join(process.cwd(), "dump");
5152
5150
  if (values.json) {
5153
5151
  const filePath = resolve(dumpDir, `${dumpName}.json`);
@@ -5159,308 +5157,11 @@ const main$5 = async () => {
5159
5157
  if (values.markdown) {
5160
5158
  const filePath = resolve(dumpDir, `${dumpName}.md`);
5161
5159
  await mkdir(dumpDir, { recursive: true });
5162
- await writeFile(filePath, toMarkdown$1(result), "utf-8");
5160
+ await writeFile(filePath, toMarkdown(result), "utf-8");
5163
5161
  console.log(`Saved: ${filePath}`);
5164
5162
  process.exit(0);
5165
5163
  }
5166
- console.log(toMarkdown$1(result));
5167
- process.exit(0);
5168
- };
5169
- main$5();
5170
-
5171
- const SIMULATOR_NAME = "cli_tune";
5172
- const CONFIG_KEYS = ["point", "authorStats", "gridAxes", "reportOrder"];
5173
- const POINT_NUMBER_FIELDS = [
5174
- "hardStopPercent",
5175
- "trailingTakePercent",
5176
- "holdMinutes",
5177
- "minAuthorTrack",
5178
- "minAuthorHitRate",
5179
- "profitLockPercent",
5180
- ];
5181
- const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
5182
- const validateIdea = (idea, line) => {
5183
- if (typeof idea !== "object" || idea === null) {
5184
- return `line ${line}: not an object`;
5185
- }
5186
- if (typeof idea.id !== "number") {
5187
- return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
5188
- }
5189
- if (typeof idea.ts !== "number") {
5190
- return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
5191
- }
5192
- if (typeof idea.symbol !== "string" || !idea.symbol) {
5193
- return `line ${line}: "symbol" must be a non-empty string`;
5194
- }
5195
- if (!IDEA_DIRECTIONS.includes(idea.direction)) {
5196
- return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
5197
- }
5198
- if (typeof idea.author !== "string" || !idea.author) {
5199
- return `line ${line}: "author" must be a non-empty string`;
5200
- }
5201
- return null;
5202
- };
5203
- const validateConfig = (config) => {
5204
- if (typeof config !== "object" || config === null || Array.isArray(config)) {
5205
- return "config must be a JSON object";
5206
- }
5207
- for (const key of Object.keys(config)) {
5208
- if (!CONFIG_KEYS.includes(key)) {
5209
- return `unknown key "${key}" (expected: ${CONFIG_KEYS.join(", ")})`;
5210
- }
5211
- }
5212
- if (typeof config.point !== "object" || config.point === null) {
5213
- return `"point" is required — the frozen grid point of a training run`;
5214
- }
5215
- for (const field of POINT_NUMBER_FIELDS) {
5216
- if (typeof config.point[field] !== "number") {
5217
- return `"point.${field}" must be a number, got ${typeof config.point[field]}`;
5218
- }
5219
- }
5220
- if (typeof config.point.authorMetric !== "string") {
5221
- return `"point.authorMetric" must be a string, got ${typeof config.point.authorMetric}`;
5222
- }
5223
- if (!Array.isArray(config.authorStats) || !config.authorStats.length) {
5224
- return `"authorStats" is required — the frozen raw track record (non-empty array)`;
5225
- }
5226
- for (let i = 0; i < config.authorStats.length; i++) {
5227
- const stat = config.authorStats[i];
5228
- if (typeof stat !== "object" ||
5229
- stat === null ||
5230
- typeof stat.author !== "string" ||
5231
- typeof stat.ideas !== "number" ||
5232
- typeof stat.hits !== "number") {
5233
- return `"authorStats[${i}]" must be { author: string, ideas: number, hits: number }`;
5234
- }
5235
- }
5236
- return null;
5237
- };
5238
- /** Одноточечные оси, зеркалящие замороженную точку — сетка инертна. */
5239
- const axesFromPoint = (point) => ({
5240
- hardStopPercent: [point.hardStopPercent],
5241
- trailingTakePercent: [point.trailingTakePercent],
5242
- holdMinutes: [point.holdMinutes],
5243
- minAuthorTrack: [point.minAuthorTrack],
5244
- minAuthorHitRate: [point.minAuthorHitRate],
5245
- profitLockPercent: [point.profitLockPercent],
5246
- authorMetric: [point.authorMetric],
5247
- });
5248
- const pointLabel = (point) => `H=${point.hardStopPercent} TT=${point.trailingTakePercent} hold=${point.holdMinutes / 60}h ` +
5249
- `track=${point.minAuthorTrack} rate=${point.minAuthorHitRate} ` +
5250
- `lock=${point.profitLockPercent} metric=${point.authorMetric}`;
5251
- const fmtRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
5252
- const toMarkdown = (test) => {
5253
- const lines = [];
5254
- lines.push(`# Tune Report — ${test.symbol}`);
5255
- lines.push("");
5256
- lines.push(`ONE out-of-sample shot of a FROZEN training artifact: nothing is trained here — the point and the raw author track record come from the input config verbatim, bans are re-derived from the frozen numbers under the point's rule, unseen authors are banned by default.`);
5257
- lines.push("");
5258
- lines.push(`Frozen point: ${pointLabel(test.point)}`);
5259
- lines.push("");
5260
- lines.push(`## Out-of-sample (${test.ideasDirectional} directional ideas)`);
5261
- lines.push("");
5262
- lines.push(`| Metric | Value |`);
5263
- lines.push(`| --- | --- |`);
5264
- lines.push(`| Trades (skipped busy) | ${test.report.trades} (${test.report.skippedBusy}) |`);
5265
- lines.push(`| PNL | ${test.report.totalPnlPercent.toFixed(2)}% |`);
5266
- lines.push(`| Win rate | ${(test.report.winRate * 100).toFixed(0)}% |`);
5267
- lines.push(`| Profit factor | ${fmtRatio(test.report.profitFactor)} |`);
5268
- lines.push(`| Max series drawdown | ${test.report.maxSeriesDrawdownPercent.toFixed(2)}% |`);
5269
- lines.push(`| Sharpe / Sortino | ${fmtRatio(test.report.sharpe)} / ${fmtRatio(test.report.sortino)} |`);
5270
- lines.push(`| Recovery factor | ${fmtRatio(test.report.recoveryFactor)} |`);
5271
- lines.push(`| Exits | ${Object.entries(test.report.exitReasons).map(([reason, count]) => `${reason}=${count}`).join(", ")} |`);
5272
- lines.push("");
5273
- lines.push(`## Test trades`);
5274
- lines.push("");
5275
- lines.push(`| Direction | Exit | PNL% | Hold | Entry (UTC) |`);
5276
- lines.push(`| --- | --- | --- | --- | --- |`);
5277
- for (const trade of test.trades) {
5278
- lines.push(`| ${trade.direction} | ${trade.exitReason} | ${trade.pnlPercent.toFixed(2)} | ${trade.holdMinutesActual}m | ${new Date(trade.entryTimestamp).toISOString()} |`);
5279
- }
5280
- lines.push("");
5281
- lines.push(`## Frozen author track record (re-derived bans under the point's rule)`);
5282
- lines.push("");
5283
- lines.push(`| Author | Ideas | Hits | Banned |`);
5284
- lines.push(`| --- | --- | --- | --- |`);
5285
- for (const stat of test.authorStats) {
5286
- lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${stat.banned ? "yes" : ""} |`);
5287
- }
5288
- return lines.join("\n");
5289
- };
5290
- const main$4 = async () => {
5291
- if (!getEntry(import.meta.url)) {
5292
- return;
5293
- }
5294
- const { values, positionals } = getArgs();
5295
- if (!values.tune) {
5296
- return;
5297
- }
5298
- const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
5299
- if (!ideasPath) {
5300
- console.error("Error: positional path to an ideas .jsonl file is required");
5301
- process.exit(1);
5302
- }
5303
- // конфиг — позиционный JSON; без него подтягивается пустой объект
5304
- // (и валидация честно скажет, что замороженной точки нет)
5305
- const [configPath = null] = positionals.filter((value) => value.endsWith(".json") && !value.endsWith(".jsonl"));
5306
- let config = {};
5307
- if (configPath) {
5308
- let content;
5309
- try {
5310
- content = await readFile(resolve(configPath), "utf-8");
5311
- }
5312
- catch (error) {
5313
- console.error(`Error: cannot read config file: ${configPath}`);
5314
- process.exit(1);
5315
- }
5316
- try {
5317
- config = JSON.parse(content);
5318
- }
5319
- catch {
5320
- console.error(`Error: invalid JSON in config file: ${configPath}`);
5321
- process.exit(1);
5322
- }
5323
- }
5324
- {
5325
- const problem = validateConfig(config);
5326
- if (problem) {
5327
- console.error(`Error: tune config does not match the structure — ${problem}`);
5328
- console.error(`Expected shape: { "point": ISimulatorGridPoint, "authorStats": [{ "author", "ideas", "hits" }], "gridAxes"?, "reportOrder"? }`);
5329
- process.exit(1);
5330
- }
5331
- }
5332
- let ideas = [];
5333
- {
5334
- let content;
5335
- try {
5336
- content = await readFile(resolve(ideasPath), "utf-8");
5337
- }
5338
- catch (error) {
5339
- console.error(`Error: cannot read ideas file: ${ideasPath}`);
5340
- process.exit(1);
5341
- }
5342
- const rows = content.split("\n").filter(Boolean);
5343
- if (!rows.length) {
5344
- console.error(`Error: ideas file is empty: ${ideasPath}`);
5345
- process.exit(1);
5346
- }
5347
- for (let i = 0; i < rows.length; i++) {
5348
- let idea;
5349
- try {
5350
- idea = JSON.parse(rows[i]);
5351
- }
5352
- catch {
5353
- console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
5354
- process.exit(1);
5355
- }
5356
- const problem = validateIdea(idea, i + 1);
5357
- if (problem) {
5358
- console.error(`Error: ideas file does not match the idea structure — ${problem}`);
5359
- console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
5360
- process.exit(1);
5361
- }
5362
- ideas.push(idea);
5363
- }
5364
- }
5365
- {
5366
- const cwd = process.cwd();
5367
- dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
5368
- }
5369
- await cli.configConnectionService.loadConfig("setup.config");
5370
- {
5371
- const loader = await cli.configConnectionService.loadConfig("loader.config");
5372
- try {
5373
- if (typeof loader === "function") {
5374
- await loader();
5375
- }
5376
- if (typeof loader?.loader === "function") {
5377
- await loader.loader();
5378
- }
5379
- }
5380
- catch (error) {
5381
- console.error("Module loader failed", error);
5382
- process.exit(-1);
5383
- }
5384
- }
5385
- await cli.moduleConnectionService.loadModule("tune.module");
5386
- {
5387
- await cli.exchangeSchemaService.addSchema();
5388
- await cli.symbolSchemaService.addSchema();
5389
- }
5390
- const [defaultExchangeName = null] = await listExchangeSchema();
5391
- const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
5392
- if (values.verbose) {
5393
- overrideExchangeSchema({
5394
- exchangeName,
5395
- callbacks: {
5396
- onCandleData(symbol, interval, since) {
5397
- console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
5398
- },
5399
- },
5400
- });
5401
- }
5402
- const symbol = values.symbol || "BTCUSDT";
5403
- // сетка для test() инертна (оценивается ровно замороженная точка):
5404
- // без явных осей в конфиге они одноточечно зеркалят точку
5405
- addSimulatorSchema({
5406
- simulatorName: SIMULATOR_NAME,
5407
- exchangeName,
5408
- gridAxes: config.gridAxes ?? axesFromPoint(config.point),
5409
- ...(config.reportOrder ? { reportOrder: config.reportOrder } : {}),
5410
- callbacks: {
5411
- onProgress: (symbol, stage, processed, total) => {
5412
- if (values.verbose) {
5413
- console.log("onProgress", { symbol, stage, processed, total });
5414
- }
5415
- },
5416
- onIdeas: (symbol, ideasTotal, ideasDirectional) => {
5417
- if (values.verbose) {
5418
- console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
5419
- }
5420
- },
5421
- onProfiles: (symbol, profiles, truncatedCount) => {
5422
- if (values.verbose) {
5423
- console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
5424
- }
5425
- },
5426
- onTestDone: (symbol, result) => {
5427
- if (values.verbose) {
5428
- console.log("onTestDone", {
5429
- symbol,
5430
- trades: result.report.trades,
5431
- pnl: +result.report.totalPnlPercent.toFixed(2),
5432
- });
5433
- }
5434
- },
5435
- },
5436
- });
5437
- // только out-of-sample: обучения нет, точка и трек-рекорд приходят
5438
- // из конфига verbatim — test() перевыводит баны под правило точки,
5439
- // невиданные авторы забанены по умолчанию
5440
- const test = await Simulator.test({
5441
- symbol,
5442
- simulatorName: SIMULATOR_NAME,
5443
- ideas,
5444
- point: config.point,
5445
- authorStats: config.authorStats,
5446
- });
5447
- const dumpName = values.output || `tune_${symbol}_${Date.now()}`;
5448
- const dumpDir = join(process.cwd(), "dump");
5449
- if (values.json) {
5450
- const filePath = resolve(dumpDir, `${dumpName}.json`);
5451
- await mkdir(dumpDir, { recursive: true });
5452
- await writeFile(filePath, JSON.stringify(test, null, 2), "utf-8");
5453
- console.log(`Saved: ${filePath}`);
5454
- process.exit(0);
5455
- }
5456
- if (values.markdown) {
5457
- const filePath = resolve(dumpDir, `${dumpName}.md`);
5458
- await mkdir(dumpDir, { recursive: true });
5459
- await writeFile(filePath, toMarkdown(test), "utf-8");
5460
- console.log(`Saved: ${filePath}`);
5461
- process.exit(0);
5462
- }
5463
- console.log(toMarkdown(test));
5164
+ console.log(toMarkdown(result));
5464
5165
  process.exit(0);
5465
5166
  };
5466
5167
  main$4();
@@ -5661,8 +5362,7 @@ Modes:
5661
5362
  --dump Fetch and save raw OHLCV candles
5662
5363
  --pnldebug Simulate PnL per minute for a given entry price and direction
5663
5364
  --brokerdebug Fire a single broker commit against the live broker adapter
5664
- --simulator <ideas.jsonl> [config.json] Feasibility probe over crowd ideas: is there a profitable corridor at all
5665
- --tune <ideas.jsonl> <config.json> ONE out-of-sample shot of a frozen training artifact (point + author track)
5365
+ --sweep <ideas.jsonl> [config.json] Grid sweep over crowd ideas: profit-before-stop corridor + raw author tracks
5666
5366
  --flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
5667
5367
  --init Scaffold a new project in the current directory
5668
5368
  --docker Scaffold a Docker workspace for running strategies in a container
@@ -5783,64 +5483,36 @@ Broker debug flags (--brokerdebug):
5783
5483
  Loads ./live.module, fetches the last candle for --symbol/--timeframe, and calls
5784
5484
  the selected broker commit with synthetic payload values derived from current price.
5785
5485
 
5786
- Simulator flags (--simulator):
5486
+ Sweep flags (--sweep):
5787
5487
 
5788
5488
  --symbol <string> Trading pair to simulate (default: BTCUSDT)
5789
5489
  --exchange <string> Exchange name (default: first registered)
5790
- --output <string> Output file base name (default: simulator_{SYMBOL}_{TIMESTAMP})
5791
- --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
5792
5492
  --markdown Save summary report to ./dump/<output>.md
5793
- --verbose Log every simulator lifecycle callback to the console
5493
+ --verbose Log every sweep lifecycle callback to the console
5794
5494
 
5795
5495
  Positionals: path to an ideas .jsonl file — one idea per line with the exact shape
5796
5496
  { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL",
5797
5497
  "author": string } — and an OPTIONAL config .json with the shape
5798
- { "gridAxes"?: ISimulatorGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }.
5498
+ { "gridAxes"?: ISweepGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }.
5799
5499
  Both files are validated BEFORE any work; a mismatch aborts the run with an error.
5800
5500
  No config -> an empty object is used and the engine defaults apply (the full
5801
5501
  default grid axes and reportOrder "sharpe" of the connection service).
5802
5502
 
5803
- A FEASIBILITY PROBE, not an out-of-sample shot (that is --tune): the grid from
5804
- the config (or the engine default) answers one question — does the feed contain
5805
- a profitable corridor at all. One candle pass per idea to the grid's longest
5806
- hold, flood dedupe (one idea per author per direction per 8h), default-ban
5807
- author filter graded inside each point's own hold window, time-based
5808
- Sharpe/Sortino, per-metric buckets with their own ranking winners and ban
5809
- dictionaries. Ideas of other symbols are filtered out — one shared feed serves
5810
- any --symbol.
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.
5811
5510
 
5812
5511
  No output flag → print the Markdown summary to stdout. With --verbose every
5813
- simulator lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
5512
+ sweep lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
5814
5513
  onGridPoint, onRanking, onDone) is logged to the console as it fires.
5815
5514
 
5816
- Module file ./modules/simulator.module is loaded automatically if it exists
5817
- (register your exchange there); without it CCXT Binance is used by default.
5818
-
5819
- Tune flags (--tune):
5820
-
5821
- --symbol <string> Trading pair to test (default: BTCUSDT)
5822
- --exchange <string> Exchange name (default: first registered)
5823
- --output <string> Output file base name (default: tune_{SYMBOL}_{TIMESTAMP})
5824
- --json Save the full ISimulatorTestResult to ./dump/<output>.json
5825
- --markdown Save the out-of-sample report to ./dump/<output>.md
5826
- --verbose Log simulator lifecycle callbacks to the console
5827
-
5828
- Positionals: path to an ideas .jsonl file (same shape and validation as
5829
- --simulator) and a config .json carrying the FROZEN training artifact:
5830
- { "point": ISimulatorGridPoint, "authorStats": [{ "author", "ideas", "hits" }],
5831
- "gridAxes"?, "reportOrder"? }. The point and authorStats are REQUIRED — without
5832
- them there is nothing to test and the run aborts with an error. gridAxes are
5833
- optional: by default they mirror the frozen point one value per axis (the grid
5834
- is inert for a test).
5835
-
5836
- ONE OUT-OF-SAMPLE SHOT, no training: the CLI never runs the sweep — pick your
5837
- candidate elsewhere (a Simulator.run of your own), freeze its point and raw
5838
- author track record into the config, and fire it once here via Simulator.test.
5839
- Bans are re-derived from the frozen numbers under the point's rule; authors
5840
- unseen in the config are banned by default. The report carries the result with
5841
- the trade list and the frozen track record with re-derived ban flags.
5842
-
5843
- Module file ./modules/tune.module is loaded automatically if it exists
5515
+ Module file ./modules/sweep.module is loaded automatically if it exists
5844
5516
  (register your exchange there); without it CCXT Binance is used by default.
5845
5517
 
5846
5518
  Flush flags (--flush):
@@ -5876,8 +5548,7 @@ Module hooks (loaded automatically by each mode):
5876
5548
  modules/dump.module --dump Exchange schema for candle dumps
5877
5549
  modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
5878
5550
  modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
5879
- modules/simulator.module --simulator Exchange schema for the crowd-ideas feasibility probe
5880
- 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
5881
5552
 
5882
5553
  --flush has no associated module. It only removes dump subdirectories.
5883
5554
 
@@ -5906,10 +5577,8 @@ Examples:
5906
5577
  node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
5907
5578
  node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
5908
5579
  node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
5909
- node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5910
- node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_probe ./assets/tv-ideas.normalized.jsonl ./assets/probe.config.json
5911
- node ${ENTRY_PATH} --tune --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
5912
- node ${ENTRY_PATH} --tune --symbol BTCUSDT --markdown --output jun_2026_tune ./assets/tv-ideas.normalized.jsonl ./assets/tune.config.json
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
5913
5582
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
5914
5583
  node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
5915
5584
  node ${ENTRY_PATH} --init --output my-trading-bot
@@ -5923,7 +5592,7 @@ const main$1 = async () => {
5923
5592
  if (!values.help) {
5924
5593
  return;
5925
5594
  }
5926
- process.stdout.write(`@backtest-kit/cli ${"17.0.0"}\n\n`);
5595
+ process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n\n`);
5927
5596
  process.stdout.write(HELP_TEXT);
5928
5597
  process.exit(0);
5929
5598
  };
@@ -5937,7 +5606,7 @@ const main = async () => {
5937
5606
  if (!values.version) {
5938
5607
  return;
5939
5608
  }
5940
- process.stdout.write(`@backtest-kit/cli ${"17.0.0"}\n`);
5609
+ process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n`);
5941
5610
  process.exit(0);
5942
5611
  };
5943
5612
  main();