@backtest-kit/cli 16.5.0 → 17.0.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 +40 -8
- package/build/index.cjs +786 -93
- package/build/index.mjs +791 -98
- 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$l = () => {
|
|
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$l();
|
|
244
244
|
|
|
245
245
|
const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
|
|
246
246
|
function dumpStackTrace(error) {
|
|
@@ -895,6 +895,10 @@ const getArgs = functoolsKit.singleshot(() => {
|
|
|
895
895
|
type: "boolean",
|
|
896
896
|
default: false,
|
|
897
897
|
},
|
|
898
|
+
tune: {
|
|
899
|
+
type: "boolean",
|
|
900
|
+
default: false,
|
|
901
|
+
},
|
|
898
902
|
commit: {
|
|
899
903
|
type: "string",
|
|
900
904
|
default: "",
|
|
@@ -2727,8 +2731,11 @@ class TelegramLogicService {
|
|
|
2727
2731
|
const unSignalNotify = BacktestKit.listenSignalNotify(async (event) => {
|
|
2728
2732
|
await this.notifySignalInfo(event);
|
|
2729
2733
|
});
|
|
2734
|
+
const unGc = functoolsKit.Source.fromInterval(90000).connect(() => {
|
|
2735
|
+
this.notifyOrderContinue.gc();
|
|
2736
|
+
});
|
|
2730
2737
|
const unConnect = () => this.connect.clear();
|
|
2731
|
-
const unListen = functoolsKit.compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unSignalNotify(), () => unConnect());
|
|
2738
|
+
const unListen = functoolsKit.compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unGc(), () => unSignalNotify(), () => unConnect());
|
|
2732
2739
|
return () => {
|
|
2733
2740
|
STOP_BOT_FN();
|
|
2734
2741
|
unListen();
|
|
@@ -3676,14 +3683,14 @@ const cli = {
|
|
|
3676
3683
|
};
|
|
3677
3684
|
init();
|
|
3678
3685
|
|
|
3679
|
-
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "flush", "init", "docker", "help", "version"];
|
|
3686
|
+
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "tune", "flush", "init", "docker", "help", "version"];
|
|
3680
3687
|
const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
|
|
3681
3688
|
const HELP_TEXT$1 = `
|
|
3682
3689
|
Example:
|
|
3683
3690
|
|
|
3684
3691
|
node ${ENTRY_PATH$1} --help
|
|
3685
3692
|
`.trimStart();
|
|
3686
|
-
const main$
|
|
3693
|
+
const main$k = async () => {
|
|
3687
3694
|
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)))) {
|
|
3688
3695
|
return;
|
|
3689
3696
|
}
|
|
@@ -3691,14 +3698,14 @@ const main$i = async () => {
|
|
|
3691
3698
|
if (MODES.some((mode) => values[mode])) {
|
|
3692
3699
|
return;
|
|
3693
3700
|
}
|
|
3694
|
-
process.stdout.write(`@backtest-kit/cli ${"
|
|
3701
|
+
process.stdout.write(`@backtest-kit/cli ${"17.0.0"}\n`);
|
|
3695
3702
|
process.stdout.write("\n");
|
|
3696
3703
|
process.stdout.write(`Run with --help to see available commands.\n`);
|
|
3697
3704
|
process.stdout.write("\n");
|
|
3698
3705
|
process.stdout.write(HELP_TEXT$1);
|
|
3699
3706
|
process.exit(0);
|
|
3700
3707
|
};
|
|
3701
|
-
main$
|
|
3708
|
+
main$k();
|
|
3702
3709
|
|
|
3703
3710
|
const notifyShutdown = functoolsKit.singleshot(async () => {
|
|
3704
3711
|
console.log("Graceful shutdown initiated.");
|
|
@@ -3714,7 +3721,7 @@ const flush = async (entryPoint) => {
|
|
|
3714
3721
|
console.log(`Removed: ${target}`);
|
|
3715
3722
|
}
|
|
3716
3723
|
};
|
|
3717
|
-
const main$
|
|
3724
|
+
const main$j = async () => {
|
|
3718
3725
|
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)))) {
|
|
3719
3726
|
return;
|
|
3720
3727
|
}
|
|
@@ -3731,7 +3738,7 @@ const main$h = async () => {
|
|
|
3731
3738
|
}
|
|
3732
3739
|
process.exit(0);
|
|
3733
3740
|
};
|
|
3734
|
-
main$
|
|
3741
|
+
main$j();
|
|
3735
3742
|
|
|
3736
3743
|
const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
|
|
3737
3744
|
process.off("SIGINT", BEFORE_EXIT_FN$6);
|
|
@@ -3754,7 +3761,7 @@ const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
|
|
|
3754
3761
|
const listenGracefulShutdown$6 = functoolsKit.singleshot(() => {
|
|
3755
3762
|
process.on("SIGINT", BEFORE_EXIT_FN$6);
|
|
3756
3763
|
});
|
|
3757
|
-
const main$
|
|
3764
|
+
const main$i = async () => {
|
|
3758
3765
|
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)))) {
|
|
3759
3766
|
return;
|
|
3760
3767
|
}
|
|
@@ -3772,7 +3779,7 @@ const main$g = async () => {
|
|
|
3772
3779
|
await cli.backtestMainService.connect();
|
|
3773
3780
|
listenGracefulShutdown$6();
|
|
3774
3781
|
};
|
|
3775
|
-
main$
|
|
3782
|
+
main$i();
|
|
3776
3783
|
|
|
3777
3784
|
const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
|
|
3778
3785
|
process.off("SIGINT", BEFORE_EXIT_FN$5);
|
|
@@ -3791,7 +3798,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
|
|
|
3791
3798
|
const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
|
|
3792
3799
|
process.on("SIGINT", BEFORE_EXIT_FN$5);
|
|
3793
3800
|
});
|
|
3794
|
-
const main$
|
|
3801
|
+
const main$h = async () => {
|
|
3795
3802
|
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)))) {
|
|
3796
3803
|
return;
|
|
3797
3804
|
}
|
|
@@ -3810,7 +3817,7 @@ const main$f = async () => {
|
|
|
3810
3817
|
listenGracefulShutdown$5();
|
|
3811
3818
|
await cli.walkerMainService.connect();
|
|
3812
3819
|
};
|
|
3813
|
-
main$
|
|
3820
|
+
main$h();
|
|
3814
3821
|
|
|
3815
3822
|
const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
3816
3823
|
process.off("SIGINT", BEFORE_EXIT_FN$4);
|
|
@@ -3832,7 +3839,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
|
3832
3839
|
const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
|
|
3833
3840
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
3834
3841
|
});
|
|
3835
|
-
const main$
|
|
3842
|
+
const main$g = async () => {
|
|
3836
3843
|
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)))) {
|
|
3837
3844
|
return;
|
|
3838
3845
|
}
|
|
@@ -3846,7 +3853,7 @@ const main$e = async () => {
|
|
|
3846
3853
|
await cli.paperMainService.connect();
|
|
3847
3854
|
listenGracefulShutdown$4();
|
|
3848
3855
|
};
|
|
3849
|
-
main$
|
|
3856
|
+
main$g();
|
|
3850
3857
|
|
|
3851
3858
|
const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
3852
3859
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -3868,7 +3875,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
|
3868
3875
|
const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
|
|
3869
3876
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
3870
3877
|
});
|
|
3871
|
-
const main$
|
|
3878
|
+
const main$f = async () => {
|
|
3872
3879
|
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)))) {
|
|
3873
3880
|
return;
|
|
3874
3881
|
}
|
|
@@ -3882,7 +3889,7 @@ const main$d = async () => {
|
|
|
3882
3889
|
await cli.liveMainService.connect();
|
|
3883
3890
|
listenGracefulShutdown$3();
|
|
3884
3891
|
};
|
|
3885
|
-
main$
|
|
3892
|
+
main$f();
|
|
3886
3893
|
|
|
3887
3894
|
const stopBacktestList$1 = async () => {
|
|
3888
3895
|
for (const item of await BacktestKit.Backtest.list()) {
|
|
@@ -3946,7 +3953,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
|
|
|
3946
3953
|
const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
|
|
3947
3954
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
3948
3955
|
});
|
|
3949
|
-
const main$
|
|
3956
|
+
const main$e = async () => {
|
|
3950
3957
|
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)))) {
|
|
3951
3958
|
return;
|
|
3952
3959
|
}
|
|
@@ -4002,7 +4009,7 @@ const main$c = async () => {
|
|
|
4002
4009
|
listenGracefulShutdown$2();
|
|
4003
4010
|
await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
|
|
4004
4011
|
};
|
|
4005
|
-
main$
|
|
4012
|
+
main$e();
|
|
4006
4013
|
|
|
4007
4014
|
const MODE_MODULE = {
|
|
4008
4015
|
backtest: "backtest.module",
|
|
@@ -4105,7 +4112,7 @@ const createGracefulShutdown = (mode) => {
|
|
|
4105
4112
|
process.on("SIGINT", handler);
|
|
4106
4113
|
});
|
|
4107
4114
|
};
|
|
4108
|
-
const main$
|
|
4115
|
+
const main$d = async () => {
|
|
4109
4116
|
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)))) {
|
|
4110
4117
|
return;
|
|
4111
4118
|
}
|
|
@@ -4181,7 +4188,7 @@ const main$b = async () => {
|
|
|
4181
4188
|
}
|
|
4182
4189
|
await getEntrySubject().next(absolutePath);
|
|
4183
4190
|
};
|
|
4184
|
-
main$
|
|
4191
|
+
main$d();
|
|
4185
4192
|
|
|
4186
4193
|
const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
4187
4194
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -4192,7 +4199,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
|
4192
4199
|
const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
|
|
4193
4200
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
4194
4201
|
});
|
|
4195
|
-
const main$
|
|
4202
|
+
const main$c = async () => {
|
|
4196
4203
|
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)))) {
|
|
4197
4204
|
return;
|
|
4198
4205
|
}
|
|
@@ -4202,7 +4209,7 @@ const main$a = async () => {
|
|
|
4202
4209
|
}
|
|
4203
4210
|
listenGracefulShutdown$1();
|
|
4204
4211
|
};
|
|
4205
|
-
main$
|
|
4212
|
+
main$c();
|
|
4206
4213
|
|
|
4207
4214
|
const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
4208
4215
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -4213,7 +4220,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
|
4213
4220
|
const listenGracefulShutdown = functoolsKit.singleshot(() => {
|
|
4214
4221
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
4215
4222
|
});
|
|
4216
|
-
const main$
|
|
4223
|
+
const main$b = async () => {
|
|
4217
4224
|
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)))) {
|
|
4218
4225
|
return;
|
|
4219
4226
|
}
|
|
@@ -4223,7 +4230,7 @@ const main$9 = async () => {
|
|
|
4223
4230
|
}
|
|
4224
4231
|
listenGracefulShutdown();
|
|
4225
4232
|
};
|
|
4226
|
-
main$
|
|
4233
|
+
main$b();
|
|
4227
4234
|
|
|
4228
4235
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
4229
4236
|
const keys = Object.keys(schema);
|
|
@@ -4245,7 +4252,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
4245
4252
|
}
|
|
4246
4253
|
return rows;
|
|
4247
4254
|
};
|
|
4248
|
-
const main$
|
|
4255
|
+
const main$a = async () => {
|
|
4249
4256
|
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)))) {
|
|
4250
4257
|
return;
|
|
4251
4258
|
}
|
|
@@ -4345,9 +4352,9 @@ const main$8 = async () => {
|
|
|
4345
4352
|
console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
|
|
4346
4353
|
process.exit(0);
|
|
4347
4354
|
};
|
|
4348
|
-
main$
|
|
4355
|
+
main$a();
|
|
4349
4356
|
|
|
4350
|
-
const main$
|
|
4357
|
+
const main$9 = async () => {
|
|
4351
4358
|
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)))) {
|
|
4352
4359
|
return;
|
|
4353
4360
|
}
|
|
@@ -4410,9 +4417,9 @@ const main$7 = async () => {
|
|
|
4410
4417
|
};
|
|
4411
4418
|
process.on("SIGINT", beforeExit);
|
|
4412
4419
|
};
|
|
4413
|
-
main$
|
|
4420
|
+
main$9();
|
|
4414
4421
|
|
|
4415
|
-
const main$
|
|
4422
|
+
const main$8 = async () => {
|
|
4416
4423
|
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)))) {
|
|
4417
4424
|
return;
|
|
4418
4425
|
}
|
|
@@ -4493,9 +4500,9 @@ const main$6 = async () => {
|
|
|
4493
4500
|
console.log(JSON.stringify(candles, null, 2));
|
|
4494
4501
|
process.exit(0);
|
|
4495
4502
|
};
|
|
4496
|
-
main$
|
|
4503
|
+
main$8();
|
|
4497
4504
|
|
|
4498
|
-
const main$
|
|
4505
|
+
const main$7 = async () => {
|
|
4499
4506
|
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)))) {
|
|
4500
4507
|
return;
|
|
4501
4508
|
}
|
|
@@ -4609,11 +4616,326 @@ const main$5 = async () => {
|
|
|
4609
4616
|
}
|
|
4610
4617
|
process.exit(0);
|
|
4611
4618
|
};
|
|
4612
|
-
main$
|
|
4619
|
+
main$7();
|
|
4613
4620
|
|
|
4614
|
-
const
|
|
4615
|
-
|
|
4616
|
-
|
|
4621
|
+
const getBroker = functoolsKit.singleshot(() => {
|
|
4622
|
+
// Reaches into BrokerAdapter internals on purpose: the public commit*
|
|
4623
|
+
// methods skip when payload.backtest=true and require enable(), while
|
|
4624
|
+
// brokerdebug must fire exactly one commit at the raw adapter proxy.
|
|
4625
|
+
const broker = BacktestKit.Broker["getInstance"]();
|
|
4626
|
+
if (!broker) {
|
|
4627
|
+
throw new Error("Broker instance is not initialized.");
|
|
4628
|
+
}
|
|
4629
|
+
return broker;
|
|
4630
|
+
});
|
|
4631
|
+
/** Called when a new signal is opened (position entry confirmed). */
|
|
4632
|
+
const commitOrderOpen = functoolsKit.trycatch(async (payload) => {
|
|
4633
|
+
const broker = getBroker();
|
|
4634
|
+
return await broker.onOrderOpenCommit(payload);
|
|
4635
|
+
}, {
|
|
4636
|
+
fallback: (error) => {
|
|
4637
|
+
console.log(error);
|
|
4638
|
+
process.exit(-1);
|
|
4639
|
+
},
|
|
4640
|
+
});
|
|
4641
|
+
/** Called when a new signal is closed (take-profit, stop-loss, or manual close). */
|
|
4642
|
+
const commitOrderClose = functoolsKit.trycatch(async (payload) => {
|
|
4643
|
+
const broker = getBroker();
|
|
4644
|
+
return await broker.onOrderCloseCommit(payload);
|
|
4645
|
+
}, {
|
|
4646
|
+
fallback: (error) => {
|
|
4647
|
+
console.log(error);
|
|
4648
|
+
process.exit(-1);
|
|
4649
|
+
},
|
|
4650
|
+
});
|
|
4651
|
+
/** Called when a partial profit close is committed. */
|
|
4652
|
+
const commitPartialProfit = functoolsKit.trycatch(async (payload) => {
|
|
4653
|
+
const broker = getBroker();
|
|
4654
|
+
return await broker.onPartialProfitCommit(payload);
|
|
4655
|
+
}, {
|
|
4656
|
+
fallback: (error) => {
|
|
4657
|
+
console.log(error);
|
|
4658
|
+
process.exit(-1);
|
|
4659
|
+
},
|
|
4660
|
+
});
|
|
4661
|
+
/** Called when a partial loss close is committed. */
|
|
4662
|
+
const commitPartialLoss = functoolsKit.trycatch(async (payload) => {
|
|
4663
|
+
const broker = getBroker();
|
|
4664
|
+
return await broker.onPartialLossCommit(payload);
|
|
4665
|
+
}, {
|
|
4666
|
+
fallback: (error) => {
|
|
4667
|
+
console.log(error);
|
|
4668
|
+
process.exit(-1);
|
|
4669
|
+
},
|
|
4670
|
+
});
|
|
4671
|
+
/** Called when a trailing stop update is committed. */
|
|
4672
|
+
const commitTrailingStop = functoolsKit.trycatch(async (payload) => {
|
|
4673
|
+
const broker = getBroker();
|
|
4674
|
+
return await broker.onTrailingStopCommit(payload);
|
|
4675
|
+
}, {
|
|
4676
|
+
fallback: (error) => {
|
|
4677
|
+
console.log(error);
|
|
4678
|
+
process.exit(-1);
|
|
4679
|
+
},
|
|
4680
|
+
});
|
|
4681
|
+
/** Called when a trailing take-profit update is committed. */
|
|
4682
|
+
const commitTrailingTake = functoolsKit.trycatch(async (payload) => {
|
|
4683
|
+
const broker = getBroker();
|
|
4684
|
+
return await broker.onTrailingTakeCommit(payload);
|
|
4685
|
+
}, {
|
|
4686
|
+
fallback: (error) => {
|
|
4687
|
+
console.log(error);
|
|
4688
|
+
process.exit(-1);
|
|
4689
|
+
},
|
|
4690
|
+
});
|
|
4691
|
+
/** Called when a breakeven stop is committed (stop loss moved to entry price). */
|
|
4692
|
+
const commitBreakeven = functoolsKit.trycatch(async (payload) => {
|
|
4693
|
+
const broker = getBroker();
|
|
4694
|
+
return await broker.onBreakevenCommit(payload);
|
|
4695
|
+
}, {
|
|
4696
|
+
fallback: (error) => {
|
|
4697
|
+
console.log(error);
|
|
4698
|
+
process.exit(-1);
|
|
4699
|
+
},
|
|
4700
|
+
});
|
|
4701
|
+
/** Called when a DCA (average-buy) entry is committed. */
|
|
4702
|
+
const commitAverageBuy = functoolsKit.trycatch(async (payload) => {
|
|
4703
|
+
const broker = getBroker();
|
|
4704
|
+
return await broker.onAverageBuyCommit(payload);
|
|
4705
|
+
}, {
|
|
4706
|
+
fallback: (error) => {
|
|
4707
|
+
console.log(error);
|
|
4708
|
+
process.exit(-1);
|
|
4709
|
+
},
|
|
4710
|
+
});
|
|
4711
|
+
const COMMITS = [
|
|
4712
|
+
"signal-open",
|
|
4713
|
+
"signal-close",
|
|
4714
|
+
"partial-profit",
|
|
4715
|
+
"partial-loss",
|
|
4716
|
+
"average-buy",
|
|
4717
|
+
"trailing-stop",
|
|
4718
|
+
"trailing-take",
|
|
4719
|
+
"breakeven",
|
|
4720
|
+
];
|
|
4721
|
+
const ZERO_PNL = {
|
|
4722
|
+
pnlPercentage: 0,
|
|
4723
|
+
priceOpen: 0,
|
|
4724
|
+
priceClose: 0,
|
|
4725
|
+
pnlCost: 0,
|
|
4726
|
+
pnlEntries: 0,
|
|
4727
|
+
};
|
|
4728
|
+
const main$6 = async () => {
|
|
4729
|
+
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
|
+
return;
|
|
4731
|
+
}
|
|
4732
|
+
const { values } = getArgs();
|
|
4733
|
+
if (!values.brokerdebug) {
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
4736
|
+
{
|
|
4737
|
+
const cwd = process.cwd();
|
|
4738
|
+
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
4739
|
+
}
|
|
4740
|
+
await cli.configConnectionService.loadConfig("setup.config");
|
|
4741
|
+
{
|
|
4742
|
+
const loader = await cli.configConnectionService.loadConfig("loader.config");
|
|
4743
|
+
try {
|
|
4744
|
+
if (typeof loader === "function") {
|
|
4745
|
+
await loader();
|
|
4746
|
+
}
|
|
4747
|
+
if (typeof loader?.loader === "function") {
|
|
4748
|
+
await loader.loader();
|
|
4749
|
+
}
|
|
4750
|
+
}
|
|
4751
|
+
catch (error) {
|
|
4752
|
+
console.error("Module loader failed", error);
|
|
4753
|
+
process.exit(-1);
|
|
4754
|
+
}
|
|
4755
|
+
}
|
|
4756
|
+
await cli.moduleConnectionService.loadModule("brokerdebug.module");
|
|
4757
|
+
const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
|
|
4758
|
+
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
4759
|
+
const symbol = values.symbol || "BTCUSDT";
|
|
4760
|
+
const commit = values.commit || "signal-open";
|
|
4761
|
+
if (!COMMITS.includes(commit)) {
|
|
4762
|
+
console.error(`Unknown --commit value: ${commit}`);
|
|
4763
|
+
console.error(`Available: ${COMMITS.join(", ")}`);
|
|
4764
|
+
process.exit(1);
|
|
4765
|
+
}
|
|
4766
|
+
const candles = await BacktestKit.Exchange.getCandles(symbol, "1m", 5, { exchangeName });
|
|
4767
|
+
const lastCandle = candles[candles.length - 1];
|
|
4768
|
+
const currentPrice = lastCandle.close;
|
|
4769
|
+
const priceTakeProfit = currentPrice * 1.02;
|
|
4770
|
+
const priceStopLoss = currentPrice * 0.98;
|
|
4771
|
+
const context = {
|
|
4772
|
+
strategyName: "broker-test",
|
|
4773
|
+
exchangeName: exchangeName,
|
|
4774
|
+
};
|
|
4775
|
+
const signalId = `broker-debug-${Date.now()}`;
|
|
4776
|
+
const when = BacktestKit.alignToInterval(new Date(), "1m");
|
|
4777
|
+
{
|
|
4778
|
+
console.log(`symbol=${symbol} price=${currentPrice} commit=${commit}`);
|
|
4779
|
+
console.time(commit);
|
|
4780
|
+
}
|
|
4781
|
+
const run = async () => {
|
|
4782
|
+
if (commit === "signal-open") {
|
|
4783
|
+
await commitOrderOpen({
|
|
4784
|
+
type: "active",
|
|
4785
|
+
symbol,
|
|
4786
|
+
signalId,
|
|
4787
|
+
cost: 100,
|
|
4788
|
+
position: "long",
|
|
4789
|
+
priceOpen: currentPrice,
|
|
4790
|
+
priceTakeProfit,
|
|
4791
|
+
priceStopLoss,
|
|
4792
|
+
pnl: ZERO_PNL,
|
|
4793
|
+
peakProfit: ZERO_PNL,
|
|
4794
|
+
maxDrawdown: ZERO_PNL,
|
|
4795
|
+
attempt: 0,
|
|
4796
|
+
context,
|
|
4797
|
+
when,
|
|
4798
|
+
backtest: true,
|
|
4799
|
+
});
|
|
4800
|
+
}
|
|
4801
|
+
if (commit === "signal-close") {
|
|
4802
|
+
await commitOrderClose({
|
|
4803
|
+
symbol,
|
|
4804
|
+
signalId,
|
|
4805
|
+
cost: 100,
|
|
4806
|
+
position: "long",
|
|
4807
|
+
currentPrice,
|
|
4808
|
+
priceOpen: currentPrice,
|
|
4809
|
+
priceTakeProfit,
|
|
4810
|
+
priceStopLoss,
|
|
4811
|
+
totalEntries: 1,
|
|
4812
|
+
totalPartials: 0,
|
|
4813
|
+
attempt: 0,
|
|
4814
|
+
pnl: ZERO_PNL,
|
|
4815
|
+
peakProfit: ZERO_PNL,
|
|
4816
|
+
maxDrawdown: ZERO_PNL,
|
|
4817
|
+
context,
|
|
4818
|
+
when,
|
|
4819
|
+
backtest: true,
|
|
4820
|
+
});
|
|
4821
|
+
}
|
|
4822
|
+
if (commit === "partial-profit") {
|
|
4823
|
+
await commitPartialProfit({
|
|
4824
|
+
symbol,
|
|
4825
|
+
signalId,
|
|
4826
|
+
percentToClose: 50,
|
|
4827
|
+
cost: 50,
|
|
4828
|
+
currentPrice,
|
|
4829
|
+
position: "long",
|
|
4830
|
+
priceTakeProfit,
|
|
4831
|
+
priceStopLoss,
|
|
4832
|
+
context,
|
|
4833
|
+
when,
|
|
4834
|
+
backtest: true,
|
|
4835
|
+
});
|
|
4836
|
+
}
|
|
4837
|
+
if (commit === "partial-loss") {
|
|
4838
|
+
await commitPartialLoss({
|
|
4839
|
+
symbol,
|
|
4840
|
+
signalId,
|
|
4841
|
+
percentToClose: 50,
|
|
4842
|
+
cost: 50,
|
|
4843
|
+
currentPrice,
|
|
4844
|
+
position: "long",
|
|
4845
|
+
priceTakeProfit,
|
|
4846
|
+
priceStopLoss,
|
|
4847
|
+
context,
|
|
4848
|
+
when,
|
|
4849
|
+
backtest: true,
|
|
4850
|
+
});
|
|
4851
|
+
}
|
|
4852
|
+
if (commit === "average-buy") {
|
|
4853
|
+
await commitAverageBuy({
|
|
4854
|
+
symbol,
|
|
4855
|
+
signalId,
|
|
4856
|
+
currentPrice,
|
|
4857
|
+
cost: 100,
|
|
4858
|
+
position: "long",
|
|
4859
|
+
priceTakeProfit,
|
|
4860
|
+
priceStopLoss,
|
|
4861
|
+
context,
|
|
4862
|
+
when,
|
|
4863
|
+
backtest: true,
|
|
4864
|
+
});
|
|
4865
|
+
}
|
|
4866
|
+
if (commit === "trailing-stop") {
|
|
4867
|
+
await commitTrailingStop({
|
|
4868
|
+
symbol,
|
|
4869
|
+
signalId,
|
|
4870
|
+
percentShift: -50,
|
|
4871
|
+
currentPrice,
|
|
4872
|
+
newStopLossPrice: priceStopLoss + (currentPrice - priceStopLoss) * 0.5,
|
|
4873
|
+
takeProfitPrice: priceTakeProfit,
|
|
4874
|
+
position: "long",
|
|
4875
|
+
context,
|
|
4876
|
+
when,
|
|
4877
|
+
backtest: true,
|
|
4878
|
+
});
|
|
4879
|
+
}
|
|
4880
|
+
if (commit === "trailing-take") {
|
|
4881
|
+
await commitTrailingTake({
|
|
4882
|
+
symbol,
|
|
4883
|
+
signalId,
|
|
4884
|
+
percentShift: 50,
|
|
4885
|
+
currentPrice,
|
|
4886
|
+
newTakeProfitPrice: priceTakeProfit + (priceTakeProfit - currentPrice) * 0.5,
|
|
4887
|
+
takeProfitPrice: priceTakeProfit,
|
|
4888
|
+
position: "long",
|
|
4889
|
+
context,
|
|
4890
|
+
when,
|
|
4891
|
+
backtest: true,
|
|
4892
|
+
});
|
|
4893
|
+
}
|
|
4894
|
+
if (commit === "breakeven") {
|
|
4895
|
+
await commitBreakeven({
|
|
4896
|
+
symbol,
|
|
4897
|
+
signalId,
|
|
4898
|
+
currentPrice,
|
|
4899
|
+
newStopLossPrice: currentPrice,
|
|
4900
|
+
newTakeProfitPrice: priceTakeProfit,
|
|
4901
|
+
position: "long",
|
|
4902
|
+
context,
|
|
4903
|
+
when,
|
|
4904
|
+
backtest: true,
|
|
4905
|
+
});
|
|
4906
|
+
}
|
|
4907
|
+
};
|
|
4908
|
+
await BacktestKit.runInMockContext(run, {
|
|
4909
|
+
when,
|
|
4910
|
+
strategyName: context.strategyName,
|
|
4911
|
+
exchangeName: context.exchangeName,
|
|
4912
|
+
frameName: "",
|
|
4913
|
+
symbol,
|
|
4914
|
+
backtest: true,
|
|
4915
|
+
});
|
|
4916
|
+
{
|
|
4917
|
+
console.log(`${commit} OK`);
|
|
4918
|
+
console.timeEnd(commit);
|
|
4919
|
+
}
|
|
4920
|
+
process.exit(0);
|
|
4921
|
+
};
|
|
4922
|
+
main$6();
|
|
4923
|
+
|
|
4924
|
+
const SIMULATOR_NAME$1 = "cli_simulator";
|
|
4925
|
+
const CONFIG_KEYS$1 = ["gridAxes", "reportOrder"];
|
|
4926
|
+
const validateConfig$1 = (config) => {
|
|
4927
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
4928
|
+
return "config must be a JSON object";
|
|
4929
|
+
}
|
|
4930
|
+
for (const key of Object.keys(config)) {
|
|
4931
|
+
if (!CONFIG_KEYS$1.includes(key)) {
|
|
4932
|
+
return `unknown key "${key}" (expected: ${CONFIG_KEYS$1.join(", ")})`;
|
|
4933
|
+
}
|
|
4934
|
+
}
|
|
4935
|
+
return null;
|
|
4936
|
+
};
|
|
4937
|
+
const IDEA_DIRECTIONS$1 = ["LONG", "SHORT", "NEUTRAL"];
|
|
4938
|
+
const validateIdea$1 = (idea, line) => {
|
|
4617
4939
|
if (typeof idea !== "object" || idea === null) {
|
|
4618
4940
|
return `line ${line}: not an object`;
|
|
4619
4941
|
}
|
|
@@ -4626,15 +4948,18 @@ const validateIdea = (idea, line) => {
|
|
|
4626
4948
|
if (typeof idea.symbol !== "string" || !idea.symbol) {
|
|
4627
4949
|
return `line ${line}: "symbol" must be a non-empty string`;
|
|
4628
4950
|
}
|
|
4629
|
-
if (!IDEA_DIRECTIONS.includes(idea.direction)) {
|
|
4630
|
-
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
4951
|
+
if (!IDEA_DIRECTIONS$1.includes(idea.direction)) {
|
|
4952
|
+
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS$1.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
4631
4953
|
}
|
|
4632
4954
|
if (typeof idea.author !== "string" || !idea.author) {
|
|
4633
4955
|
return `line ${line}: "author" must be a non-empty string`;
|
|
4634
4956
|
}
|
|
4635
4957
|
return null;
|
|
4636
4958
|
};
|
|
4637
|
-
const toMarkdown = (result) => {
|
|
4959
|
+
const toMarkdown$1 = (result) => {
|
|
4960
|
+
const buckets = Object.entries(result.reports).filter(([, bucket]) => bucket.reports.length > 0);
|
|
4961
|
+
const allReports = buckets.flatMap(([, bucket]) => bucket.reports);
|
|
4962
|
+
const profitable = allReports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
|
|
4638
4963
|
const lines = [];
|
|
4639
4964
|
lines.push(`# Simulator Report — ${result.symbol}`);
|
|
4640
4965
|
lines.push("");
|
|
@@ -4642,34 +4967,38 @@ const toMarkdown = (result) => {
|
|
|
4642
4967
|
lines.push(`| --- | --- |`);
|
|
4643
4968
|
lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
|
|
4644
4969
|
lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
|
|
4645
|
-
lines.push(`|
|
|
4646
|
-
lines.push(`| Grid points | ${result.reports.length} |`);
|
|
4970
|
+
lines.push(`| Profitable corridor | ${profitable} / ${allReports.length} grid points |`);
|
|
4647
4971
|
lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
|
|
4648
|
-
|
|
4649
|
-
|
|
4650
|
-
|
|
4651
|
-
|
|
4652
|
-
|
|
4653
|
-
|
|
4654
|
-
|
|
4655
|
-
|
|
4656
|
-
|
|
4972
|
+
// каждая метрика — самодостаточная корзина: свои победители и
|
|
4973
|
+
// свои словари банов, между собой не склеиваются
|
|
4974
|
+
for (const [metric, bucket] of buckets) {
|
|
4975
|
+
lines.push("");
|
|
4976
|
+
lines.push(`## Metric: ${metric}`);
|
|
4977
|
+
lines.push("");
|
|
4978
|
+
lines.push(`| Criterion | Stop% | Hold | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
|
|
4979
|
+
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
|
|
4980
|
+
for (const best of bucket.best) {
|
|
4981
|
+
if (!best.report) {
|
|
4982
|
+
lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
|
|
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)}% |`);
|
|
4657
4997
|
}
|
|
4658
|
-
const { point } = best.report;
|
|
4659
|
-
lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.trailingTakePercent} | ${point.holdMinutes / 60}h | ${point.minIdeasAligned} | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
|
|
4660
|
-
`${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
|
|
4661
|
-
}
|
|
4662
|
-
lines.push("");
|
|
4663
|
-
lines.push(`## Allowed authors (production whitelist)`);
|
|
4664
|
-
lines.push("");
|
|
4665
|
-
lines.push(`| Author | Ideas | Hits | HitRate |`);
|
|
4666
|
-
lines.push(`| --- | --- | --- | --- |`);
|
|
4667
|
-
for (const stat of result.authorStats.filter(({ banned }) => !banned)) {
|
|
4668
|
-
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
|
|
4669
4998
|
}
|
|
4670
4999
|
return lines.join("\n");
|
|
4671
5000
|
};
|
|
4672
|
-
const main$
|
|
5001
|
+
const main$5 = async () => {
|
|
4673
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)))) {
|
|
4674
5003
|
return;
|
|
4675
5004
|
}
|
|
@@ -4682,6 +5011,33 @@ const main$4 = async () => {
|
|
|
4682
5011
|
console.error("Error: positional path to an ideas .jsonl file is required");
|
|
4683
5012
|
process.exit(1);
|
|
4684
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$1(config);
|
|
5035
|
+
if (problem) {
|
|
5036
|
+
console.error(`Error: simulator config does not match the structure — ${problem}`);
|
|
5037
|
+
console.error(`Expected shape: { "gridAxes"?: ISimulatorGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }`);
|
|
5038
|
+
process.exit(1);
|
|
5039
|
+
}
|
|
5040
|
+
}
|
|
4685
5041
|
let ideas = [];
|
|
4686
5042
|
{
|
|
4687
5043
|
let content;
|
|
@@ -4706,7 +5062,7 @@ const main$4 = async () => {
|
|
|
4706
5062
|
console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
|
|
4707
5063
|
process.exit(1);
|
|
4708
5064
|
}
|
|
4709
|
-
const problem = validateIdea(idea, i + 1);
|
|
5065
|
+
const problem = validateIdea$1(idea, i + 1);
|
|
4710
5066
|
if (problem) {
|
|
4711
5067
|
console.error(`Error: ideas file does not match the idea structure — ${problem}`);
|
|
4712
5068
|
console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
|
|
@@ -4742,20 +5098,24 @@ const main$4 = async () => {
|
|
|
4742
5098
|
}
|
|
4743
5099
|
const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
|
|
4744
5100
|
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
5101
|
+
if (values.verbose) {
|
|
5102
|
+
BacktestKit.overrideExchangeSchema({
|
|
5103
|
+
exchangeName,
|
|
5104
|
+
callbacks: {
|
|
5105
|
+
onCandleData(symbol, interval, since) {
|
|
5106
|
+
console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
|
|
5107
|
+
},
|
|
5108
|
+
},
|
|
5109
|
+
});
|
|
5110
|
+
}
|
|
4745
5111
|
const symbol = values.symbol || "BTCUSDT";
|
|
5112
|
+
// оси и порядок отчёта приходят позиционным конфигом потребителя;
|
|
5113
|
+
// пустой конфиг — дефолтная сетка движка из connection-сервиса
|
|
4746
5114
|
BacktestKit.addSimulatorSchema({
|
|
4747
|
-
simulatorName: SIMULATOR_NAME,
|
|
5115
|
+
simulatorName: SIMULATOR_NAME$1,
|
|
4748
5116
|
exchangeName,
|
|
4749
|
-
gridAxes: {
|
|
4750
|
-
|
|
4751
|
-
trailingTakePercent: [0.5, 1, 1.5, 2, 3, 4],
|
|
4752
|
-
holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
|
|
4753
|
-
minIdeasAligned: [1, 2, 3],
|
|
4754
|
-
minAuthorTrack: [2, 3, 5],
|
|
4755
|
-
minAuthorHitRate: [0.5, 0.6],
|
|
4756
|
-
minWeightAligned: [0, 0.6, 1.2],
|
|
4757
|
-
profitLockPercent: [0, 1.5, 2.5],
|
|
4758
|
-
},
|
|
5117
|
+
...(config.gridAxes ? { gridAxes: config.gridAxes } : {}),
|
|
5118
|
+
...(config.reportOrder ? { reportOrder: config.reportOrder } : {}),
|
|
4759
5119
|
callbacks: {
|
|
4760
5120
|
onProgress: (symbol, stage, processed, total) => {
|
|
4761
5121
|
if (values.verbose) {
|
|
@@ -4807,9 +5167,8 @@ const main$4 = async () => {
|
|
|
4807
5167
|
if (values.verbose) {
|
|
4808
5168
|
console.log("onDone", {
|
|
4809
5169
|
symbol,
|
|
4810
|
-
reports: result.reports.length,
|
|
4811
|
-
|
|
4812
|
-
bannedAuthors: result.bannedAuthors.length,
|
|
5170
|
+
reports: Object.values(result.reports).flatMap((bucket) => bucket.reports).length,
|
|
5171
|
+
bans: Object.values(result.reports).flatMap((bucket) => bucket.bans).length,
|
|
4813
5172
|
});
|
|
4814
5173
|
}
|
|
4815
5174
|
},
|
|
@@ -4817,7 +5176,7 @@ const main$4 = async () => {
|
|
|
4817
5176
|
});
|
|
4818
5177
|
const result = await BacktestKit.Simulator.run({
|
|
4819
5178
|
symbol,
|
|
4820
|
-
simulatorName: SIMULATOR_NAME,
|
|
5179
|
+
simulatorName: SIMULATOR_NAME$1,
|
|
4821
5180
|
ideas,
|
|
4822
5181
|
});
|
|
4823
5182
|
const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
|
|
@@ -4832,11 +5191,308 @@ const main$4 = async () => {
|
|
|
4832
5191
|
if (values.markdown) {
|
|
4833
5192
|
const filePath = path.resolve(dumpDir, `${dumpName}.md`);
|
|
4834
5193
|
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
4835
|
-
await fs$1.writeFile(filePath, toMarkdown(result), "utf-8");
|
|
5194
|
+
await fs$1.writeFile(filePath, toMarkdown$1(result), "utf-8");
|
|
4836
5195
|
console.log(`Saved: ${filePath}`);
|
|
4837
5196
|
process.exit(0);
|
|
4838
5197
|
}
|
|
4839
|
-
console.log(toMarkdown(result));
|
|
5198
|
+
console.log(toMarkdown$1(result));
|
|
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));
|
|
4840
5496
|
process.exit(0);
|
|
4841
5497
|
};
|
|
4842
5498
|
main$4();
|
|
@@ -5037,7 +5693,8 @@ Modes:
|
|
|
5037
5693
|
--dump Fetch and save raw OHLCV candles
|
|
5038
5694
|
--pnldebug Simulate PnL per minute for a given entry price and direction
|
|
5039
5695
|
--brokerdebug Fire a single broker commit against the live broker adapter
|
|
5040
|
-
--simulator <ideas.jsonl>
|
|
5696
|
+
--simulator <ideas.jsonl> [config.json] Feasibility probe over crowd ideas: is there a profitable corridor at all
|
|
5697
|
+
--tune <ideas.jsonl> <config.json> ONE out-of-sample shot of a frozen training artifact (point + author track)
|
|
5041
5698
|
--flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
|
|
5042
5699
|
--init Scaffold a new project in the current directory
|
|
5043
5700
|
--docker Scaffold a Docker workspace for running strategies in a container
|
|
@@ -5167,16 +5824,22 @@ Simulator flags (--simulator):
|
|
|
5167
5824
|
--markdown Save summary report to ./dump/<output>.md
|
|
5168
5825
|
--verbose Log every simulator lifecycle callback to the console
|
|
5169
5826
|
|
|
5170
|
-
|
|
5827
|
+
Positionals: path to an ideas .jsonl file — one idea per line with the exact shape
|
|
5171
5828
|
{ "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL",
|
|
5172
|
-
"author": string }
|
|
5173
|
-
|
|
5829
|
+
"author": string } — and an OPTIONAL config .json with the shape
|
|
5830
|
+
{ "gridAxes"?: ISimulatorGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }.
|
|
5831
|
+
Both files are validated BEFORE any work; a mismatch aborts the run with an error.
|
|
5832
|
+
No config -> an empty object is used and the engine defaults apply (the full
|
|
5833
|
+
default grid axes and reportOrder "sharpe" of the connection service).
|
|
5174
5834
|
|
|
5175
|
-
|
|
5176
|
-
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5835
|
+
A FEASIBILITY PROBE, not an out-of-sample shot (that is --tune): the grid from
|
|
5836
|
+
the config (or the engine default) answers one question — does the feed contain
|
|
5837
|
+
a profitable corridor at all. One candle pass per idea to the grid's longest
|
|
5838
|
+
hold, flood dedupe (one idea per author per direction per 8h), default-ban
|
|
5839
|
+
author filter graded inside each point's own hold window, time-based
|
|
5840
|
+
Sharpe/Sortino, per-metric buckets with their own ranking winners and ban
|
|
5841
|
+
dictionaries. Ideas of other symbols are filtered out — one shared feed serves
|
|
5842
|
+
any --symbol.
|
|
5180
5843
|
|
|
5181
5844
|
No output flag → print the Markdown summary to stdout. With --verbose every
|
|
5182
5845
|
simulator lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
|
|
@@ -5185,6 +5848,33 @@ Simulator flags (--simulator):
|
|
|
5185
5848
|
Module file ./modules/simulator.module is loaded automatically if it exists
|
|
5186
5849
|
(register your exchange there); without it CCXT Binance is used by default.
|
|
5187
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
|
|
5876
|
+
(register your exchange there); without it CCXT Binance is used by default.
|
|
5877
|
+
|
|
5188
5878
|
Flush flags (--flush):
|
|
5189
5879
|
|
|
5190
5880
|
One or more positional entry points. For each entry point the following
|
|
@@ -5218,7 +5908,8 @@ Module hooks (loaded automatically by each mode):
|
|
|
5218
5908
|
modules/dump.module --dump Exchange schema for candle dumps
|
|
5219
5909
|
modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
|
|
5220
5910
|
modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
|
|
5221
|
-
modules/simulator.module --simulator Exchange schema for crowd-ideas
|
|
5911
|
+
modules/simulator.module --simulator Exchange schema for the crowd-ideas feasibility probe
|
|
5912
|
+
modules/tune.module --tune Exchange schema for the walk-forward parameter search
|
|
5222
5913
|
|
|
5223
5914
|
--flush has no associated module. It only removes dump subdirectories.
|
|
5224
5915
|
|
|
@@ -5248,7 +5939,9 @@ Examples:
|
|
|
5248
5939
|
node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
|
|
5249
5940
|
node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
|
|
5250
5941
|
node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
|
|
5251
|
-
node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output
|
|
5942
|
+
node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_probe ./assets/tv-ideas.normalized.jsonl ./assets/probe.config.json
|
|
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
|
|
5252
5945
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
|
|
5253
5946
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
|
|
5254
5947
|
node ${ENTRY_PATH} --init --output my-trading-bot
|
|
@@ -5262,7 +5955,7 @@ const main$1 = async () => {
|
|
|
5262
5955
|
if (!values.help) {
|
|
5263
5956
|
return;
|
|
5264
5957
|
}
|
|
5265
|
-
process.stdout.write(`@backtest-kit/cli ${"
|
|
5958
|
+
process.stdout.write(`@backtest-kit/cli ${"17.0.0"}\n\n`);
|
|
5266
5959
|
process.stdout.write(HELP_TEXT);
|
|
5267
5960
|
process.exit(0);
|
|
5268
5961
|
};
|
|
@@ -5276,7 +5969,7 @@ const main = async () => {
|
|
|
5276
5969
|
if (!values.version) {
|
|
5277
5970
|
return;
|
|
5278
5971
|
}
|
|
5279
|
-
process.stdout.write(`@backtest-kit/cli ${"
|
|
5972
|
+
process.stdout.write(`@backtest-kit/cli ${"17.0.0"}\n`);
|
|
5280
5973
|
process.exit(0);
|
|
5281
5974
|
};
|
|
5282
5975
|
main();
|