@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/README.md +14 -45
- package/build/index.cjs +152 -450
- package/build/index.mjs +156 -454
- package/docker/package.json +6 -6
- package/package.json +13 -13
- package/template/project/package.mustache +5 -5
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,
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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", "
|
|
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$
|
|
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 ${"
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
4316
|
+
console.log(await toMarkdown$1(signalId, plots, signalSchema));
|
|
4325
4317
|
process.exit(0);
|
|
4326
4318
|
};
|
|
4327
|
-
main$
|
|
4319
|
+
main$9();
|
|
4328
4320
|
|
|
4329
|
-
const main$
|
|
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$
|
|
4384
|
+
main$8();
|
|
4393
4385
|
|
|
4394
|
-
const main$
|
|
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$
|
|
4467
|
+
main$7();
|
|
4476
4468
|
|
|
4477
|
-
const main$
|
|
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$
|
|
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$
|
|
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$
|
|
4886
|
+
main$5();
|
|
4895
4887
|
|
|
4896
|
-
const SIMULATOR_NAME
|
|
4897
|
-
const
|
|
4898
|
-
const
|
|
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
|
|
4912
|
-
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS
|
|
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
|
|
4920
|
-
|
|
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(`#
|
|
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(`|
|
|
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(`##
|
|
4940
|
+
lines.push(`## Grid winners`);
|
|
4933
4941
|
lines.push("");
|
|
4934
|
-
lines.push(`| Criterion | Stop% |
|
|
4942
|
+
lines.push(`| Criterion | Stop% | Trail% | Hold | Lock% | Trades | PNL% | WinRate | Sharpe | Sortino |`);
|
|
4935
4943
|
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
|
|
4936
|
-
for (const best of
|
|
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.
|
|
4943
|
-
`${best.report.
|
|
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(
|
|
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
|
-
|
|
4951
|
-
|
|
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$
|
|
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.
|
|
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
|
|
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("
|
|
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
|
-
//
|
|
5044
|
-
|
|
5045
|
-
|
|
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,
|
|
5103
|
+
onAuthorsTrained: (symbol, tracks) => {
|
|
5084
5104
|
if (values.verbose) {
|
|
5085
5105
|
console.log("onAuthorsTrained", {
|
|
5086
5106
|
symbol,
|
|
5087
|
-
authors:
|
|
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:
|
|
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
|
-
|
|
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
|
|
5143
|
+
const result = await Sweep.run({
|
|
5127
5144
|
symbol,
|
|
5128
|
-
|
|
5145
|
+
sweepName: SIMULATOR_NAME,
|
|
5129
5146
|
ideas,
|
|
5130
5147
|
});
|
|
5131
|
-
const dumpName = values.output || `
|
|
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
|
|
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
|
|
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
|
-
--
|
|
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
|
-
|
|
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:
|
|
5763
|
-
--json Save full
|
|
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
|
|
5493
|
+
--verbose Log every sweep lifecycle callback to the console
|
|
5766
5494
|
|
|
5767
|
-
|
|
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 }
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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} --
|
|
5877
|
-
node ${ENTRY_PATH} --
|
|
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 ${"
|
|
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 ${"
|
|
5609
|
+
process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n`);
|
|
5908
5610
|
process.exit(0);
|
|
5909
5611
|
};
|
|
5910
5612
|
main();
|