@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.cjs
CHANGED
|
@@ -233,14 +233,14 @@ const getReadySubject = functoolsKit.singleshot(() => {
|
|
|
233
233
|
return new functoolsKit.Subject();
|
|
234
234
|
});
|
|
235
235
|
|
|
236
|
-
const main$
|
|
236
|
+
const main$k = () => {
|
|
237
237
|
const entrySubject = getEntrySubject();
|
|
238
238
|
entrySubject.subscribe(async (path) => {
|
|
239
239
|
console.log("Running", path);
|
|
240
240
|
await getReadySubject().next();
|
|
241
241
|
});
|
|
242
242
|
};
|
|
243
|
-
main$
|
|
243
|
+
main$k();
|
|
244
244
|
|
|
245
245
|
const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
|
|
246
246
|
function dumpStackTrace(error) {
|
|
@@ -891,11 +891,7 @@ const getArgs = functoolsKit.singleshot(() => {
|
|
|
891
891
|
type: "boolean",
|
|
892
892
|
default: false,
|
|
893
893
|
},
|
|
894
|
-
|
|
895
|
-
type: "boolean",
|
|
896
|
-
default: false,
|
|
897
|
-
},
|
|
898
|
-
tune: {
|
|
894
|
+
sweep: {
|
|
899
895
|
type: "boolean",
|
|
900
896
|
default: false,
|
|
901
897
|
},
|
|
@@ -3683,14 +3679,14 @@ const cli = {
|
|
|
3683
3679
|
};
|
|
3684
3680
|
init();
|
|
3685
3681
|
|
|
3686
|
-
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "
|
|
3682
|
+
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "sweep", "flush", "init", "docker", "help", "version"];
|
|
3687
3683
|
const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
|
|
3688
3684
|
const HELP_TEXT$1 = `
|
|
3689
3685
|
Example:
|
|
3690
3686
|
|
|
3691
3687
|
node ${ENTRY_PATH$1} --help
|
|
3692
3688
|
`.trimStart();
|
|
3693
|
-
const main$
|
|
3689
|
+
const main$j = async () => {
|
|
3694
3690
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3695
3691
|
return;
|
|
3696
3692
|
}
|
|
@@ -3698,14 +3694,14 @@ const main$k = async () => {
|
|
|
3698
3694
|
if (MODES.some((mode) => values[mode])) {
|
|
3699
3695
|
return;
|
|
3700
3696
|
}
|
|
3701
|
-
process.stdout.write(`@backtest-kit/cli ${"17.
|
|
3697
|
+
process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n`);
|
|
3702
3698
|
process.stdout.write("\n");
|
|
3703
3699
|
process.stdout.write(`Run with --help to see available commands.\n`);
|
|
3704
3700
|
process.stdout.write("\n");
|
|
3705
3701
|
process.stdout.write(HELP_TEXT$1);
|
|
3706
3702
|
process.exit(0);
|
|
3707
3703
|
};
|
|
3708
|
-
main$
|
|
3704
|
+
main$j();
|
|
3709
3705
|
|
|
3710
3706
|
const notifyShutdown = functoolsKit.singleshot(async () => {
|
|
3711
3707
|
console.log("Graceful shutdown initiated.");
|
|
@@ -3721,7 +3717,7 @@ const flush = async (entryPoint) => {
|
|
|
3721
3717
|
console.log(`Removed: ${target}`);
|
|
3722
3718
|
}
|
|
3723
3719
|
};
|
|
3724
|
-
const main$
|
|
3720
|
+
const main$i = async () => {
|
|
3725
3721
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3726
3722
|
return;
|
|
3727
3723
|
}
|
|
@@ -3738,7 +3734,7 @@ const main$j = async () => {
|
|
|
3738
3734
|
}
|
|
3739
3735
|
process.exit(0);
|
|
3740
3736
|
};
|
|
3741
|
-
main$
|
|
3737
|
+
main$i();
|
|
3742
3738
|
|
|
3743
3739
|
const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
|
|
3744
3740
|
process.off("SIGINT", BEFORE_EXIT_FN$6);
|
|
@@ -3761,7 +3757,7 @@ const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
|
|
|
3761
3757
|
const listenGracefulShutdown$6 = functoolsKit.singleshot(() => {
|
|
3762
3758
|
process.on("SIGINT", BEFORE_EXIT_FN$6);
|
|
3763
3759
|
});
|
|
3764
|
-
const main$
|
|
3760
|
+
const main$h = async () => {
|
|
3765
3761
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3766
3762
|
return;
|
|
3767
3763
|
}
|
|
@@ -3779,7 +3775,7 @@ const main$i = async () => {
|
|
|
3779
3775
|
await cli.backtestMainService.connect();
|
|
3780
3776
|
listenGracefulShutdown$6();
|
|
3781
3777
|
};
|
|
3782
|
-
main$
|
|
3778
|
+
main$h();
|
|
3783
3779
|
|
|
3784
3780
|
const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
|
|
3785
3781
|
process.off("SIGINT", BEFORE_EXIT_FN$5);
|
|
@@ -3798,7 +3794,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
|
|
|
3798
3794
|
const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
|
|
3799
3795
|
process.on("SIGINT", BEFORE_EXIT_FN$5);
|
|
3800
3796
|
});
|
|
3801
|
-
const main$
|
|
3797
|
+
const main$g = async () => {
|
|
3802
3798
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3803
3799
|
return;
|
|
3804
3800
|
}
|
|
@@ -3817,7 +3813,7 @@ const main$h = async () => {
|
|
|
3817
3813
|
listenGracefulShutdown$5();
|
|
3818
3814
|
await cli.walkerMainService.connect();
|
|
3819
3815
|
};
|
|
3820
|
-
main$
|
|
3816
|
+
main$g();
|
|
3821
3817
|
|
|
3822
3818
|
const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
3823
3819
|
process.off("SIGINT", BEFORE_EXIT_FN$4);
|
|
@@ -3839,7 +3835,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
|
3839
3835
|
const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
|
|
3840
3836
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
3841
3837
|
});
|
|
3842
|
-
const main$
|
|
3838
|
+
const main$f = async () => {
|
|
3843
3839
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3844
3840
|
return;
|
|
3845
3841
|
}
|
|
@@ -3853,7 +3849,7 @@ const main$g = async () => {
|
|
|
3853
3849
|
await cli.paperMainService.connect();
|
|
3854
3850
|
listenGracefulShutdown$4();
|
|
3855
3851
|
};
|
|
3856
|
-
main$
|
|
3852
|
+
main$f();
|
|
3857
3853
|
|
|
3858
3854
|
const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
3859
3855
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -3875,7 +3871,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
|
3875
3871
|
const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
|
|
3876
3872
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
3877
3873
|
});
|
|
3878
|
-
const main$
|
|
3874
|
+
const main$e = async () => {
|
|
3879
3875
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3880
3876
|
return;
|
|
3881
3877
|
}
|
|
@@ -3889,7 +3885,7 @@ const main$f = async () => {
|
|
|
3889
3885
|
await cli.liveMainService.connect();
|
|
3890
3886
|
listenGracefulShutdown$3();
|
|
3891
3887
|
};
|
|
3892
|
-
main$
|
|
3888
|
+
main$e();
|
|
3893
3889
|
|
|
3894
3890
|
const stopBacktestList$1 = async () => {
|
|
3895
3891
|
for (const item of await BacktestKit.Backtest.list()) {
|
|
@@ -3953,7 +3949,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
|
|
|
3953
3949
|
const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
|
|
3954
3950
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
3955
3951
|
});
|
|
3956
|
-
const main$
|
|
3952
|
+
const main$d = async () => {
|
|
3957
3953
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3958
3954
|
return;
|
|
3959
3955
|
}
|
|
@@ -4009,7 +4005,7 @@ const main$e = async () => {
|
|
|
4009
4005
|
listenGracefulShutdown$2();
|
|
4010
4006
|
await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
|
|
4011
4007
|
};
|
|
4012
|
-
main$
|
|
4008
|
+
main$d();
|
|
4013
4009
|
|
|
4014
4010
|
const MODE_MODULE = {
|
|
4015
4011
|
backtest: "backtest.module",
|
|
@@ -4112,7 +4108,7 @@ const createGracefulShutdown = (mode) => {
|
|
|
4112
4108
|
process.on("SIGINT", handler);
|
|
4113
4109
|
});
|
|
4114
4110
|
};
|
|
4115
|
-
const main$
|
|
4111
|
+
const main$c = async () => {
|
|
4116
4112
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
4117
4113
|
return;
|
|
4118
4114
|
}
|
|
@@ -4188,7 +4184,7 @@ const main$d = async () => {
|
|
|
4188
4184
|
}
|
|
4189
4185
|
await getEntrySubject().next(absolutePath);
|
|
4190
4186
|
};
|
|
4191
|
-
main$
|
|
4187
|
+
main$c();
|
|
4192
4188
|
|
|
4193
4189
|
const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
4194
4190
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -4199,7 +4195,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
|
4199
4195
|
const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
|
|
4200
4196
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
4201
4197
|
});
|
|
4202
|
-
const main$
|
|
4198
|
+
const main$b = async () => {
|
|
4203
4199
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
4204
4200
|
return;
|
|
4205
4201
|
}
|
|
@@ -4209,7 +4205,7 @@ const main$c = async () => {
|
|
|
4209
4205
|
}
|
|
4210
4206
|
listenGracefulShutdown$1();
|
|
4211
4207
|
};
|
|
4212
|
-
main$
|
|
4208
|
+
main$b();
|
|
4213
4209
|
|
|
4214
4210
|
const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
4215
4211
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -4220,7 +4216,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
|
4220
4216
|
const listenGracefulShutdown = functoolsKit.singleshot(() => {
|
|
4221
4217
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
4222
4218
|
});
|
|
4223
|
-
const main$
|
|
4219
|
+
const main$a = async () => {
|
|
4224
4220
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
4225
4221
|
return;
|
|
4226
4222
|
}
|
|
@@ -4230,7 +4226,7 @@ const main$b = async () => {
|
|
|
4230
4226
|
}
|
|
4231
4227
|
listenGracefulShutdown();
|
|
4232
4228
|
};
|
|
4233
|
-
main$
|
|
4229
|
+
main$a();
|
|
4234
4230
|
|
|
4235
4231
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
4236
4232
|
const keys = Object.keys(schema);
|
|
@@ -4252,7 +4248,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
4252
4248
|
}
|
|
4253
4249
|
return rows;
|
|
4254
4250
|
};
|
|
4255
|
-
const main$
|
|
4251
|
+
const main$9 = async () => {
|
|
4256
4252
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
4257
4253
|
return;
|
|
4258
4254
|
}
|
|
@@ -4352,9 +4348,9 @@ const main$a = async () => {
|
|
|
4352
4348
|
console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
|
|
4353
4349
|
process.exit(0);
|
|
4354
4350
|
};
|
|
4355
|
-
main$
|
|
4351
|
+
main$9();
|
|
4356
4352
|
|
|
4357
|
-
const main$
|
|
4353
|
+
const main$8 = async () => {
|
|
4358
4354
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
4359
4355
|
return;
|
|
4360
4356
|
}
|
|
@@ -4417,9 +4413,9 @@ const main$9 = async () => {
|
|
|
4417
4413
|
};
|
|
4418
4414
|
process.on("SIGINT", beforeExit);
|
|
4419
4415
|
};
|
|
4420
|
-
main$
|
|
4416
|
+
main$8();
|
|
4421
4417
|
|
|
4422
|
-
const main$
|
|
4418
|
+
const main$7 = async () => {
|
|
4423
4419
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
4424
4420
|
return;
|
|
4425
4421
|
}
|
|
@@ -4500,9 +4496,9 @@ const main$8 = async () => {
|
|
|
4500
4496
|
console.log(JSON.stringify(candles, null, 2));
|
|
4501
4497
|
process.exit(0);
|
|
4502
4498
|
};
|
|
4503
|
-
main$
|
|
4499
|
+
main$7();
|
|
4504
4500
|
|
|
4505
|
-
const main$
|
|
4501
|
+
const main$6 = async () => {
|
|
4506
4502
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
4507
4503
|
return;
|
|
4508
4504
|
}
|
|
@@ -4616,7 +4612,7 @@ const main$7 = async () => {
|
|
|
4616
4612
|
}
|
|
4617
4613
|
process.exit(0);
|
|
4618
4614
|
};
|
|
4619
|
-
main$
|
|
4615
|
+
main$6();
|
|
4620
4616
|
|
|
4621
4617
|
const getBroker = functoolsKit.singleshot(() => {
|
|
4622
4618
|
// Reaches into BrokerAdapter internals on purpose: the public commit*
|
|
@@ -4725,7 +4721,7 @@ const ZERO_PNL = {
|
|
|
4725
4721
|
pnlCost: 0,
|
|
4726
4722
|
pnlEntries: 0,
|
|
4727
4723
|
};
|
|
4728
|
-
const main$
|
|
4724
|
+
const main$5 = async () => {
|
|
4729
4725
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
4730
4726
|
return;
|
|
4731
4727
|
}
|
|
@@ -4919,23 +4915,23 @@ const main$6 = async () => {
|
|
|
4919
4915
|
}
|
|
4920
4916
|
process.exit(0);
|
|
4921
4917
|
};
|
|
4922
|
-
main$
|
|
4918
|
+
main$5();
|
|
4923
4919
|
|
|
4924
|
-
const SIMULATOR_NAME
|
|
4925
|
-
const CONFIG_KEYS
|
|
4926
|
-
const validateConfig
|
|
4920
|
+
const SIMULATOR_NAME = "cli_sweep";
|
|
4921
|
+
const CONFIG_KEYS = ["gridAxes", "reportOrder"];
|
|
4922
|
+
const validateConfig = (config) => {
|
|
4927
4923
|
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
4928
4924
|
return "config must be a JSON object";
|
|
4929
4925
|
}
|
|
4930
4926
|
for (const key of Object.keys(config)) {
|
|
4931
|
-
if (!CONFIG_KEYS
|
|
4932
|
-
return `unknown key "${key}" (expected: ${CONFIG_KEYS
|
|
4927
|
+
if (!CONFIG_KEYS.includes(key)) {
|
|
4928
|
+
return `unknown key "${key}" (expected: ${CONFIG_KEYS.join(", ")})`;
|
|
4933
4929
|
}
|
|
4934
4930
|
}
|
|
4935
4931
|
return null;
|
|
4936
4932
|
};
|
|
4937
|
-
const IDEA_DIRECTIONS
|
|
4938
|
-
const validateIdea
|
|
4933
|
+
const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
|
|
4934
|
+
const validateIdea = (idea, line) => {
|
|
4939
4935
|
if (typeof idea !== "object" || idea === null) {
|
|
4940
4936
|
return `line ${line}: not an object`;
|
|
4941
4937
|
}
|
|
@@ -4948,20 +4944,21 @@ const validateIdea$1 = (idea, line) => {
|
|
|
4948
4944
|
if (typeof idea.symbol !== "string" || !idea.symbol) {
|
|
4949
4945
|
return `line ${line}: "symbol" must be a non-empty string`;
|
|
4950
4946
|
}
|
|
4951
|
-
if (!IDEA_DIRECTIONS
|
|
4952
|
-
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS
|
|
4947
|
+
if (!IDEA_DIRECTIONS.includes(idea.direction)) {
|
|
4948
|
+
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
4953
4949
|
}
|
|
4954
4950
|
if (typeof idea.author !== "string" || !idea.author) {
|
|
4955
4951
|
return `line ${line}: "author" must be a non-empty string`;
|
|
4956
4952
|
}
|
|
4957
4953
|
return null;
|
|
4958
4954
|
};
|
|
4959
|
-
const
|
|
4960
|
-
|
|
4961
|
-
const
|
|
4955
|
+
const fmtSimRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
|
|
4956
|
+
const toMarkdown = (result) => {
|
|
4957
|
+
const bucket = result.reports;
|
|
4958
|
+
const allReports = bucket.reports;
|
|
4962
4959
|
const profitable = allReports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
|
|
4963
4960
|
const lines = [];
|
|
4964
|
-
lines.push(`#
|
|
4961
|
+
lines.push(`# Sweep Report — ${result.symbol}`);
|
|
4965
4962
|
lines.push("");
|
|
4966
4963
|
lines.push(`| Metric | Value |`);
|
|
4967
4964
|
lines.push(`| --- | --- |`);
|
|
@@ -4969,41 +4966,44 @@ const toMarkdown$1 = (result) => {
|
|
|
4969
4966
|
lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
|
|
4970
4967
|
lines.push(`| Profitable corridor | ${profitable} / ${allReports.length} grid points |`);
|
|
4971
4968
|
lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
|
|
4972
|
-
//
|
|
4973
|
-
//
|
|
4974
|
-
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
continue;
|
|
4984
|
-
}
|
|
4985
|
-
const { point } = best.report;
|
|
4986
|
-
lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.holdMinutes / 60}h | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
|
|
4987
|
-
`${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
|
|
4988
|
-
}
|
|
4989
|
-
const sharpeBest = bucket.best.find(({ criterion }) => criterion === "sharpe");
|
|
4990
|
-
lines.push("");
|
|
4991
|
-
lines.push(`### Allowed authors (sharpe winner rule)`);
|
|
4992
|
-
lines.push("");
|
|
4993
|
-
lines.push(`| Author | Ideas | Hits | HitRate |`);
|
|
4994
|
-
lines.push(`| --- | --- | --- | --- |`);
|
|
4995
|
-
for (const stat of (sharpeBest?.authorStats ?? []).filter(({ banned }) => !banned)) {
|
|
4996
|
-
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
|
|
4969
|
+
// единственная метрика — profit-before-stop: одна корзина с
|
|
4970
|
+
// победителями и author tracks
|
|
4971
|
+
lines.push("");
|
|
4972
|
+
lines.push(`## Grid winners`);
|
|
4973
|
+
lines.push("");
|
|
4974
|
+
lines.push(`| Criterion | Stop% | Trail% | Hold | Lock% | Trades | PNL% | WinRate | Sharpe | Sortino |`);
|
|
4975
|
+
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
|
|
4976
|
+
for (const best of bucket.best) {
|
|
4977
|
+
if (!best.report) {
|
|
4978
|
+
lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
|
|
4979
|
+
continue;
|
|
4997
4980
|
}
|
|
4981
|
+
const { point } = best.report;
|
|
4982
|
+
lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.trailingTakePercent} | ${point.holdMinutes / 60}h | ${point.profitLockPercent} | ` +
|
|
4983
|
+
`${best.report.tradesList.length} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${fmtSimRatio(best.report.sharpe)} | ${fmtSimRatio(best.report.sortino)} |`);
|
|
4984
|
+
}
|
|
4985
|
+
// сырой author track: userspace решает, кому верить (порога/бана
|
|
4986
|
+
// нет). Топ по hitRate
|
|
4987
|
+
const tracks = [...bucket.tracks]
|
|
4988
|
+
.filter(({ ideas }) => ideas > 0)
|
|
4989
|
+
.sort((a, b) => b.hitRate - a.hitRate || b.ideas - a.ideas)
|
|
4990
|
+
.slice(0, 20);
|
|
4991
|
+
lines.push("");
|
|
4992
|
+
lines.push(`### Author tracks — raw ideas/hits/hitRate per rule (top ${tracks.length} by hitRate; full set in --json)`);
|
|
4993
|
+
lines.push("");
|
|
4994
|
+
lines.push(`| Author | Hold | Lock% | Stop% | Trail% | Ideas | Hits | HitRate |`);
|
|
4995
|
+
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- |`);
|
|
4996
|
+
for (const t of tracks) {
|
|
4997
|
+
lines.push(`| ${t.author} | ${t.holdMinutes / 60}h | ${t.profitLockPercent} | ${t.hardStopPercent} | ${t.trailingTakePercent} | ${t.ideas} | ${t.hits} | ${(t.hitRate * 100).toFixed(0)}% |`);
|
|
4998
4998
|
}
|
|
4999
4999
|
return lines.join("\n");
|
|
5000
5000
|
};
|
|
5001
|
-
const main$
|
|
5001
|
+
const main$4 = async () => {
|
|
5002
5002
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
5003
5003
|
return;
|
|
5004
5004
|
}
|
|
5005
5005
|
const { values, positionals } = getArgs();
|
|
5006
|
-
if (!values.
|
|
5006
|
+
if (!values.sweep) {
|
|
5007
5007
|
return;
|
|
5008
5008
|
}
|
|
5009
5009
|
const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
|
|
@@ -5031,10 +5031,10 @@ const main$5 = async () => {
|
|
|
5031
5031
|
console.error(`Error: invalid JSON in config file: ${configPath}`);
|
|
5032
5032
|
process.exit(1);
|
|
5033
5033
|
}
|
|
5034
|
-
const problem = validateConfig
|
|
5034
|
+
const problem = validateConfig(config);
|
|
5035
5035
|
if (problem) {
|
|
5036
|
-
console.error(`Error:
|
|
5037
|
-
console.error(`Expected shape: { "gridAxes"?:
|
|
5036
|
+
console.error(`Error: sweep config does not match the structure — ${problem}`);
|
|
5037
|
+
console.error(`Expected shape: { "gridAxes"?: ISweepGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }`);
|
|
5038
5038
|
process.exit(1);
|
|
5039
5039
|
}
|
|
5040
5040
|
}
|
|
@@ -5062,7 +5062,7 @@ const main$5 = async () => {
|
|
|
5062
5062
|
console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
|
|
5063
5063
|
process.exit(1);
|
|
5064
5064
|
}
|
|
5065
|
-
const problem = validateIdea
|
|
5065
|
+
const problem = validateIdea(idea, i + 1);
|
|
5066
5066
|
if (problem) {
|
|
5067
5067
|
console.error(`Error: ideas file does not match the idea structure — ${problem}`);
|
|
5068
5068
|
console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
|
|
@@ -5091,7 +5091,7 @@ const main$5 = async () => {
|
|
|
5091
5091
|
process.exit(-1);
|
|
5092
5092
|
}
|
|
5093
5093
|
}
|
|
5094
|
-
await cli.moduleConnectionService.loadModule("
|
|
5094
|
+
await cli.moduleConnectionService.loadModule("sweep.module");
|
|
5095
5095
|
{
|
|
5096
5096
|
await cli.exchangeSchemaService.addSchema();
|
|
5097
5097
|
await cli.symbolSchemaService.addSchema();
|
|
@@ -5111,8 +5111,8 @@ const main$5 = async () => {
|
|
|
5111
5111
|
const symbol = values.symbol || "BTCUSDT";
|
|
5112
5112
|
// оси и порядок отчёта приходят позиционным конфигом потребителя;
|
|
5113
5113
|
// пустой конфиг — дефолтная сетка движка из connection-сервиса
|
|
5114
|
-
BacktestKit.
|
|
5115
|
-
|
|
5114
|
+
BacktestKit.addSweepSchema({
|
|
5115
|
+
sweepName: SIMULATOR_NAME,
|
|
5116
5116
|
exchangeName,
|
|
5117
5117
|
...(config.gridAxes ? { gridAxes: config.gridAxes } : {}),
|
|
5118
5118
|
...(config.reportOrder ? { reportOrder: config.reportOrder } : {}),
|
|
@@ -5132,22 +5132,20 @@ const main$5 = async () => {
|
|
|
5132
5132
|
console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
|
|
5133
5133
|
}
|
|
5134
5134
|
},
|
|
5135
|
-
onAuthorsTrained: (symbol,
|
|
5135
|
+
onAuthorsTrained: (symbol, tracks) => {
|
|
5136
5136
|
if (values.verbose) {
|
|
5137
5137
|
console.log("onAuthorsTrained", {
|
|
5138
5138
|
symbol,
|
|
5139
|
-
authors:
|
|
5140
|
-
banned: stats.filter(({ banned }) => banned).length,
|
|
5141
|
-
bannedIdeas,
|
|
5139
|
+
authors: tracks.length,
|
|
5142
5140
|
});
|
|
5143
5141
|
}
|
|
5144
5142
|
},
|
|
5145
|
-
onGridPoint: (symbol, report) => {
|
|
5143
|
+
onGridPoint: (symbol, report, trades) => {
|
|
5146
5144
|
if (values.verbose) {
|
|
5147
5145
|
console.log("onGridPoint", {
|
|
5148
5146
|
symbol,
|
|
5149
5147
|
point: report.point,
|
|
5150
|
-
trades:
|
|
5148
|
+
trades: trades.length,
|
|
5151
5149
|
pnl: +report.totalPnlPercent.toFixed(2),
|
|
5152
5150
|
sharpe: +report.sharpe.toFixed(2),
|
|
5153
5151
|
});
|
|
@@ -5167,19 +5165,19 @@ const main$5 = async () => {
|
|
|
5167
5165
|
if (values.verbose) {
|
|
5168
5166
|
console.log("onDone", {
|
|
5169
5167
|
symbol,
|
|
5170
|
-
reports:
|
|
5171
|
-
|
|
5168
|
+
reports: result.reports.reports.length,
|
|
5169
|
+
tracks: result.reports.tracks.length,
|
|
5172
5170
|
});
|
|
5173
5171
|
}
|
|
5174
5172
|
},
|
|
5175
5173
|
},
|
|
5176
5174
|
});
|
|
5177
|
-
const result = await BacktestKit.
|
|
5175
|
+
const result = await BacktestKit.Sweep.run({
|
|
5178
5176
|
symbol,
|
|
5179
|
-
|
|
5177
|
+
sweepName: SIMULATOR_NAME,
|
|
5180
5178
|
ideas,
|
|
5181
5179
|
});
|
|
5182
|
-
const dumpName = values.output || `
|
|
5180
|
+
const dumpName = values.output || `sweep_${symbol}_${Date.now()}`;
|
|
5183
5181
|
const dumpDir = path.join(process.cwd(), "dump");
|
|
5184
5182
|
if (values.json) {
|
|
5185
5183
|
const filePath = path.resolve(dumpDir, `${dumpName}.json`);
|
|
@@ -5191,308 +5189,11 @@ const main$5 = async () => {
|
|
|
5191
5189
|
if (values.markdown) {
|
|
5192
5190
|
const filePath = path.resolve(dumpDir, `${dumpName}.md`);
|
|
5193
5191
|
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
5194
|
-
await fs$1.writeFile(filePath, toMarkdown
|
|
5192
|
+
await fs$1.writeFile(filePath, toMarkdown(result), "utf-8");
|
|
5195
5193
|
console.log(`Saved: ${filePath}`);
|
|
5196
5194
|
process.exit(0);
|
|
5197
5195
|
}
|
|
5198
|
-
console.log(toMarkdown
|
|
5199
|
-
process.exit(0);
|
|
5200
|
-
};
|
|
5201
|
-
main$5();
|
|
5202
|
-
|
|
5203
|
-
const SIMULATOR_NAME = "cli_tune";
|
|
5204
|
-
const CONFIG_KEYS = ["point", "authorStats", "gridAxes", "reportOrder"];
|
|
5205
|
-
const POINT_NUMBER_FIELDS = [
|
|
5206
|
-
"hardStopPercent",
|
|
5207
|
-
"trailingTakePercent",
|
|
5208
|
-
"holdMinutes",
|
|
5209
|
-
"minAuthorTrack",
|
|
5210
|
-
"minAuthorHitRate",
|
|
5211
|
-
"profitLockPercent",
|
|
5212
|
-
];
|
|
5213
|
-
const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
|
|
5214
|
-
const validateIdea = (idea, line) => {
|
|
5215
|
-
if (typeof idea !== "object" || idea === null) {
|
|
5216
|
-
return `line ${line}: not an object`;
|
|
5217
|
-
}
|
|
5218
|
-
if (typeof idea.id !== "number") {
|
|
5219
|
-
return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
|
|
5220
|
-
}
|
|
5221
|
-
if (typeof idea.ts !== "number") {
|
|
5222
|
-
return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
|
|
5223
|
-
}
|
|
5224
|
-
if (typeof idea.symbol !== "string" || !idea.symbol) {
|
|
5225
|
-
return `line ${line}: "symbol" must be a non-empty string`;
|
|
5226
|
-
}
|
|
5227
|
-
if (!IDEA_DIRECTIONS.includes(idea.direction)) {
|
|
5228
|
-
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
5229
|
-
}
|
|
5230
|
-
if (typeof idea.author !== "string" || !idea.author) {
|
|
5231
|
-
return `line ${line}: "author" must be a non-empty string`;
|
|
5232
|
-
}
|
|
5233
|
-
return null;
|
|
5234
|
-
};
|
|
5235
|
-
const validateConfig = (config) => {
|
|
5236
|
-
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
5237
|
-
return "config must be a JSON object";
|
|
5238
|
-
}
|
|
5239
|
-
for (const key of Object.keys(config)) {
|
|
5240
|
-
if (!CONFIG_KEYS.includes(key)) {
|
|
5241
|
-
return `unknown key "${key}" (expected: ${CONFIG_KEYS.join(", ")})`;
|
|
5242
|
-
}
|
|
5243
|
-
}
|
|
5244
|
-
if (typeof config.point !== "object" || config.point === null) {
|
|
5245
|
-
return `"point" is required — the frozen grid point of a training run`;
|
|
5246
|
-
}
|
|
5247
|
-
for (const field of POINT_NUMBER_FIELDS) {
|
|
5248
|
-
if (typeof config.point[field] !== "number") {
|
|
5249
|
-
return `"point.${field}" must be a number, got ${typeof config.point[field]}`;
|
|
5250
|
-
}
|
|
5251
|
-
}
|
|
5252
|
-
if (typeof config.point.authorMetric !== "string") {
|
|
5253
|
-
return `"point.authorMetric" must be a string, got ${typeof config.point.authorMetric}`;
|
|
5254
|
-
}
|
|
5255
|
-
if (!Array.isArray(config.authorStats) || !config.authorStats.length) {
|
|
5256
|
-
return `"authorStats" is required — the frozen raw track record (non-empty array)`;
|
|
5257
|
-
}
|
|
5258
|
-
for (let i = 0; i < config.authorStats.length; i++) {
|
|
5259
|
-
const stat = config.authorStats[i];
|
|
5260
|
-
if (typeof stat !== "object" ||
|
|
5261
|
-
stat === null ||
|
|
5262
|
-
typeof stat.author !== "string" ||
|
|
5263
|
-
typeof stat.ideas !== "number" ||
|
|
5264
|
-
typeof stat.hits !== "number") {
|
|
5265
|
-
return `"authorStats[${i}]" must be { author: string, ideas: number, hits: number }`;
|
|
5266
|
-
}
|
|
5267
|
-
}
|
|
5268
|
-
return null;
|
|
5269
|
-
};
|
|
5270
|
-
/** Одноточечные оси, зеркалящие замороженную точку — сетка инертна. */
|
|
5271
|
-
const axesFromPoint = (point) => ({
|
|
5272
|
-
hardStopPercent: [point.hardStopPercent],
|
|
5273
|
-
trailingTakePercent: [point.trailingTakePercent],
|
|
5274
|
-
holdMinutes: [point.holdMinutes],
|
|
5275
|
-
minAuthorTrack: [point.minAuthorTrack],
|
|
5276
|
-
minAuthorHitRate: [point.minAuthorHitRate],
|
|
5277
|
-
profitLockPercent: [point.profitLockPercent],
|
|
5278
|
-
authorMetric: [point.authorMetric],
|
|
5279
|
-
});
|
|
5280
|
-
const pointLabel = (point) => `H=${point.hardStopPercent} TT=${point.trailingTakePercent} hold=${point.holdMinutes / 60}h ` +
|
|
5281
|
-
`track=${point.minAuthorTrack} rate=${point.minAuthorHitRate} ` +
|
|
5282
|
-
`lock=${point.profitLockPercent} metric=${point.authorMetric}`;
|
|
5283
|
-
const fmtRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
|
|
5284
|
-
const toMarkdown = (test) => {
|
|
5285
|
-
const lines = [];
|
|
5286
|
-
lines.push(`# Tune Report — ${test.symbol}`);
|
|
5287
|
-
lines.push("");
|
|
5288
|
-
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.`);
|
|
5289
|
-
lines.push("");
|
|
5290
|
-
lines.push(`Frozen point: ${pointLabel(test.point)}`);
|
|
5291
|
-
lines.push("");
|
|
5292
|
-
lines.push(`## Out-of-sample (${test.ideasDirectional} directional ideas)`);
|
|
5293
|
-
lines.push("");
|
|
5294
|
-
lines.push(`| Metric | Value |`);
|
|
5295
|
-
lines.push(`| --- | --- |`);
|
|
5296
|
-
lines.push(`| Trades (skipped busy) | ${test.report.trades} (${test.report.skippedBusy}) |`);
|
|
5297
|
-
lines.push(`| PNL | ${test.report.totalPnlPercent.toFixed(2)}% |`);
|
|
5298
|
-
lines.push(`| Win rate | ${(test.report.winRate * 100).toFixed(0)}% |`);
|
|
5299
|
-
lines.push(`| Profit factor | ${fmtRatio(test.report.profitFactor)} |`);
|
|
5300
|
-
lines.push(`| Max series drawdown | ${test.report.maxSeriesDrawdownPercent.toFixed(2)}% |`);
|
|
5301
|
-
lines.push(`| Sharpe / Sortino | ${fmtRatio(test.report.sharpe)} / ${fmtRatio(test.report.sortino)} |`);
|
|
5302
|
-
lines.push(`| Recovery factor | ${fmtRatio(test.report.recoveryFactor)} |`);
|
|
5303
|
-
lines.push(`| Exits | ${Object.entries(test.report.exitReasons).map(([reason, count]) => `${reason}=${count}`).join(", ")} |`);
|
|
5304
|
-
lines.push("");
|
|
5305
|
-
lines.push(`## Test trades`);
|
|
5306
|
-
lines.push("");
|
|
5307
|
-
lines.push(`| Direction | Exit | PNL% | Hold | Entry (UTC) |`);
|
|
5308
|
-
lines.push(`| --- | --- | --- | --- | --- |`);
|
|
5309
|
-
for (const trade of test.trades) {
|
|
5310
|
-
lines.push(`| ${trade.direction} | ${trade.exitReason} | ${trade.pnlPercent.toFixed(2)} | ${trade.holdMinutesActual}m | ${new Date(trade.entryTimestamp).toISOString()} |`);
|
|
5311
|
-
}
|
|
5312
|
-
lines.push("");
|
|
5313
|
-
lines.push(`## Frozen author track record (re-derived bans under the point's rule)`);
|
|
5314
|
-
lines.push("");
|
|
5315
|
-
lines.push(`| Author | Ideas | Hits | Banned |`);
|
|
5316
|
-
lines.push(`| --- | --- | --- | --- |`);
|
|
5317
|
-
for (const stat of test.authorStats) {
|
|
5318
|
-
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${stat.banned ? "yes" : ""} |`);
|
|
5319
|
-
}
|
|
5320
|
-
return lines.join("\n");
|
|
5321
|
-
};
|
|
5322
|
-
const main$4 = async () => {
|
|
5323
|
-
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
5324
|
-
return;
|
|
5325
|
-
}
|
|
5326
|
-
const { values, positionals } = getArgs();
|
|
5327
|
-
if (!values.tune) {
|
|
5328
|
-
return;
|
|
5329
|
-
}
|
|
5330
|
-
const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
|
|
5331
|
-
if (!ideasPath) {
|
|
5332
|
-
console.error("Error: positional path to an ideas .jsonl file is required");
|
|
5333
|
-
process.exit(1);
|
|
5334
|
-
}
|
|
5335
|
-
// конфиг — позиционный JSON; без него подтягивается пустой объект
|
|
5336
|
-
// (и валидация честно скажет, что замороженной точки нет)
|
|
5337
|
-
const [configPath = null] = positionals.filter((value) => value.endsWith(".json") && !value.endsWith(".jsonl"));
|
|
5338
|
-
let config = {};
|
|
5339
|
-
if (configPath) {
|
|
5340
|
-
let content;
|
|
5341
|
-
try {
|
|
5342
|
-
content = await fs$1.readFile(path.resolve(configPath), "utf-8");
|
|
5343
|
-
}
|
|
5344
|
-
catch (error) {
|
|
5345
|
-
console.error(`Error: cannot read config file: ${configPath}`);
|
|
5346
|
-
process.exit(1);
|
|
5347
|
-
}
|
|
5348
|
-
try {
|
|
5349
|
-
config = JSON.parse(content);
|
|
5350
|
-
}
|
|
5351
|
-
catch {
|
|
5352
|
-
console.error(`Error: invalid JSON in config file: ${configPath}`);
|
|
5353
|
-
process.exit(1);
|
|
5354
|
-
}
|
|
5355
|
-
}
|
|
5356
|
-
{
|
|
5357
|
-
const problem = validateConfig(config);
|
|
5358
|
-
if (problem) {
|
|
5359
|
-
console.error(`Error: tune config does not match the structure — ${problem}`);
|
|
5360
|
-
console.error(`Expected shape: { "point": ISimulatorGridPoint, "authorStats": [{ "author", "ideas", "hits" }], "gridAxes"?, "reportOrder"? }`);
|
|
5361
|
-
process.exit(1);
|
|
5362
|
-
}
|
|
5363
|
-
}
|
|
5364
|
-
let ideas = [];
|
|
5365
|
-
{
|
|
5366
|
-
let content;
|
|
5367
|
-
try {
|
|
5368
|
-
content = await fs$1.readFile(path.resolve(ideasPath), "utf-8");
|
|
5369
|
-
}
|
|
5370
|
-
catch (error) {
|
|
5371
|
-
console.error(`Error: cannot read ideas file: ${ideasPath}`);
|
|
5372
|
-
process.exit(1);
|
|
5373
|
-
}
|
|
5374
|
-
const rows = content.split("\n").filter(Boolean);
|
|
5375
|
-
if (!rows.length) {
|
|
5376
|
-
console.error(`Error: ideas file is empty: ${ideasPath}`);
|
|
5377
|
-
process.exit(1);
|
|
5378
|
-
}
|
|
5379
|
-
for (let i = 0; i < rows.length; i++) {
|
|
5380
|
-
let idea;
|
|
5381
|
-
try {
|
|
5382
|
-
idea = JSON.parse(rows[i]);
|
|
5383
|
-
}
|
|
5384
|
-
catch {
|
|
5385
|
-
console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
|
|
5386
|
-
process.exit(1);
|
|
5387
|
-
}
|
|
5388
|
-
const problem = validateIdea(idea, i + 1);
|
|
5389
|
-
if (problem) {
|
|
5390
|
-
console.error(`Error: ideas file does not match the idea structure — ${problem}`);
|
|
5391
|
-
console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
|
|
5392
|
-
process.exit(1);
|
|
5393
|
-
}
|
|
5394
|
-
ideas.push(idea);
|
|
5395
|
-
}
|
|
5396
|
-
}
|
|
5397
|
-
{
|
|
5398
|
-
const cwd = process.cwd();
|
|
5399
|
-
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
5400
|
-
}
|
|
5401
|
-
await cli.configConnectionService.loadConfig("setup.config");
|
|
5402
|
-
{
|
|
5403
|
-
const loader = await cli.configConnectionService.loadConfig("loader.config");
|
|
5404
|
-
try {
|
|
5405
|
-
if (typeof loader === "function") {
|
|
5406
|
-
await loader();
|
|
5407
|
-
}
|
|
5408
|
-
if (typeof loader?.loader === "function") {
|
|
5409
|
-
await loader.loader();
|
|
5410
|
-
}
|
|
5411
|
-
}
|
|
5412
|
-
catch (error) {
|
|
5413
|
-
console.error("Module loader failed", error);
|
|
5414
|
-
process.exit(-1);
|
|
5415
|
-
}
|
|
5416
|
-
}
|
|
5417
|
-
await cli.moduleConnectionService.loadModule("tune.module");
|
|
5418
|
-
{
|
|
5419
|
-
await cli.exchangeSchemaService.addSchema();
|
|
5420
|
-
await cli.symbolSchemaService.addSchema();
|
|
5421
|
-
}
|
|
5422
|
-
const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
|
|
5423
|
-
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
5424
|
-
if (values.verbose) {
|
|
5425
|
-
BacktestKit.overrideExchangeSchema({
|
|
5426
|
-
exchangeName,
|
|
5427
|
-
callbacks: {
|
|
5428
|
-
onCandleData(symbol, interval, since) {
|
|
5429
|
-
console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
|
|
5430
|
-
},
|
|
5431
|
-
},
|
|
5432
|
-
});
|
|
5433
|
-
}
|
|
5434
|
-
const symbol = values.symbol || "BTCUSDT";
|
|
5435
|
-
// сетка для test() инертна (оценивается ровно замороженная точка):
|
|
5436
|
-
// без явных осей в конфиге они одноточечно зеркалят точку
|
|
5437
|
-
BacktestKit.addSimulatorSchema({
|
|
5438
|
-
simulatorName: SIMULATOR_NAME,
|
|
5439
|
-
exchangeName,
|
|
5440
|
-
gridAxes: config.gridAxes ?? axesFromPoint(config.point),
|
|
5441
|
-
...(config.reportOrder ? { reportOrder: config.reportOrder } : {}),
|
|
5442
|
-
callbacks: {
|
|
5443
|
-
onProgress: (symbol, stage, processed, total) => {
|
|
5444
|
-
if (values.verbose) {
|
|
5445
|
-
console.log("onProgress", { symbol, stage, processed, total });
|
|
5446
|
-
}
|
|
5447
|
-
},
|
|
5448
|
-
onIdeas: (symbol, ideasTotal, ideasDirectional) => {
|
|
5449
|
-
if (values.verbose) {
|
|
5450
|
-
console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
|
|
5451
|
-
}
|
|
5452
|
-
},
|
|
5453
|
-
onProfiles: (symbol, profiles, truncatedCount) => {
|
|
5454
|
-
if (values.verbose) {
|
|
5455
|
-
console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
|
|
5456
|
-
}
|
|
5457
|
-
},
|
|
5458
|
-
onTestDone: (symbol, result) => {
|
|
5459
|
-
if (values.verbose) {
|
|
5460
|
-
console.log("onTestDone", {
|
|
5461
|
-
symbol,
|
|
5462
|
-
trades: result.report.trades,
|
|
5463
|
-
pnl: +result.report.totalPnlPercent.toFixed(2),
|
|
5464
|
-
});
|
|
5465
|
-
}
|
|
5466
|
-
},
|
|
5467
|
-
},
|
|
5468
|
-
});
|
|
5469
|
-
// только out-of-sample: обучения нет, точка и трек-рекорд приходят
|
|
5470
|
-
// из конфига verbatim — test() перевыводит баны под правило точки,
|
|
5471
|
-
// невиданные авторы забанены по умолчанию
|
|
5472
|
-
const test = await BacktestKit.Simulator.test({
|
|
5473
|
-
symbol,
|
|
5474
|
-
simulatorName: SIMULATOR_NAME,
|
|
5475
|
-
ideas,
|
|
5476
|
-
point: config.point,
|
|
5477
|
-
authorStats: config.authorStats,
|
|
5478
|
-
});
|
|
5479
|
-
const dumpName = values.output || `tune_${symbol}_${Date.now()}`;
|
|
5480
|
-
const dumpDir = path.join(process.cwd(), "dump");
|
|
5481
|
-
if (values.json) {
|
|
5482
|
-
const filePath = path.resolve(dumpDir, `${dumpName}.json`);
|
|
5483
|
-
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
5484
|
-
await fs$1.writeFile(filePath, JSON.stringify(test, null, 2), "utf-8");
|
|
5485
|
-
console.log(`Saved: ${filePath}`);
|
|
5486
|
-
process.exit(0);
|
|
5487
|
-
}
|
|
5488
|
-
if (values.markdown) {
|
|
5489
|
-
const filePath = path.resolve(dumpDir, `${dumpName}.md`);
|
|
5490
|
-
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
5491
|
-
await fs$1.writeFile(filePath, toMarkdown(test), "utf-8");
|
|
5492
|
-
console.log(`Saved: ${filePath}`);
|
|
5493
|
-
process.exit(0);
|
|
5494
|
-
}
|
|
5495
|
-
console.log(toMarkdown(test));
|
|
5196
|
+
console.log(toMarkdown(result));
|
|
5496
5197
|
process.exit(0);
|
|
5497
5198
|
};
|
|
5498
5199
|
main$4();
|
|
@@ -5693,8 +5394,7 @@ Modes:
|
|
|
5693
5394
|
--dump Fetch and save raw OHLCV candles
|
|
5694
5395
|
--pnldebug Simulate PnL per minute for a given entry price and direction
|
|
5695
5396
|
--brokerdebug Fire a single broker commit against the live broker adapter
|
|
5696
|
-
--
|
|
5697
|
-
--tune <ideas.jsonl> <config.json> ONE out-of-sample shot of a frozen training artifact (point + author track)
|
|
5397
|
+
--sweep <ideas.jsonl> [config.json] Grid sweep over crowd ideas: profit-before-stop corridor + raw author tracks
|
|
5698
5398
|
--flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
|
|
5699
5399
|
--init Scaffold a new project in the current directory
|
|
5700
5400
|
--docker Scaffold a Docker workspace for running strategies in a container
|
|
@@ -5815,64 +5515,36 @@ Broker debug flags (--brokerdebug):
|
|
|
5815
5515
|
Loads ./live.module, fetches the last candle for --symbol/--timeframe, and calls
|
|
5816
5516
|
the selected broker commit with synthetic payload values derived from current price.
|
|
5817
5517
|
|
|
5818
|
-
|
|
5518
|
+
Sweep flags (--sweep):
|
|
5819
5519
|
|
|
5820
5520
|
--symbol <string> Trading pair to simulate (default: BTCUSDT)
|
|
5821
5521
|
--exchange <string> Exchange name (default: first registered)
|
|
5822
|
-
--output <string> Output file base name (default:
|
|
5823
|
-
--json Save full
|
|
5522
|
+
--output <string> Output file base name (default: sweep_{SYMBOL}_{TIMESTAMP})
|
|
5523
|
+
--json Save full ISweepResult to ./dump/<output>.json
|
|
5824
5524
|
--markdown Save summary report to ./dump/<output>.md
|
|
5825
|
-
--verbose Log every
|
|
5525
|
+
--verbose Log every sweep lifecycle callback to the console
|
|
5826
5526
|
|
|
5827
5527
|
Positionals: path to an ideas .jsonl file — one idea per line with the exact shape
|
|
5828
5528
|
{ "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL",
|
|
5829
5529
|
"author": string } — and an OPTIONAL config .json with the shape
|
|
5830
|
-
{ "gridAxes"?:
|
|
5530
|
+
{ "gridAxes"?: ISweepGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }.
|
|
5831
5531
|
Both files are validated BEFORE any work; a mismatch aborts the run with an error.
|
|
5832
5532
|
No config -> an empty object is used and the engine defaults apply (the full
|
|
5833
5533
|
default grid axes and reportOrder "sharpe" of the connection service).
|
|
5834
5534
|
|
|
5835
|
-
A
|
|
5836
|
-
|
|
5837
|
-
|
|
5838
|
-
|
|
5839
|
-
|
|
5840
|
-
|
|
5841
|
-
|
|
5842
|
-
any --symbol.
|
|
5535
|
+
A GRID SWEEP: the grid from the config (or the engine default) is evaluated
|
|
5536
|
+
per point. One candle pass per idea to the grid's longest hold, flood dedupe
|
|
5537
|
+
(one idea per author per direction per 8h), EVERY author traded (no ban — the
|
|
5538
|
+
engine reports the raw per-author track graded inside each point's own hold
|
|
5539
|
+
window; who to trust is userspace), time-based Sharpe/Sortino, per-metric
|
|
5540
|
+
buckets with their own ranking winners and author tracks. Ideas of other
|
|
5541
|
+
symbols are filtered out — one shared feed serves any --symbol.
|
|
5843
5542
|
|
|
5844
5543
|
No output flag → print the Markdown summary to stdout. With --verbose every
|
|
5845
|
-
|
|
5544
|
+
sweep lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
|
|
5846
5545
|
onGridPoint, onRanking, onDone) is logged to the console as it fires.
|
|
5847
5546
|
|
|
5848
|
-
Module file ./modules/
|
|
5849
|
-
(register your exchange there); without it CCXT Binance is used by default.
|
|
5850
|
-
|
|
5851
|
-
Tune flags (--tune):
|
|
5852
|
-
|
|
5853
|
-
--symbol <string> Trading pair to test (default: BTCUSDT)
|
|
5854
|
-
--exchange <string> Exchange name (default: first registered)
|
|
5855
|
-
--output <string> Output file base name (default: tune_{SYMBOL}_{TIMESTAMP})
|
|
5856
|
-
--json Save the full ISimulatorTestResult to ./dump/<output>.json
|
|
5857
|
-
--markdown Save the out-of-sample report to ./dump/<output>.md
|
|
5858
|
-
--verbose Log simulator lifecycle callbacks to the console
|
|
5859
|
-
|
|
5860
|
-
Positionals: path to an ideas .jsonl file (same shape and validation as
|
|
5861
|
-
--simulator) and a config .json carrying the FROZEN training artifact:
|
|
5862
|
-
{ "point": ISimulatorGridPoint, "authorStats": [{ "author", "ideas", "hits" }],
|
|
5863
|
-
"gridAxes"?, "reportOrder"? }. The point and authorStats are REQUIRED — without
|
|
5864
|
-
them there is nothing to test and the run aborts with an error. gridAxes are
|
|
5865
|
-
optional: by default they mirror the frozen point one value per axis (the grid
|
|
5866
|
-
is inert for a test).
|
|
5867
|
-
|
|
5868
|
-
ONE OUT-OF-SAMPLE SHOT, no training: the CLI never runs the sweep — pick your
|
|
5869
|
-
candidate elsewhere (a Simulator.run of your own), freeze its point and raw
|
|
5870
|
-
author track record into the config, and fire it once here via Simulator.test.
|
|
5871
|
-
Bans are re-derived from the frozen numbers under the point's rule; authors
|
|
5872
|
-
unseen in the config are banned by default. The report carries the result with
|
|
5873
|
-
the trade list and the frozen track record with re-derived ban flags.
|
|
5874
|
-
|
|
5875
|
-
Module file ./modules/tune.module is loaded automatically if it exists
|
|
5547
|
+
Module file ./modules/sweep.module is loaded automatically if it exists
|
|
5876
5548
|
(register your exchange there); without it CCXT Binance is used by default.
|
|
5877
5549
|
|
|
5878
5550
|
Flush flags (--flush):
|
|
@@ -5908,8 +5580,7 @@ Module hooks (loaded automatically by each mode):
|
|
|
5908
5580
|
modules/dump.module --dump Exchange schema for candle dumps
|
|
5909
5581
|
modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
|
|
5910
5582
|
modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
|
|
5911
|
-
modules/
|
|
5912
|
-
modules/tune.module --tune Exchange schema for the walk-forward parameter search
|
|
5583
|
+
modules/sweep.module --sweep Exchange schema for the crowd-ideas feasibility probe
|
|
5913
5584
|
|
|
5914
5585
|
--flush has no associated module. It only removes dump subdirectories.
|
|
5915
5586
|
|
|
@@ -5938,10 +5609,8 @@ Examples:
|
|
|
5938
5609
|
node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
|
|
5939
5610
|
node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
|
|
5940
5611
|
node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
|
|
5941
|
-
node ${ENTRY_PATH} --
|
|
5942
|
-
node ${ENTRY_PATH} --
|
|
5943
|
-
node ${ENTRY_PATH} --tune --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
|
|
5944
|
-
node ${ENTRY_PATH} --tune --symbol BTCUSDT --markdown --output jun_2026_tune ./assets/tv-ideas.normalized.jsonl ./assets/tune.config.json
|
|
5612
|
+
node ${ENTRY_PATH} --sweep --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
|
|
5613
|
+
node ${ENTRY_PATH} --sweep --symbol BTCUSDT --json --output jun_2026_probe ./assets/tv-ideas.normalized.jsonl ./assets/probe.config.json
|
|
5945
5614
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
|
|
5946
5615
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
|
|
5947
5616
|
node ${ENTRY_PATH} --init --output my-trading-bot
|
|
@@ -5955,7 +5624,7 @@ const main$1 = async () => {
|
|
|
5955
5624
|
if (!values.help) {
|
|
5956
5625
|
return;
|
|
5957
5626
|
}
|
|
5958
|
-
process.stdout.write(`@backtest-kit/cli ${"17.
|
|
5627
|
+
process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n\n`);
|
|
5959
5628
|
process.stdout.write(HELP_TEXT);
|
|
5960
5629
|
process.exit(0);
|
|
5961
5630
|
};
|
|
@@ -5969,7 +5638,7 @@ const main = async () => {
|
|
|
5969
5638
|
if (!values.version) {
|
|
5970
5639
|
return;
|
|
5971
5640
|
}
|
|
5972
|
-
process.stdout.write(`@backtest-kit/cli ${"17.
|
|
5641
|
+
process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n`);
|
|
5973
5642
|
process.exit(0);
|
|
5974
5643
|
};
|
|
5975
5644
|
main();
|