@backtest-kit/cli 16.6.0 → 17.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -45
- package/build/index.cjs +152 -450
- package/build/index.mjs +156 -454
- package/docker/package.json +6 -6
- package/package.json +13 -13
- package/template/project/package.mustache +5 -5
package/build/index.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,18 +891,10 @@ const getArgs = functoolsKit.singleshot(() => {
|
|
|
891
891
|
type: "boolean",
|
|
892
892
|
default: false,
|
|
893
893
|
},
|
|
894
|
-
|
|
894
|
+
sweep: {
|
|
895
895
|
type: "boolean",
|
|
896
896
|
default: false,
|
|
897
897
|
},
|
|
898
|
-
tune: {
|
|
899
|
-
type: "boolean",
|
|
900
|
-
default: false,
|
|
901
|
-
},
|
|
902
|
-
split: {
|
|
903
|
-
type: "string",
|
|
904
|
-
default: "",
|
|
905
|
-
},
|
|
906
898
|
commit: {
|
|
907
899
|
type: "string",
|
|
908
900
|
default: "",
|
|
@@ -3687,14 +3679,14 @@ const cli = {
|
|
|
3687
3679
|
};
|
|
3688
3680
|
init();
|
|
3689
3681
|
|
|
3690
|
-
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"];
|
|
3691
3683
|
const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
|
|
3692
3684
|
const HELP_TEXT$1 = `
|
|
3693
3685
|
Example:
|
|
3694
3686
|
|
|
3695
3687
|
node ${ENTRY_PATH$1} --help
|
|
3696
3688
|
`.trimStart();
|
|
3697
|
-
const main$
|
|
3689
|
+
const main$j = async () => {
|
|
3698
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)))) {
|
|
3699
3691
|
return;
|
|
3700
3692
|
}
|
|
@@ -3702,14 +3694,14 @@ const main$k = async () => {
|
|
|
3702
3694
|
if (MODES.some((mode) => values[mode])) {
|
|
3703
3695
|
return;
|
|
3704
3696
|
}
|
|
3705
|
-
process.stdout.write(`@backtest-kit/cli ${"
|
|
3697
|
+
process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n`);
|
|
3706
3698
|
process.stdout.write("\n");
|
|
3707
3699
|
process.stdout.write(`Run with --help to see available commands.\n`);
|
|
3708
3700
|
process.stdout.write("\n");
|
|
3709
3701
|
process.stdout.write(HELP_TEXT$1);
|
|
3710
3702
|
process.exit(0);
|
|
3711
3703
|
};
|
|
3712
|
-
main$
|
|
3704
|
+
main$j();
|
|
3713
3705
|
|
|
3714
3706
|
const notifyShutdown = functoolsKit.singleshot(async () => {
|
|
3715
3707
|
console.log("Graceful shutdown initiated.");
|
|
@@ -3725,7 +3717,7 @@ const flush = async (entryPoint) => {
|
|
|
3725
3717
|
console.log(`Removed: ${target}`);
|
|
3726
3718
|
}
|
|
3727
3719
|
};
|
|
3728
|
-
const main$
|
|
3720
|
+
const main$i = async () => {
|
|
3729
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)))) {
|
|
3730
3722
|
return;
|
|
3731
3723
|
}
|
|
@@ -3742,7 +3734,7 @@ const main$j = async () => {
|
|
|
3742
3734
|
}
|
|
3743
3735
|
process.exit(0);
|
|
3744
3736
|
};
|
|
3745
|
-
main$
|
|
3737
|
+
main$i();
|
|
3746
3738
|
|
|
3747
3739
|
const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
|
|
3748
3740
|
process.off("SIGINT", BEFORE_EXIT_FN$6);
|
|
@@ -3765,7 +3757,7 @@ const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
|
|
|
3765
3757
|
const listenGracefulShutdown$6 = functoolsKit.singleshot(() => {
|
|
3766
3758
|
process.on("SIGINT", BEFORE_EXIT_FN$6);
|
|
3767
3759
|
});
|
|
3768
|
-
const main$
|
|
3760
|
+
const main$h = async () => {
|
|
3769
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)))) {
|
|
3770
3762
|
return;
|
|
3771
3763
|
}
|
|
@@ -3783,7 +3775,7 @@ const main$i = async () => {
|
|
|
3783
3775
|
await cli.backtestMainService.connect();
|
|
3784
3776
|
listenGracefulShutdown$6();
|
|
3785
3777
|
};
|
|
3786
|
-
main$
|
|
3778
|
+
main$h();
|
|
3787
3779
|
|
|
3788
3780
|
const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
|
|
3789
3781
|
process.off("SIGINT", BEFORE_EXIT_FN$5);
|
|
@@ -3802,7 +3794,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
|
|
|
3802
3794
|
const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
|
|
3803
3795
|
process.on("SIGINT", BEFORE_EXIT_FN$5);
|
|
3804
3796
|
});
|
|
3805
|
-
const main$
|
|
3797
|
+
const main$g = async () => {
|
|
3806
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)))) {
|
|
3807
3799
|
return;
|
|
3808
3800
|
}
|
|
@@ -3821,7 +3813,7 @@ const main$h = async () => {
|
|
|
3821
3813
|
listenGracefulShutdown$5();
|
|
3822
3814
|
await cli.walkerMainService.connect();
|
|
3823
3815
|
};
|
|
3824
|
-
main$
|
|
3816
|
+
main$g();
|
|
3825
3817
|
|
|
3826
3818
|
const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
3827
3819
|
process.off("SIGINT", BEFORE_EXIT_FN$4);
|
|
@@ -3843,7 +3835,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
|
3843
3835
|
const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
|
|
3844
3836
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
3845
3837
|
});
|
|
3846
|
-
const main$
|
|
3838
|
+
const main$f = async () => {
|
|
3847
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)))) {
|
|
3848
3840
|
return;
|
|
3849
3841
|
}
|
|
@@ -3857,7 +3849,7 @@ const main$g = async () => {
|
|
|
3857
3849
|
await cli.paperMainService.connect();
|
|
3858
3850
|
listenGracefulShutdown$4();
|
|
3859
3851
|
};
|
|
3860
|
-
main$
|
|
3852
|
+
main$f();
|
|
3861
3853
|
|
|
3862
3854
|
const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
3863
3855
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -3879,7 +3871,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
|
3879
3871
|
const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
|
|
3880
3872
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
3881
3873
|
});
|
|
3882
|
-
const main$
|
|
3874
|
+
const main$e = async () => {
|
|
3883
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)))) {
|
|
3884
3876
|
return;
|
|
3885
3877
|
}
|
|
@@ -3893,7 +3885,7 @@ const main$f = async () => {
|
|
|
3893
3885
|
await cli.liveMainService.connect();
|
|
3894
3886
|
listenGracefulShutdown$3();
|
|
3895
3887
|
};
|
|
3896
|
-
main$
|
|
3888
|
+
main$e();
|
|
3897
3889
|
|
|
3898
3890
|
const stopBacktestList$1 = async () => {
|
|
3899
3891
|
for (const item of await BacktestKit.Backtest.list()) {
|
|
@@ -3957,7 +3949,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
|
|
|
3957
3949
|
const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
|
|
3958
3950
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
3959
3951
|
});
|
|
3960
|
-
const main$
|
|
3952
|
+
const main$d = async () => {
|
|
3961
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)))) {
|
|
3962
3954
|
return;
|
|
3963
3955
|
}
|
|
@@ -4013,7 +4005,7 @@ const main$e = async () => {
|
|
|
4013
4005
|
listenGracefulShutdown$2();
|
|
4014
4006
|
await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
|
|
4015
4007
|
};
|
|
4016
|
-
main$
|
|
4008
|
+
main$d();
|
|
4017
4009
|
|
|
4018
4010
|
const MODE_MODULE = {
|
|
4019
4011
|
backtest: "backtest.module",
|
|
@@ -4116,7 +4108,7 @@ const createGracefulShutdown = (mode) => {
|
|
|
4116
4108
|
process.on("SIGINT", handler);
|
|
4117
4109
|
});
|
|
4118
4110
|
};
|
|
4119
|
-
const main$
|
|
4111
|
+
const main$c = async () => {
|
|
4120
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)))) {
|
|
4121
4113
|
return;
|
|
4122
4114
|
}
|
|
@@ -4192,7 +4184,7 @@ const main$d = async () => {
|
|
|
4192
4184
|
}
|
|
4193
4185
|
await getEntrySubject().next(absolutePath);
|
|
4194
4186
|
};
|
|
4195
|
-
main$
|
|
4187
|
+
main$c();
|
|
4196
4188
|
|
|
4197
4189
|
const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
4198
4190
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -4203,7 +4195,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
|
4203
4195
|
const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
|
|
4204
4196
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
4205
4197
|
});
|
|
4206
|
-
const main$
|
|
4198
|
+
const main$b = async () => {
|
|
4207
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)))) {
|
|
4208
4200
|
return;
|
|
4209
4201
|
}
|
|
@@ -4213,7 +4205,7 @@ const main$c = async () => {
|
|
|
4213
4205
|
}
|
|
4214
4206
|
listenGracefulShutdown$1();
|
|
4215
4207
|
};
|
|
4216
|
-
main$
|
|
4208
|
+
main$b();
|
|
4217
4209
|
|
|
4218
4210
|
const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
4219
4211
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -4224,7 +4216,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
|
4224
4216
|
const listenGracefulShutdown = functoolsKit.singleshot(() => {
|
|
4225
4217
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
4226
4218
|
});
|
|
4227
|
-
const main$
|
|
4219
|
+
const main$a = async () => {
|
|
4228
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)))) {
|
|
4229
4221
|
return;
|
|
4230
4222
|
}
|
|
@@ -4234,7 +4226,7 @@ const main$b = async () => {
|
|
|
4234
4226
|
}
|
|
4235
4227
|
listenGracefulShutdown();
|
|
4236
4228
|
};
|
|
4237
|
-
main$
|
|
4229
|
+
main$a();
|
|
4238
4230
|
|
|
4239
4231
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
4240
4232
|
const keys = Object.keys(schema);
|
|
@@ -4256,7 +4248,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
4256
4248
|
}
|
|
4257
4249
|
return rows;
|
|
4258
4250
|
};
|
|
4259
|
-
const main$
|
|
4251
|
+
const main$9 = async () => {
|
|
4260
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)))) {
|
|
4261
4253
|
return;
|
|
4262
4254
|
}
|
|
@@ -4356,9 +4348,9 @@ const main$a = async () => {
|
|
|
4356
4348
|
console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
|
|
4357
4349
|
process.exit(0);
|
|
4358
4350
|
};
|
|
4359
|
-
main$
|
|
4351
|
+
main$9();
|
|
4360
4352
|
|
|
4361
|
-
const main$
|
|
4353
|
+
const main$8 = async () => {
|
|
4362
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)))) {
|
|
4363
4355
|
return;
|
|
4364
4356
|
}
|
|
@@ -4421,9 +4413,9 @@ const main$9 = async () => {
|
|
|
4421
4413
|
};
|
|
4422
4414
|
process.on("SIGINT", beforeExit);
|
|
4423
4415
|
};
|
|
4424
|
-
main$
|
|
4416
|
+
main$8();
|
|
4425
4417
|
|
|
4426
|
-
const main$
|
|
4418
|
+
const main$7 = async () => {
|
|
4427
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)))) {
|
|
4428
4420
|
return;
|
|
4429
4421
|
}
|
|
@@ -4504,9 +4496,9 @@ const main$8 = async () => {
|
|
|
4504
4496
|
console.log(JSON.stringify(candles, null, 2));
|
|
4505
4497
|
process.exit(0);
|
|
4506
4498
|
};
|
|
4507
|
-
main$
|
|
4499
|
+
main$7();
|
|
4508
4500
|
|
|
4509
|
-
const main$
|
|
4501
|
+
const main$6 = async () => {
|
|
4510
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)))) {
|
|
4511
4503
|
return;
|
|
4512
4504
|
}
|
|
@@ -4620,7 +4612,7 @@ const main$7 = async () => {
|
|
|
4620
4612
|
}
|
|
4621
4613
|
process.exit(0);
|
|
4622
4614
|
};
|
|
4623
|
-
main$
|
|
4615
|
+
main$6();
|
|
4624
4616
|
|
|
4625
4617
|
const getBroker = functoolsKit.singleshot(() => {
|
|
4626
4618
|
// Reaches into BrokerAdapter internals on purpose: the public commit*
|
|
@@ -4729,7 +4721,7 @@ const ZERO_PNL = {
|
|
|
4729
4721
|
pnlCost: 0,
|
|
4730
4722
|
pnlEntries: 0,
|
|
4731
4723
|
};
|
|
4732
|
-
const main$
|
|
4724
|
+
const main$5 = async () => {
|
|
4733
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)))) {
|
|
4734
4726
|
return;
|
|
4735
4727
|
}
|
|
@@ -4923,11 +4915,23 @@ const main$6 = async () => {
|
|
|
4923
4915
|
}
|
|
4924
4916
|
process.exit(0);
|
|
4925
4917
|
};
|
|
4926
|
-
main$
|
|
4918
|
+
main$5();
|
|
4927
4919
|
|
|
4928
|
-
const SIMULATOR_NAME
|
|
4929
|
-
const
|
|
4930
|
-
const
|
|
4920
|
+
const SIMULATOR_NAME = "cli_sweep";
|
|
4921
|
+
const CONFIG_KEYS = ["gridAxes", "reportOrder"];
|
|
4922
|
+
const validateConfig = (config) => {
|
|
4923
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
4924
|
+
return "config must be a JSON object";
|
|
4925
|
+
}
|
|
4926
|
+
for (const key of Object.keys(config)) {
|
|
4927
|
+
if (!CONFIG_KEYS.includes(key)) {
|
|
4928
|
+
return `unknown key "${key}" (expected: ${CONFIG_KEYS.join(", ")})`;
|
|
4929
|
+
}
|
|
4930
|
+
}
|
|
4931
|
+
return null;
|
|
4932
|
+
};
|
|
4933
|
+
const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
|
|
4934
|
+
const validateIdea = (idea, line) => {
|
|
4931
4935
|
if (typeof idea !== "object" || idea === null) {
|
|
4932
4936
|
return `line ${line}: not an object`;
|
|
4933
4937
|
}
|
|
@@ -4940,59 +4944,66 @@ const validateIdea$1 = (idea, line) => {
|
|
|
4940
4944
|
if (typeof idea.symbol !== "string" || !idea.symbol) {
|
|
4941
4945
|
return `line ${line}: "symbol" must be a non-empty string`;
|
|
4942
4946
|
}
|
|
4943
|
-
if (!IDEA_DIRECTIONS
|
|
4944
|
-
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)}`;
|
|
4945
4949
|
}
|
|
4946
4950
|
if (typeof idea.author !== "string" || !idea.author) {
|
|
4947
4951
|
return `line ${line}: "author" must be a non-empty string`;
|
|
4948
4952
|
}
|
|
4949
4953
|
return null;
|
|
4950
4954
|
};
|
|
4951
|
-
const
|
|
4952
|
-
|
|
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;
|
|
4959
|
+
const profitable = allReports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
|
|
4953
4960
|
const lines = [];
|
|
4954
|
-
lines.push(`#
|
|
4961
|
+
lines.push(`# Sweep Report — ${result.symbol}`);
|
|
4955
4962
|
lines.push("");
|
|
4956
4963
|
lines.push(`| Metric | Value |`);
|
|
4957
4964
|
lines.push(`| --- | --- |`);
|
|
4958
4965
|
lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
|
|
4959
4966
|
lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
|
|
4960
|
-
lines.push(`|
|
|
4961
|
-
lines.push(`| Profitable corridor | ${profitable} / ${result.reports.length} grid points |`);
|
|
4967
|
+
lines.push(`| Profitable corridor | ${profitable} / ${allReports.length} grid points |`);
|
|
4962
4968
|
lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
|
|
4969
|
+
// единственная метрика — profit-before-stop: одна корзина с
|
|
4970
|
+
// победителями и author tracks
|
|
4963
4971
|
lines.push("");
|
|
4964
|
-
lines.push(`##
|
|
4972
|
+
lines.push(`## Grid winners`);
|
|
4965
4973
|
lines.push("");
|
|
4966
|
-
lines.push(`| Criterion | Stop% |
|
|
4974
|
+
lines.push(`| Criterion | Stop% | Trail% | Hold | Lock% | Trades | PNL% | WinRate | Sharpe | Sortino |`);
|
|
4967
4975
|
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
|
|
4968
|
-
for (const best of
|
|
4976
|
+
for (const best of bucket.best) {
|
|
4969
4977
|
if (!best.report) {
|
|
4970
4978
|
lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
|
|
4971
4979
|
continue;
|
|
4972
4980
|
}
|
|
4973
4981
|
const { point } = best.report;
|
|
4974
|
-
lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.
|
|
4975
|
-
`${best.report.
|
|
4976
|
-
}
|
|
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);
|
|
4977
4991
|
lines.push("");
|
|
4978
|
-
lines.push(
|
|
4992
|
+
lines.push(`### Author tracks — raw ideas/hits/hitRate per rule (top ${tracks.length} by hitRate; full set in --json)`);
|
|
4979
4993
|
lines.push("");
|
|
4980
|
-
lines.push(`| Author | Ideas | Hits | HitRate |`);
|
|
4981
|
-
lines.push(`| --- | --- | --- | --- |`);
|
|
4982
|
-
|
|
4983
|
-
|
|
4984
|
-
const sharpeBest = result.best.find(({ criterion }) => criterion === "sharpe");
|
|
4985
|
-
for (const stat of (sharpeBest?.authorStats ?? []).filter(({ banned }) => !banned)) {
|
|
4986
|
-
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
|
|
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)}% |`);
|
|
4987
4998
|
}
|
|
4988
4999
|
return lines.join("\n");
|
|
4989
5000
|
};
|
|
4990
|
-
const main$
|
|
5001
|
+
const main$4 = async () => {
|
|
4991
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)))) {
|
|
4992
5003
|
return;
|
|
4993
5004
|
}
|
|
4994
5005
|
const { values, positionals } = getArgs();
|
|
4995
|
-
if (!values.
|
|
5006
|
+
if (!values.sweep) {
|
|
4996
5007
|
return;
|
|
4997
5008
|
}
|
|
4998
5009
|
const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
|
|
@@ -5000,6 +5011,33 @@ const main$5 = async () => {
|
|
|
5000
5011
|
console.error("Error: positional path to an ideas .jsonl file is required");
|
|
5001
5012
|
process.exit(1);
|
|
5002
5013
|
}
|
|
5014
|
+
// конфиг — позиционный JSON; без него подтягивается пустой объект
|
|
5015
|
+
// и схема живёт на дефолтах движка (оси, порядок отчёта)
|
|
5016
|
+
const [configPath = null] = positionals.filter((value) => value.endsWith(".json") && !value.endsWith(".jsonl"));
|
|
5017
|
+
let config = {};
|
|
5018
|
+
if (configPath) {
|
|
5019
|
+
let content;
|
|
5020
|
+
try {
|
|
5021
|
+
content = await fs$1.readFile(path.resolve(configPath), "utf-8");
|
|
5022
|
+
}
|
|
5023
|
+
catch (error) {
|
|
5024
|
+
console.error(`Error: cannot read config file: ${configPath}`);
|
|
5025
|
+
process.exit(1);
|
|
5026
|
+
}
|
|
5027
|
+
try {
|
|
5028
|
+
config = JSON.parse(content);
|
|
5029
|
+
}
|
|
5030
|
+
catch {
|
|
5031
|
+
console.error(`Error: invalid JSON in config file: ${configPath}`);
|
|
5032
|
+
process.exit(1);
|
|
5033
|
+
}
|
|
5034
|
+
const problem = validateConfig(config);
|
|
5035
|
+
if (problem) {
|
|
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
|
+
process.exit(1);
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
5003
5041
|
let ideas = [];
|
|
5004
5042
|
{
|
|
5005
5043
|
let content;
|
|
@@ -5024,7 +5062,7 @@ const main$5 = async () => {
|
|
|
5024
5062
|
console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
|
|
5025
5063
|
process.exit(1);
|
|
5026
5064
|
}
|
|
5027
|
-
const problem = validateIdea
|
|
5065
|
+
const problem = validateIdea(idea, i + 1);
|
|
5028
5066
|
if (problem) {
|
|
5029
5067
|
console.error(`Error: ideas file does not match the idea structure — ${problem}`);
|
|
5030
5068
|
console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
|
|
@@ -5053,7 +5091,7 @@ const main$5 = async () => {
|
|
|
5053
5091
|
process.exit(-1);
|
|
5054
5092
|
}
|
|
5055
5093
|
}
|
|
5056
|
-
await cli.moduleConnectionService.loadModule("
|
|
5094
|
+
await cli.moduleConnectionService.loadModule("sweep.module");
|
|
5057
5095
|
{
|
|
5058
5096
|
await cli.exchangeSchemaService.addSchema();
|
|
5059
5097
|
await cli.symbolSchemaService.addSchema();
|
|
@@ -5071,31 +5109,13 @@ const main$5 = async () => {
|
|
|
5071
5109
|
});
|
|
5072
5110
|
}
|
|
5073
5111
|
const symbol = values.symbol || "BTCUSDT";
|
|
5074
|
-
//
|
|
5075
|
-
//
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
BacktestKit.addSimulatorSchema({
|
|
5079
|
-
simulatorName: SIMULATOR_NAME$1,
|
|
5112
|
+
// оси и порядок отчёта приходят позиционным конфигом потребителя;
|
|
5113
|
+
// пустой конфиг — дефолтная сетка движка из connection-сервиса
|
|
5114
|
+
BacktestKit.addSweepSchema({
|
|
5115
|
+
sweepName: SIMULATOR_NAME,
|
|
5080
5116
|
exchangeName,
|
|
5081
|
-
gridAxes: {
|
|
5082
|
-
|
|
5083
|
-
hardStopPercent: [2, 3, 5, 7],
|
|
5084
|
-
// инертен: проба не собирает прибыль, выход — по времени или стопу
|
|
5085
|
-
trailingTakePercent: [100],
|
|
5086
|
-
holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
|
|
5087
|
-
// одного проверенного автора достаточно — консенсус не перебираем
|
|
5088
|
-
minIdeasAligned: [1],
|
|
5089
|
-
// правило бана — единственная перебираемая "умность" пробы
|
|
5090
|
-
minAuthorTrack: [3, 5],
|
|
5091
|
-
minAuthorHitRate: [0.5, 0.6],
|
|
5092
|
-
minAuthorWilson: [0],
|
|
5093
|
-
minWeightAligned: [0],
|
|
5094
|
-
profitLockPercent: [0],
|
|
5095
|
-
authorMetric: ["close"],
|
|
5096
|
-
banCriteria: ["sharpe", "pnl"],
|
|
5097
|
-
},
|
|
5098
|
-
reportOrder: "sharpe",
|
|
5117
|
+
...(config.gridAxes ? { gridAxes: config.gridAxes } : {}),
|
|
5118
|
+
...(config.reportOrder ? { reportOrder: config.reportOrder } : {}),
|
|
5099
5119
|
callbacks: {
|
|
5100
5120
|
onProgress: (symbol, stage, processed, total) => {
|
|
5101
5121
|
if (values.verbose) {
|
|
@@ -5112,22 +5132,20 @@ const main$5 = async () => {
|
|
|
5112
5132
|
console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
|
|
5113
5133
|
}
|
|
5114
5134
|
},
|
|
5115
|
-
onAuthorsTrained: (symbol,
|
|
5135
|
+
onAuthorsTrained: (symbol, tracks) => {
|
|
5116
5136
|
if (values.verbose) {
|
|
5117
5137
|
console.log("onAuthorsTrained", {
|
|
5118
5138
|
symbol,
|
|
5119
|
-
authors:
|
|
5120
|
-
banned: stats.filter(({ banned }) => banned).length,
|
|
5121
|
-
bannedIdeas,
|
|
5139
|
+
authors: tracks.length,
|
|
5122
5140
|
});
|
|
5123
5141
|
}
|
|
5124
5142
|
},
|
|
5125
|
-
onGridPoint: (symbol, report) => {
|
|
5143
|
+
onGridPoint: (symbol, report, trades) => {
|
|
5126
5144
|
if (values.verbose) {
|
|
5127
5145
|
console.log("onGridPoint", {
|
|
5128
5146
|
symbol,
|
|
5129
5147
|
point: report.point,
|
|
5130
|
-
trades:
|
|
5148
|
+
trades: trades.length,
|
|
5131
5149
|
pnl: +report.totalPnlPercent.toFixed(2),
|
|
5132
5150
|
sharpe: +report.sharpe.toFixed(2),
|
|
5133
5151
|
});
|
|
@@ -5147,20 +5165,19 @@ const main$5 = async () => {
|
|
|
5147
5165
|
if (values.verbose) {
|
|
5148
5166
|
console.log("onDone", {
|
|
5149
5167
|
symbol,
|
|
5150
|
-
reports: result.reports.length,
|
|
5151
|
-
|
|
5152
|
-
bannedAuthors: result.bannedAuthors.length,
|
|
5168
|
+
reports: result.reports.reports.length,
|
|
5169
|
+
tracks: result.reports.tracks.length,
|
|
5153
5170
|
});
|
|
5154
5171
|
}
|
|
5155
5172
|
},
|
|
5156
5173
|
},
|
|
5157
5174
|
});
|
|
5158
|
-
const result = await BacktestKit.
|
|
5175
|
+
const result = await BacktestKit.Sweep.run({
|
|
5159
5176
|
symbol,
|
|
5160
|
-
|
|
5177
|
+
sweepName: SIMULATOR_NAME,
|
|
5161
5178
|
ideas,
|
|
5162
5179
|
});
|
|
5163
|
-
const dumpName = values.output || `
|
|
5180
|
+
const dumpName = values.output || `sweep_${symbol}_${Date.now()}`;
|
|
5164
5181
|
const dumpDir = path.join(process.cwd(), "dump");
|
|
5165
5182
|
if (values.json) {
|
|
5166
5183
|
const filePath = path.resolve(dumpDir, `${dumpName}.json`);
|
|
@@ -5172,299 +5189,11 @@ const main$5 = async () => {
|
|
|
5172
5189
|
if (values.markdown) {
|
|
5173
5190
|
const filePath = path.resolve(dumpDir, `${dumpName}.md`);
|
|
5174
5191
|
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
5175
|
-
await fs$1.writeFile(filePath, toMarkdown
|
|
5192
|
+
await fs$1.writeFile(filePath, toMarkdown(result), "utf-8");
|
|
5176
5193
|
console.log(`Saved: ${filePath}`);
|
|
5177
5194
|
process.exit(0);
|
|
5178
5195
|
}
|
|
5179
|
-
console.log(toMarkdown
|
|
5180
|
-
process.exit(0);
|
|
5181
|
-
};
|
|
5182
|
-
main$5();
|
|
5183
|
-
|
|
5184
|
-
const SIMULATOR_NAME = "cli_tune";
|
|
5185
|
-
/** Доля времени фида, уходящая в обучение; хвост — один OOS-выстрел. */
|
|
5186
|
-
const DEFAULT_TRAIN_SPLIT = 0.7;
|
|
5187
|
-
const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
|
|
5188
|
-
const validateIdea = (idea, line) => {
|
|
5189
|
-
if (typeof idea !== "object" || idea === null) {
|
|
5190
|
-
return `line ${line}: not an object`;
|
|
5191
|
-
}
|
|
5192
|
-
if (typeof idea.id !== "number") {
|
|
5193
|
-
return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
|
|
5194
|
-
}
|
|
5195
|
-
if (typeof idea.ts !== "number") {
|
|
5196
|
-
return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
|
|
5197
|
-
}
|
|
5198
|
-
if (typeof idea.symbol !== "string" || !idea.symbol) {
|
|
5199
|
-
return `line ${line}: "symbol" must be a non-empty string`;
|
|
5200
|
-
}
|
|
5201
|
-
if (!IDEA_DIRECTIONS.includes(idea.direction)) {
|
|
5202
|
-
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
5203
|
-
}
|
|
5204
|
-
if (typeof idea.author !== "string" || !idea.author) {
|
|
5205
|
-
return `line ${line}: "author" must be a non-empty string`;
|
|
5206
|
-
}
|
|
5207
|
-
return null;
|
|
5208
|
-
};
|
|
5209
|
-
const pointLabel = (point) => `H=${point.hardStopPercent} TT=${point.trailingTakePercent} hold=${point.holdMinutes / 60}h N=${point.minIdeasAligned} ` +
|
|
5210
|
-
`track=${point.minAuthorTrack} rate=${point.minAuthorHitRate} wilson=${point.minAuthorWilson} W=${point.minWeightAligned} ` +
|
|
5211
|
-
`lock=${point.profitLockPercent} metric=${point.authorMetric}`;
|
|
5212
|
-
const fmtRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
|
|
5213
|
-
const toMarkdown = (train, test, trainSplit) => {
|
|
5214
|
-
const lines = [];
|
|
5215
|
-
lines.push(`# Tune Report — ${train.symbol}`);
|
|
5216
|
-
lines.push("");
|
|
5217
|
-
lines.push(`Walk-forward: train on the first ${Math.round(trainSplit * 100)}% of the feed time range, ONE out-of-sample shot of the frozen sharpe winner on the tail.`);
|
|
5218
|
-
lines.push("");
|
|
5219
|
-
lines.push(`## Train (${train.ideasDirectional} directional ideas, ${train.reports.length} grid points)`);
|
|
5220
|
-
lines.push("");
|
|
5221
|
-
lines.push(`| Criterion | Point | Trades | PNL% | WinRate | DD% | Sharpe | Sortino |`);
|
|
5222
|
-
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- |`);
|
|
5223
|
-
for (const best of train.best) {
|
|
5224
|
-
if (!best.report) {
|
|
5225
|
-
lines.push(`| ${best.criterion} | — | — | — | — | — | — | — |`);
|
|
5226
|
-
continue;
|
|
5227
|
-
}
|
|
5228
|
-
lines.push(`| ${best.criterion} | ${pointLabel(best.report.point)} | ${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ` +
|
|
5229
|
-
`${(best.report.winRate * 100).toFixed(0)}% | ${best.report.maxSeriesDrawdownPercent.toFixed(2)} | ${fmtRatio(best.report.sharpe)} | ${fmtRatio(best.report.sortino)} |`);
|
|
5230
|
-
}
|
|
5231
|
-
lines.push("");
|
|
5232
|
-
lines.push(`## Out-of-sample (frozen sharpe winner, ${test.ideasDirectional} tail ideas)`);
|
|
5233
|
-
lines.push("");
|
|
5234
|
-
lines.push(`Frozen point: ${pointLabel(test.point)}`);
|
|
5235
|
-
lines.push("");
|
|
5236
|
-
lines.push(`| Metric | Value |`);
|
|
5237
|
-
lines.push(`| --- | --- |`);
|
|
5238
|
-
lines.push(`| Trades (skipped busy) | ${test.report.trades} (${test.report.skippedBusy}) |`);
|
|
5239
|
-
lines.push(`| PNL | ${test.report.totalPnlPercent.toFixed(2)}% |`);
|
|
5240
|
-
lines.push(`| Win rate | ${(test.report.winRate * 100).toFixed(0)}% |`);
|
|
5241
|
-
lines.push(`| Profit factor | ${fmtRatio(test.report.profitFactor)} |`);
|
|
5242
|
-
lines.push(`| Max series drawdown | ${test.report.maxSeriesDrawdownPercent.toFixed(2)}% |`);
|
|
5243
|
-
lines.push(`| Sharpe / Sortino | ${fmtRatio(test.report.sharpe)} / ${fmtRatio(test.report.sortino)} |`);
|
|
5244
|
-
lines.push(`| Recovery factor | ${fmtRatio(test.report.recoveryFactor)} |`);
|
|
5245
|
-
lines.push(`| Exits | ${Object.entries(test.report.exitReasons).map(([reason, count]) => `${reason}=${count}`).join(", ")} |`);
|
|
5246
|
-
lines.push("");
|
|
5247
|
-
lines.push(`## Test trades`);
|
|
5248
|
-
lines.push("");
|
|
5249
|
-
lines.push(`| Direction | Exit | PNL% | Hold | Entry (UTC) |`);
|
|
5250
|
-
lines.push(`| --- | --- | --- | --- | --- |`);
|
|
5251
|
-
for (const trade of test.trades) {
|
|
5252
|
-
lines.push(`| ${trade.direction} | ${trade.exitReason} | ${trade.pnlPercent.toFixed(2)} | ${trade.holdMinutesActual}m | ${new Date(trade.entryTimestamp).toISOString()} |`);
|
|
5253
|
-
}
|
|
5254
|
-
lines.push("");
|
|
5255
|
-
lines.push(`## Frozen author track record (train range, sharpe winner rule)`);
|
|
5256
|
-
lines.push("");
|
|
5257
|
-
lines.push(`Freeze these raw numbers for production \`Simulator.test\` — banned flags and vote weights are re-derived from them under the frozen point's rule; an author absent from the list is banned by default.`);
|
|
5258
|
-
lines.push("");
|
|
5259
|
-
lines.push(`| Author | Ideas | Hits | Banned |`);
|
|
5260
|
-
lines.push(`| --- | --- | --- | --- |`);
|
|
5261
|
-
for (const stat of test.authorStats) {
|
|
5262
|
-
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${stat.banned ? "yes" : ""} |`);
|
|
5263
|
-
}
|
|
5264
|
-
return lines.join("\n");
|
|
5265
|
-
};
|
|
5266
|
-
const main$4 = async () => {
|
|
5267
|
-
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)))) {
|
|
5268
|
-
return;
|
|
5269
|
-
}
|
|
5270
|
-
const { values, positionals } = getArgs();
|
|
5271
|
-
if (!values.tune) {
|
|
5272
|
-
return;
|
|
5273
|
-
}
|
|
5274
|
-
const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
|
|
5275
|
-
if (!ideasPath) {
|
|
5276
|
-
console.error("Error: positional path to an ideas .jsonl file is required");
|
|
5277
|
-
process.exit(1);
|
|
5278
|
-
}
|
|
5279
|
-
let ideas = [];
|
|
5280
|
-
{
|
|
5281
|
-
let content;
|
|
5282
|
-
try {
|
|
5283
|
-
content = await fs$1.readFile(path.resolve(ideasPath), "utf-8");
|
|
5284
|
-
}
|
|
5285
|
-
catch (error) {
|
|
5286
|
-
console.error(`Error: cannot read ideas file: ${ideasPath}`);
|
|
5287
|
-
process.exit(1);
|
|
5288
|
-
}
|
|
5289
|
-
const rows = content.split("\n").filter(Boolean);
|
|
5290
|
-
if (!rows.length) {
|
|
5291
|
-
console.error(`Error: ideas file is empty: ${ideasPath}`);
|
|
5292
|
-
process.exit(1);
|
|
5293
|
-
}
|
|
5294
|
-
for (let i = 0; i < rows.length; i++) {
|
|
5295
|
-
let idea;
|
|
5296
|
-
try {
|
|
5297
|
-
idea = JSON.parse(rows[i]);
|
|
5298
|
-
}
|
|
5299
|
-
catch {
|
|
5300
|
-
console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
|
|
5301
|
-
process.exit(1);
|
|
5302
|
-
}
|
|
5303
|
-
const problem = validateIdea(idea, i + 1);
|
|
5304
|
-
if (problem) {
|
|
5305
|
-
console.error(`Error: ideas file does not match the idea structure — ${problem}`);
|
|
5306
|
-
console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
|
|
5307
|
-
process.exit(1);
|
|
5308
|
-
}
|
|
5309
|
-
ideas.push(idea);
|
|
5310
|
-
}
|
|
5311
|
-
}
|
|
5312
|
-
const trainSplit = values.split
|
|
5313
|
-
? parseFloat(values.split)
|
|
5314
|
-
: DEFAULT_TRAIN_SPLIT;
|
|
5315
|
-
if (!Number.isFinite(trainSplit) || trainSplit <= 0 || trainSplit >= 1) {
|
|
5316
|
-
console.error(`Error: --split must be a fraction between 0 and 1, got ${values.split}`);
|
|
5317
|
-
process.exit(1);
|
|
5318
|
-
}
|
|
5319
|
-
{
|
|
5320
|
-
const cwd = process.cwd();
|
|
5321
|
-
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
5322
|
-
}
|
|
5323
|
-
await cli.configConnectionService.loadConfig("setup.config");
|
|
5324
|
-
{
|
|
5325
|
-
const loader = await cli.configConnectionService.loadConfig("loader.config");
|
|
5326
|
-
try {
|
|
5327
|
-
if (typeof loader === "function") {
|
|
5328
|
-
await loader();
|
|
5329
|
-
}
|
|
5330
|
-
if (typeof loader?.loader === "function") {
|
|
5331
|
-
await loader.loader();
|
|
5332
|
-
}
|
|
5333
|
-
}
|
|
5334
|
-
catch (error) {
|
|
5335
|
-
console.error("Module loader failed", error);
|
|
5336
|
-
process.exit(-1);
|
|
5337
|
-
}
|
|
5338
|
-
}
|
|
5339
|
-
await cli.moduleConnectionService.loadModule("tune.module");
|
|
5340
|
-
{
|
|
5341
|
-
await cli.exchangeSchemaService.addSchema();
|
|
5342
|
-
await cli.symbolSchemaService.addSchema();
|
|
5343
|
-
}
|
|
5344
|
-
const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
|
|
5345
|
-
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
5346
|
-
if (values.verbose) {
|
|
5347
|
-
BacktestKit.overrideExchangeSchema({
|
|
5348
|
-
exchangeName,
|
|
5349
|
-
callbacks: {
|
|
5350
|
-
onCandleData(symbol, interval, since) {
|
|
5351
|
-
console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
|
|
5352
|
-
},
|
|
5353
|
-
},
|
|
5354
|
-
});
|
|
5355
|
-
}
|
|
5356
|
-
const symbol = values.symbol || "BTCUSDT";
|
|
5357
|
-
// Подбор параметров: полная сетка с собирающей прибыль механикой —
|
|
5358
|
-
// замок, трейлинг, взвешенный консенсус, Вильсон, обе метрики бана.
|
|
5359
|
-
// Честность обеспечивается сплитом: обучение видит только голову,
|
|
5360
|
-
// хвост тратится на один выстрел замороженного sharpe-победителя
|
|
5361
|
-
BacktestKit.addSimulatorSchema({
|
|
5362
|
-
simulatorName: SIMULATOR_NAME,
|
|
5363
|
-
exchangeName,
|
|
5364
|
-
gridAxes: {
|
|
5365
|
-
// стопы < 2% сидят внутри медианного шейкаута и не выигрывают
|
|
5366
|
-
hardStopPercent: [2, 2.5, 3, 4, 5, 7],
|
|
5367
|
-
trailingTakePercent: [1, 1.5, 2, 3, 4],
|
|
5368
|
-
holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
|
|
5369
|
-
minIdeasAligned: [1, 2],
|
|
5370
|
-
minAuthorTrack: [2, 3, 5],
|
|
5371
|
-
minAuthorHitRate: [0.5, 0.6],
|
|
5372
|
-
minAuthorWilson: [0, 0.6],
|
|
5373
|
-
minWeightAligned: [0, 0.6, 1.2],
|
|
5374
|
-
profitLockPercent: [0, 1.5, 2.5],
|
|
5375
|
-
authorMetric: ["close", "reach"],
|
|
5376
|
-
banCriteria: ["sharpe"],
|
|
5377
|
-
},
|
|
5378
|
-
reportOrder: "sharpe",
|
|
5379
|
-
callbacks: {
|
|
5380
|
-
onProgress: (symbol, stage, processed, total) => {
|
|
5381
|
-
if (values.verbose) {
|
|
5382
|
-
console.log("onProgress", { symbol, stage, processed, total });
|
|
5383
|
-
}
|
|
5384
|
-
},
|
|
5385
|
-
onIdeas: (symbol, ideasTotal, ideasDirectional) => {
|
|
5386
|
-
if (values.verbose) {
|
|
5387
|
-
console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
|
|
5388
|
-
}
|
|
5389
|
-
},
|
|
5390
|
-
onProfiles: (symbol, profiles, truncatedCount) => {
|
|
5391
|
-
if (values.verbose) {
|
|
5392
|
-
console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
|
|
5393
|
-
}
|
|
5394
|
-
},
|
|
5395
|
-
onAuthorsTrained: (symbol, stats, bannedIdeas) => {
|
|
5396
|
-
if (values.verbose) {
|
|
5397
|
-
console.log("onAuthorsTrained", {
|
|
5398
|
-
symbol,
|
|
5399
|
-
authors: stats.length,
|
|
5400
|
-
banned: stats.filter(({ banned }) => banned).length,
|
|
5401
|
-
bannedIdeas,
|
|
5402
|
-
});
|
|
5403
|
-
}
|
|
5404
|
-
},
|
|
5405
|
-
onRanking: (symbol, criterion, _sorted, best) => {
|
|
5406
|
-
if (values.verbose) {
|
|
5407
|
-
console.log("onRanking", {
|
|
5408
|
-
symbol,
|
|
5409
|
-
criterion,
|
|
5410
|
-
point: best.report?.point ?? null,
|
|
5411
|
-
pnl: best.report ? +best.report.totalPnlPercent.toFixed(2) : null,
|
|
5412
|
-
});
|
|
5413
|
-
}
|
|
5414
|
-
},
|
|
5415
|
-
onTestDone: (symbol, result) => {
|
|
5416
|
-
if (values.verbose) {
|
|
5417
|
-
console.log("onTestDone", {
|
|
5418
|
-
symbol,
|
|
5419
|
-
trades: result.report.trades,
|
|
5420
|
-
pnl: +result.report.totalPnlPercent.toFixed(2),
|
|
5421
|
-
});
|
|
5422
|
-
}
|
|
5423
|
-
},
|
|
5424
|
-
},
|
|
5425
|
-
});
|
|
5426
|
-
// обучение видит ТОЛЬКО голову ленты; хвост — один OOS-выстрел
|
|
5427
|
-
const sorted = [...ideas].sort((a, b) => a.ts - b.ts);
|
|
5428
|
-
const cutoff = sorted[0].ts + (sorted[sorted.length - 1].ts - sorted[0].ts) * trainSplit;
|
|
5429
|
-
const trainIdeas = sorted.filter(({ ts }) => ts < cutoff);
|
|
5430
|
-
const testIdeas = sorted.filter(({ ts }) => ts >= cutoff);
|
|
5431
|
-
const train = await BacktestKit.Simulator.run({
|
|
5432
|
-
symbol,
|
|
5433
|
-
simulatorName: SIMULATOR_NAME,
|
|
5434
|
-
ideas: trainIdeas,
|
|
5435
|
-
});
|
|
5436
|
-
const sharpeBest = train.best.find(({ criterion }) => criterion === "sharpe");
|
|
5437
|
-
if (!sharpeBest?.report) {
|
|
5438
|
-
console.error("Error: training produced no sharpe winner — nothing to freeze for the out-of-sample shot");
|
|
5439
|
-
process.exit(1);
|
|
5440
|
-
}
|
|
5441
|
-
// заморозка: точка и трек-рекорд sharpe-победителя как есть — из
|
|
5442
|
-
// замороженных статов test() читает только author/ideas/hits и сам
|
|
5443
|
-
// перевыводит баны и веса под правило точки
|
|
5444
|
-
const test = await BacktestKit.Simulator.test({
|
|
5445
|
-
symbol,
|
|
5446
|
-
simulatorName: SIMULATOR_NAME,
|
|
5447
|
-
ideas: testIdeas,
|
|
5448
|
-
point: sharpeBest.report.point,
|
|
5449
|
-
authorStats: sharpeBest.authorStats,
|
|
5450
|
-
});
|
|
5451
|
-
const dumpName = values.output || `tune_${symbol}_${Date.now()}`;
|
|
5452
|
-
const dumpDir = path.join(process.cwd(), "dump");
|
|
5453
|
-
if (values.json) {
|
|
5454
|
-
const filePath = path.resolve(dumpDir, `${dumpName}.json`);
|
|
5455
|
-
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
5456
|
-
await fs$1.writeFile(filePath, JSON.stringify({ trainSplit, train, test }, null, 2), "utf-8");
|
|
5457
|
-
console.log(`Saved: ${filePath}`);
|
|
5458
|
-
process.exit(0);
|
|
5459
|
-
}
|
|
5460
|
-
if (values.markdown) {
|
|
5461
|
-
const filePath = path.resolve(dumpDir, `${dumpName}.md`);
|
|
5462
|
-
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
5463
|
-
await fs$1.writeFile(filePath, toMarkdown(train, test, trainSplit), "utf-8");
|
|
5464
|
-
console.log(`Saved: ${filePath}`);
|
|
5465
|
-
process.exit(0);
|
|
5466
|
-
}
|
|
5467
|
-
console.log(toMarkdown(train, test, trainSplit));
|
|
5196
|
+
console.log(toMarkdown(result));
|
|
5468
5197
|
process.exit(0);
|
|
5469
5198
|
};
|
|
5470
5199
|
main$4();
|
|
@@ -5665,8 +5394,7 @@ Modes:
|
|
|
5665
5394
|
--dump Fetch and save raw OHLCV candles
|
|
5666
5395
|
--pnldebug Simulate PnL per minute for a given entry price and direction
|
|
5667
5396
|
--brokerdebug Fire a single broker commit against the live broker adapter
|
|
5668
|
-
--
|
|
5669
|
-
--tune <ideas.jsonl> Walk-forward parameter search: train on the head, one frozen shot on the tail
|
|
5397
|
+
--sweep <ideas.jsonl> [config.json] Grid sweep over crowd ideas: profit-before-stop corridor + raw author tracks
|
|
5670
5398
|
--flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
|
|
5671
5399
|
--init Scaffold a new project in the current directory
|
|
5672
5400
|
--docker Scaffold a Docker workspace for running strategies in a container
|
|
@@ -5787,59 +5515,36 @@ Broker debug flags (--brokerdebug):
|
|
|
5787
5515
|
Loads ./live.module, fetches the last candle for --symbol/--timeframe, and calls
|
|
5788
5516
|
the selected broker commit with synthetic payload values derived from current price.
|
|
5789
5517
|
|
|
5790
|
-
|
|
5518
|
+
Sweep flags (--sweep):
|
|
5791
5519
|
|
|
5792
5520
|
--symbol <string> Trading pair to simulate (default: BTCUSDT)
|
|
5793
5521
|
--exchange <string> Exchange name (default: first registered)
|
|
5794
|
-
--output <string> Output file base name (default:
|
|
5795
|
-
--json Save full
|
|
5522
|
+
--output <string> Output file base name (default: sweep_{SYMBOL}_{TIMESTAMP})
|
|
5523
|
+
--json Save full ISweepResult to ./dump/<output>.json
|
|
5796
5524
|
--markdown Save summary report to ./dump/<output>.md
|
|
5797
|
-
--verbose Log every
|
|
5525
|
+
--verbose Log every sweep lifecycle callback to the console
|
|
5798
5526
|
|
|
5799
|
-
|
|
5527
|
+
Positionals: path to an ideas .jsonl file — one idea per line with the exact shape
|
|
5800
5528
|
{ "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL",
|
|
5801
|
-
"author": string }
|
|
5802
|
-
|
|
5803
|
-
|
|
5804
|
-
|
|
5805
|
-
|
|
5806
|
-
|
|
5807
|
-
|
|
5808
|
-
|
|
5809
|
-
|
|
5810
|
-
|
|
5811
|
-
|
|
5529
|
+
"author": string } — and an OPTIONAL config .json with the shape
|
|
5530
|
+
{ "gridAxes"?: ISweepGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }.
|
|
5531
|
+
Both files are validated BEFORE any work; a mismatch aborts the run with an error.
|
|
5532
|
+
No config -> an empty object is used and the engine defaults apply (the full
|
|
5533
|
+
default grid axes and reportOrder "sharpe" of the connection service).
|
|
5534
|
+
|
|
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.
|
|
5812
5542
|
|
|
5813
5543
|
No output flag → print the Markdown summary to stdout. With --verbose every
|
|
5814
|
-
|
|
5544
|
+
sweep lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
|
|
5815
5545
|
onGridPoint, onRanking, onDone) is logged to the console as it fires.
|
|
5816
5546
|
|
|
5817
|
-
Module file ./modules/
|
|
5818
|
-
(register your exchange there); without it CCXT Binance is used by default.
|
|
5819
|
-
|
|
5820
|
-
Tune flags (--tune):
|
|
5821
|
-
|
|
5822
|
-
--symbol <string> Trading pair to tune (default: BTCUSDT)
|
|
5823
|
-
--exchange <string> Exchange name (default: first registered)
|
|
5824
|
-
--split <string> Train share of the feed time range, 0..1 (default: 0.7)
|
|
5825
|
-
--output <string> Output file base name (default: tune_{SYMBOL}_{TIMESTAMP})
|
|
5826
|
-
--json Save { trainSplit, train, test } to ./dump/<output>.json
|
|
5827
|
-
--markdown Save walk-forward report to ./dump/<output>.md
|
|
5828
|
-
--verbose Log simulator lifecycle callbacks to the console
|
|
5829
|
-
|
|
5830
|
-
Positional: path to an ideas .jsonl file — same shape and validation as --simulator.
|
|
5831
|
-
|
|
5832
|
-
The PARAMETER SEARCH counterpart of the --simulator probe: the full grid with the
|
|
5833
|
-
profit-harvesting machinery on — profit lock, trailing take, weighted consensus,
|
|
5834
|
-
Wilson-bound ban, both author-hit metrics (close/reach). Honesty is structural:
|
|
5835
|
-
training sees ONLY the head of the feed (--split of its time range), then the
|
|
5836
|
-
sharpe winner is frozen — point and raw author track record — and fired exactly
|
|
5837
|
-
once on the tail via Simulator.test. Nothing is trained on the tail; authors
|
|
5838
|
-
unseen in training are banned by default. The report carries the train winners,
|
|
5839
|
-
the out-of-sample result with the trade list, and the frozen author track record
|
|
5840
|
-
to hardcode for production Simulator.test.
|
|
5841
|
-
|
|
5842
|
-
Module file ./modules/tune.module is loaded automatically if it exists
|
|
5547
|
+
Module file ./modules/sweep.module is loaded automatically if it exists
|
|
5843
5548
|
(register your exchange there); without it CCXT Binance is used by default.
|
|
5844
5549
|
|
|
5845
5550
|
Flush flags (--flush):
|
|
@@ -5875,8 +5580,7 @@ Module hooks (loaded automatically by each mode):
|
|
|
5875
5580
|
modules/dump.module --dump Exchange schema for candle dumps
|
|
5876
5581
|
modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
|
|
5877
5582
|
modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
|
|
5878
|
-
modules/
|
|
5879
|
-
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
|
|
5880
5584
|
|
|
5881
5585
|
--flush has no associated module. It only removes dump subdirectories.
|
|
5882
5586
|
|
|
@@ -5905,10 +5609,8 @@ Examples:
|
|
|
5905
5609
|
node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
|
|
5906
5610
|
node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
|
|
5907
5611
|
node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
|
|
5908
|
-
node ${ENTRY_PATH} --
|
|
5909
|
-
node ${ENTRY_PATH} --
|
|
5910
|
-
node ${ENTRY_PATH} --tune --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
|
|
5911
|
-
node ${ENTRY_PATH} --tune --symbol BTCUSDT --split 0.7 --markdown --output jun_2026_tune ./assets/tv-ideas.normalized.jsonl
|
|
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
|
|
5912
5614
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
|
|
5913
5615
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
|
|
5914
5616
|
node ${ENTRY_PATH} --init --output my-trading-bot
|
|
@@ -5922,7 +5624,7 @@ const main$1 = async () => {
|
|
|
5922
5624
|
if (!values.help) {
|
|
5923
5625
|
return;
|
|
5924
5626
|
}
|
|
5925
|
-
process.stdout.write(`@backtest-kit/cli ${"
|
|
5627
|
+
process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n\n`);
|
|
5926
5628
|
process.stdout.write(HELP_TEXT);
|
|
5927
5629
|
process.exit(0);
|
|
5928
5630
|
};
|
|
@@ -5936,7 +5638,7 @@ const main = async () => {
|
|
|
5936
5638
|
if (!values.version) {
|
|
5937
5639
|
return;
|
|
5938
5640
|
}
|
|
5939
|
-
process.stdout.write(`@backtest-kit/cli ${"
|
|
5641
|
+
process.stdout.write(`@backtest-kit/cli ${"17.7.0"}\n`);
|
|
5940
5642
|
process.exit(0);
|
|
5941
5643
|
};
|
|
5942
5644
|
main();
|