@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/README.md +13 -44
- package/build/index.cjs +117 -448
- package/build/index.mjs +121 -452
- 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,11 +866,7 @@ const getArgs = singleshot(() => {
|
|
|
866
866
|
type: "boolean",
|
|
867
867
|
default: false,
|
|
868
868
|
},
|
|
869
|
-
|
|
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", "
|
|
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$
|
|
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.
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
4316
|
+
console.log(await toMarkdown$1(signalId, plots, signalSchema));
|
|
4321
4317
|
process.exit(0);
|
|
4322
4318
|
};
|
|
4323
|
-
main$
|
|
4319
|
+
main$9();
|
|
4324
4320
|
|
|
4325
|
-
const main$
|
|
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$
|
|
4384
|
+
main$8();
|
|
4389
4385
|
|
|
4390
|
-
const main$
|
|
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$
|
|
4467
|
+
main$7();
|
|
4472
4468
|
|
|
4473
|
-
const main$
|
|
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$
|
|
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$
|
|
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$
|
|
4886
|
+
main$5();
|
|
4891
4887
|
|
|
4892
|
-
const SIMULATOR_NAME
|
|
4893
|
-
const CONFIG_KEYS
|
|
4894
|
-
const validateConfig
|
|
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
|
|
4900
|
-
return `unknown key "${key}" (expected: ${CONFIG_KEYS
|
|
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
|
|
4906
|
-
const validateIdea
|
|
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
|
|
4920
|
-
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)}`;
|
|
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
|
|
4928
|
-
|
|
4929
|
-
const
|
|
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(`#
|
|
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
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
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$
|
|
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.
|
|
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
|
|
5002
|
+
const problem = validateConfig(config);
|
|
5003
5003
|
if (problem) {
|
|
5004
|
-
console.error(`Error:
|
|
5005
|
-
console.error(`Expected shape: { "gridAxes"?:
|
|
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
|
|
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("
|
|
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
|
-
|
|
5083
|
-
|
|
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,
|
|
5103
|
+
onAuthorsTrained: (symbol, tracks) => {
|
|
5104
5104
|
if (values.verbose) {
|
|
5105
5105
|
console.log("onAuthorsTrained", {
|
|
5106
5106
|
symbol,
|
|
5107
|
-
authors:
|
|
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:
|
|
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:
|
|
5139
|
-
|
|
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
|
|
5143
|
+
const result = await Sweep.run({
|
|
5146
5144
|
symbol,
|
|
5147
|
-
|
|
5145
|
+
sweepName: SIMULATOR_NAME,
|
|
5148
5146
|
ideas,
|
|
5149
5147
|
});
|
|
5150
|
-
const dumpName = values.output || `
|
|
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
|
|
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
|
|
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
|
-
--
|
|
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
|
-
|
|
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:
|
|
5791
|
-
--json Save full
|
|
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
|
|
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"?:
|
|
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
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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} --
|
|
5910
|
-
node ${ENTRY_PATH} --
|
|
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.
|
|
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.
|
|
5609
|
+
process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n`);
|
|
5941
5610
|
process.exit(0);
|
|
5942
5611
|
};
|
|
5943
5612
|
main();
|