@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.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as BacktestKit from 'backtest-kit';
|
|
3
|
-
import { setConfig, listenExit, emitters, Notification, Cron, Recent, Storage, Markdown, Report, Dump, State, Memory, MarkdownWriter, ReportWriter, PersistSignalAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistPartialAdapter, PersistBreakevenAdapter, PersistCandleAdapter, PersistStorageAdapter, PersistNotificationAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistIntervalAdapter, PersistMemoryAdapter, PersistRecentAdapter, PersistStateAdapter, PersistSessionAdapter, PersistStrategyAdapter, Log, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, RecentLive, RecentBacktest, Broker, SessionLive, SessionBacktest, MemoryLive, MemoryBacktest, StateLive, StateBacktest, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, System, Cache, Interval, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenOrderFill, listenOrderReject, listenOrderContinue, listenOrderStop, listenSignalNotify, Exchange, addSimulatorSchema, Simulator } from 'backtest-kit';
|
|
4
|
-
import { singleshot, sleep, getErrorMessage, errorData, BehaviorSubject, Subject, str, compose, createAwaiter, execpool, queued, randomString, TIMEOUT_SYMBOL, typo, retry, trycatch, ttl, memoize, isObject } from 'functools-kit';
|
|
3
|
+
import { setConfig, listenExit, emitters, Notification, Cron, Recent, Storage, Markdown, Report, Dump, State, Memory, MarkdownWriter, ReportWriter, PersistSignalAdapter, PersistRiskAdapter, PersistScheduleAdapter, PersistPartialAdapter, PersistBreakevenAdapter, PersistCandleAdapter, PersistStorageAdapter, PersistNotificationAdapter, PersistLogAdapter, PersistMeasureAdapter, PersistIntervalAdapter, PersistMemoryAdapter, PersistRecentAdapter, PersistStateAdapter, PersistSessionAdapter, PersistStrategyAdapter, Log, StorageLive, StorageBacktest, NotificationLive, NotificationBacktest, RecentLive, RecentBacktest, Broker, SessionLive, SessionBacktest, MemoryLive, MemoryBacktest, StateLive, StateBacktest, listExchangeSchema, addExchangeSchema, roundTicks, listFrameSchema, addFrameSchema, listenDoneLive, listenDoneBacktest, shutdown, listenSignal, listStrategySchema, overrideExchangeSchema, Backtest, System, Cache, Interval, alignToInterval, addWalkerSchema, overrideWalkerSchema, Walker, listenDoneWalker, Live, getCandles, checkCandles, warmCandles, listenRisk, listenStrategyCommit, listenOrderFill, listenOrderReject, listenOrderContinue, listenOrderStop, listenSignalNotify, Exchange, runInMockContext, addSimulatorSchema, Simulator } from 'backtest-kit';
|
|
4
|
+
import { singleshot, sleep, getErrorMessage, errorData, BehaviorSubject, Subject, str, compose, createAwaiter, execpool, queued, randomString, TIMEOUT_SYMBOL, typo, retry, trycatch, ttl, Source, memoize, isObject } from 'functools-kit';
|
|
5
5
|
import { exec, spawn } from 'child_process';
|
|
6
6
|
import fs, { constants, realpathSync } from 'fs';
|
|
7
7
|
import * as stackTrace from 'stack-trace';
|
|
@@ -28,7 +28,7 @@ import Module, { createRequire } from 'module';
|
|
|
28
28
|
import * as BacktestKitGraph from '@backtest-kit/graph';
|
|
29
29
|
import * as BacktestKitOllama from '@backtest-kit/ollama';
|
|
30
30
|
import * as BacktestKitPinets from '@backtest-kit/pinets';
|
|
31
|
-
import { run as run$1, Code, toMarkdown as toMarkdown$
|
|
31
|
+
import { run as run$1, Code, toMarkdown as toMarkdown$2 } from '@backtest-kit/pinets';
|
|
32
32
|
import * as BacktestKitSignals from '@backtest-kit/signals';
|
|
33
33
|
import open from 'open';
|
|
34
34
|
|
|
@@ -208,14 +208,14 @@ const getReadySubject = singleshot(() => {
|
|
|
208
208
|
return new Subject();
|
|
209
209
|
});
|
|
210
210
|
|
|
211
|
-
const main$
|
|
211
|
+
const main$l = () => {
|
|
212
212
|
const entrySubject = getEntrySubject();
|
|
213
213
|
entrySubject.subscribe(async (path) => {
|
|
214
214
|
console.log("Running", path);
|
|
215
215
|
await getReadySubject().next();
|
|
216
216
|
});
|
|
217
217
|
};
|
|
218
|
-
main$
|
|
218
|
+
main$l();
|
|
219
219
|
|
|
220
220
|
const ERROR_HANDLER_INSTALLED = Symbol.for("error-handler-installed");
|
|
221
221
|
function dumpStackTrace(error) {
|
|
@@ -870,6 +870,10 @@ const getArgs = singleshot(() => {
|
|
|
870
870
|
type: "boolean",
|
|
871
871
|
default: false,
|
|
872
872
|
},
|
|
873
|
+
tune: {
|
|
874
|
+
type: "boolean",
|
|
875
|
+
default: false,
|
|
876
|
+
},
|
|
873
877
|
commit: {
|
|
874
878
|
type: "string",
|
|
875
879
|
default: "",
|
|
@@ -2702,8 +2706,11 @@ class TelegramLogicService {
|
|
|
2702
2706
|
const unSignalNotify = listenSignalNotify(async (event) => {
|
|
2703
2707
|
await this.notifySignalInfo(event);
|
|
2704
2708
|
});
|
|
2709
|
+
const unGc = Source.fromInterval(90000).connect(() => {
|
|
2710
|
+
this.notifyOrderContinue.gc();
|
|
2711
|
+
});
|
|
2705
2712
|
const unConnect = () => this.connect.clear();
|
|
2706
|
-
const unListen = compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unSignalNotify(), () => unConnect());
|
|
2713
|
+
const unListen = compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unGc(), () => unSignalNotify(), () => unConnect());
|
|
2707
2714
|
return () => {
|
|
2708
2715
|
STOP_BOT_FN();
|
|
2709
2716
|
unListen();
|
|
@@ -3644,14 +3651,14 @@ const cli = {
|
|
|
3644
3651
|
};
|
|
3645
3652
|
init();
|
|
3646
3653
|
|
|
3647
|
-
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "flush", "init", "docker", "help", "version"];
|
|
3654
|
+
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "tune", "flush", "init", "docker", "help", "version"];
|
|
3648
3655
|
const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
|
|
3649
3656
|
const HELP_TEXT$1 = `
|
|
3650
3657
|
Example:
|
|
3651
3658
|
|
|
3652
3659
|
node ${ENTRY_PATH$1} --help
|
|
3653
3660
|
`.trimStart();
|
|
3654
|
-
const main$
|
|
3661
|
+
const main$k = async () => {
|
|
3655
3662
|
if (!getEntry(import.meta.url)) {
|
|
3656
3663
|
return;
|
|
3657
3664
|
}
|
|
@@ -3659,14 +3666,14 @@ const main$i = async () => {
|
|
|
3659
3666
|
if (MODES.some((mode) => values[mode])) {
|
|
3660
3667
|
return;
|
|
3661
3668
|
}
|
|
3662
|
-
process.stdout.write(`@backtest-kit/cli ${"
|
|
3669
|
+
process.stdout.write(`@backtest-kit/cli ${"17.0.0"}\n`);
|
|
3663
3670
|
process.stdout.write("\n");
|
|
3664
3671
|
process.stdout.write(`Run with --help to see available commands.\n`);
|
|
3665
3672
|
process.stdout.write("\n");
|
|
3666
3673
|
process.stdout.write(HELP_TEXT$1);
|
|
3667
3674
|
process.exit(0);
|
|
3668
3675
|
};
|
|
3669
|
-
main$
|
|
3676
|
+
main$k();
|
|
3670
3677
|
|
|
3671
3678
|
const notifyShutdown = singleshot(async () => {
|
|
3672
3679
|
console.log("Graceful shutdown initiated.");
|
|
@@ -3682,7 +3689,7 @@ const flush = async (entryPoint) => {
|
|
|
3682
3689
|
console.log(`Removed: ${target}`);
|
|
3683
3690
|
}
|
|
3684
3691
|
};
|
|
3685
|
-
const main$
|
|
3692
|
+
const main$j = async () => {
|
|
3686
3693
|
if (!getEntry(import.meta.url)) {
|
|
3687
3694
|
return;
|
|
3688
3695
|
}
|
|
@@ -3699,7 +3706,7 @@ const main$h = async () => {
|
|
|
3699
3706
|
}
|
|
3700
3707
|
process.exit(0);
|
|
3701
3708
|
};
|
|
3702
|
-
main$
|
|
3709
|
+
main$j();
|
|
3703
3710
|
|
|
3704
3711
|
const BEFORE_EXIT_FN$6 = singleshot(async () => {
|
|
3705
3712
|
process.off("SIGINT", BEFORE_EXIT_FN$6);
|
|
@@ -3722,7 +3729,7 @@ const BEFORE_EXIT_FN$6 = singleshot(async () => {
|
|
|
3722
3729
|
const listenGracefulShutdown$6 = singleshot(() => {
|
|
3723
3730
|
process.on("SIGINT", BEFORE_EXIT_FN$6);
|
|
3724
3731
|
});
|
|
3725
|
-
const main$
|
|
3732
|
+
const main$i = async () => {
|
|
3726
3733
|
if (!getEntry(import.meta.url)) {
|
|
3727
3734
|
return;
|
|
3728
3735
|
}
|
|
@@ -3740,7 +3747,7 @@ const main$g = async () => {
|
|
|
3740
3747
|
await cli.backtestMainService.connect();
|
|
3741
3748
|
listenGracefulShutdown$6();
|
|
3742
3749
|
};
|
|
3743
|
-
main$
|
|
3750
|
+
main$i();
|
|
3744
3751
|
|
|
3745
3752
|
const BEFORE_EXIT_FN$5 = singleshot(async () => {
|
|
3746
3753
|
process.off("SIGINT", BEFORE_EXIT_FN$5);
|
|
@@ -3759,7 +3766,7 @@ const BEFORE_EXIT_FN$5 = singleshot(async () => {
|
|
|
3759
3766
|
const listenGracefulShutdown$5 = singleshot(() => {
|
|
3760
3767
|
process.on("SIGINT", BEFORE_EXIT_FN$5);
|
|
3761
3768
|
});
|
|
3762
|
-
const main$
|
|
3769
|
+
const main$h = async () => {
|
|
3763
3770
|
if (!getEntry(import.meta.url)) {
|
|
3764
3771
|
return;
|
|
3765
3772
|
}
|
|
@@ -3778,7 +3785,7 @@ const main$f = async () => {
|
|
|
3778
3785
|
listenGracefulShutdown$5();
|
|
3779
3786
|
await cli.walkerMainService.connect();
|
|
3780
3787
|
};
|
|
3781
|
-
main$
|
|
3788
|
+
main$h();
|
|
3782
3789
|
|
|
3783
3790
|
const BEFORE_EXIT_FN$4 = singleshot(async () => {
|
|
3784
3791
|
process.off("SIGINT", BEFORE_EXIT_FN$4);
|
|
@@ -3800,7 +3807,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
|
|
|
3800
3807
|
const listenGracefulShutdown$4 = singleshot(() => {
|
|
3801
3808
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
3802
3809
|
});
|
|
3803
|
-
const main$
|
|
3810
|
+
const main$g = async () => {
|
|
3804
3811
|
if (!getEntry(import.meta.url)) {
|
|
3805
3812
|
return;
|
|
3806
3813
|
}
|
|
@@ -3814,7 +3821,7 @@ const main$e = async () => {
|
|
|
3814
3821
|
await cli.paperMainService.connect();
|
|
3815
3822
|
listenGracefulShutdown$4();
|
|
3816
3823
|
};
|
|
3817
|
-
main$
|
|
3824
|
+
main$g();
|
|
3818
3825
|
|
|
3819
3826
|
const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
3820
3827
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -3836,7 +3843,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
|
3836
3843
|
const listenGracefulShutdown$3 = singleshot(() => {
|
|
3837
3844
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
3838
3845
|
});
|
|
3839
|
-
const main$
|
|
3846
|
+
const main$f = async () => {
|
|
3840
3847
|
if (!getEntry(import.meta.url)) {
|
|
3841
3848
|
return;
|
|
3842
3849
|
}
|
|
@@ -3850,7 +3857,7 @@ const main$d = async () => {
|
|
|
3850
3857
|
await cli.liveMainService.connect();
|
|
3851
3858
|
listenGracefulShutdown$3();
|
|
3852
3859
|
};
|
|
3853
|
-
main$
|
|
3860
|
+
main$f();
|
|
3854
3861
|
|
|
3855
3862
|
const stopBacktestList$1 = async () => {
|
|
3856
3863
|
for (const item of await Backtest.list()) {
|
|
@@ -3914,7 +3921,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
|
|
|
3914
3921
|
const listenGracefulShutdown$2 = singleshot(() => {
|
|
3915
3922
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
3916
3923
|
});
|
|
3917
|
-
const main$
|
|
3924
|
+
const main$e = async () => {
|
|
3918
3925
|
if (!getEntry(import.meta.url)) {
|
|
3919
3926
|
return;
|
|
3920
3927
|
}
|
|
@@ -3970,7 +3977,7 @@ const main$c = async () => {
|
|
|
3970
3977
|
listenGracefulShutdown$2();
|
|
3971
3978
|
await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
|
|
3972
3979
|
};
|
|
3973
|
-
main$
|
|
3980
|
+
main$e();
|
|
3974
3981
|
|
|
3975
3982
|
const MODE_MODULE = {
|
|
3976
3983
|
backtest: "backtest.module",
|
|
@@ -4073,7 +4080,7 @@ const createGracefulShutdown = (mode) => {
|
|
|
4073
4080
|
process.on("SIGINT", handler);
|
|
4074
4081
|
});
|
|
4075
4082
|
};
|
|
4076
|
-
const main$
|
|
4083
|
+
const main$d = async () => {
|
|
4077
4084
|
if (!getEntry(import.meta.url)) {
|
|
4078
4085
|
return;
|
|
4079
4086
|
}
|
|
@@ -4149,7 +4156,7 @@ const main$b = async () => {
|
|
|
4149
4156
|
}
|
|
4150
4157
|
await getEntrySubject().next(absolutePath);
|
|
4151
4158
|
};
|
|
4152
|
-
main$
|
|
4159
|
+
main$d();
|
|
4153
4160
|
|
|
4154
4161
|
const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
4155
4162
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -4160,7 +4167,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
|
4160
4167
|
const listenGracefulShutdown$1 = singleshot(() => {
|
|
4161
4168
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
4162
4169
|
});
|
|
4163
|
-
const main$
|
|
4170
|
+
const main$c = async () => {
|
|
4164
4171
|
if (!getEntry(import.meta.url)) {
|
|
4165
4172
|
return;
|
|
4166
4173
|
}
|
|
@@ -4170,7 +4177,7 @@ const main$a = async () => {
|
|
|
4170
4177
|
}
|
|
4171
4178
|
listenGracefulShutdown$1();
|
|
4172
4179
|
};
|
|
4173
|
-
main$
|
|
4180
|
+
main$c();
|
|
4174
4181
|
|
|
4175
4182
|
const BEFORE_EXIT_FN = singleshot(async () => {
|
|
4176
4183
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -4181,7 +4188,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
|
|
|
4181
4188
|
const listenGracefulShutdown = singleshot(() => {
|
|
4182
4189
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
4183
4190
|
});
|
|
4184
|
-
const main$
|
|
4191
|
+
const main$b = async () => {
|
|
4185
4192
|
if (!getEntry(import.meta.url)) {
|
|
4186
4193
|
return;
|
|
4187
4194
|
}
|
|
@@ -4191,7 +4198,7 @@ const main$9 = async () => {
|
|
|
4191
4198
|
}
|
|
4192
4199
|
listenGracefulShutdown();
|
|
4193
4200
|
};
|
|
4194
|
-
main$
|
|
4201
|
+
main$b();
|
|
4195
4202
|
|
|
4196
4203
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
4197
4204
|
const keys = Object.keys(schema);
|
|
@@ -4213,7 +4220,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
4213
4220
|
}
|
|
4214
4221
|
return rows;
|
|
4215
4222
|
};
|
|
4216
|
-
const main$
|
|
4223
|
+
const main$a = async () => {
|
|
4217
4224
|
if (!getEntry(import.meta.url)) {
|
|
4218
4225
|
return;
|
|
4219
4226
|
}
|
|
@@ -4305,17 +4312,17 @@ const main$8 = async () => {
|
|
|
4305
4312
|
if (values.markdown) {
|
|
4306
4313
|
const filePath = resolve(dumpDir, `${dumpName}.md`);
|
|
4307
4314
|
await mkdir(dumpDir, { recursive: true });
|
|
4308
|
-
await writeFile(filePath, await toMarkdown$
|
|
4315
|
+
await writeFile(filePath, await toMarkdown$2(signalId, plots, signalSchema), "utf-8");
|
|
4309
4316
|
console.log(`Saved: ${filePath}`);
|
|
4310
4317
|
process.exit(0);
|
|
4311
4318
|
return;
|
|
4312
4319
|
}
|
|
4313
|
-
console.log(await toMarkdown$
|
|
4320
|
+
console.log(await toMarkdown$2(signalId, plots, signalSchema));
|
|
4314
4321
|
process.exit(0);
|
|
4315
4322
|
};
|
|
4316
|
-
main$
|
|
4323
|
+
main$a();
|
|
4317
4324
|
|
|
4318
|
-
const main$
|
|
4325
|
+
const main$9 = async () => {
|
|
4319
4326
|
if (!getEntry(import.meta.url)) {
|
|
4320
4327
|
return;
|
|
4321
4328
|
}
|
|
@@ -4378,9 +4385,9 @@ const main$7 = async () => {
|
|
|
4378
4385
|
};
|
|
4379
4386
|
process.on("SIGINT", beforeExit);
|
|
4380
4387
|
};
|
|
4381
|
-
main$
|
|
4388
|
+
main$9();
|
|
4382
4389
|
|
|
4383
|
-
const main$
|
|
4390
|
+
const main$8 = async () => {
|
|
4384
4391
|
if (!getEntry(import.meta.url)) {
|
|
4385
4392
|
return;
|
|
4386
4393
|
}
|
|
@@ -4461,9 +4468,9 @@ const main$6 = async () => {
|
|
|
4461
4468
|
console.log(JSON.stringify(candles, null, 2));
|
|
4462
4469
|
process.exit(0);
|
|
4463
4470
|
};
|
|
4464
|
-
main$
|
|
4471
|
+
main$8();
|
|
4465
4472
|
|
|
4466
|
-
const main$
|
|
4473
|
+
const main$7 = async () => {
|
|
4467
4474
|
if (!getEntry(import.meta.url)) {
|
|
4468
4475
|
return;
|
|
4469
4476
|
}
|
|
@@ -4577,11 +4584,326 @@ const main$5 = async () => {
|
|
|
4577
4584
|
}
|
|
4578
4585
|
process.exit(0);
|
|
4579
4586
|
};
|
|
4580
|
-
main$
|
|
4587
|
+
main$7();
|
|
4581
4588
|
|
|
4582
|
-
const
|
|
4583
|
-
|
|
4584
|
-
|
|
4589
|
+
const getBroker = singleshot(() => {
|
|
4590
|
+
// Reaches into BrokerAdapter internals on purpose: the public commit*
|
|
4591
|
+
// methods skip when payload.backtest=true and require enable(), while
|
|
4592
|
+
// brokerdebug must fire exactly one commit at the raw adapter proxy.
|
|
4593
|
+
const broker = Broker["getInstance"]();
|
|
4594
|
+
if (!broker) {
|
|
4595
|
+
throw new Error("Broker instance is not initialized.");
|
|
4596
|
+
}
|
|
4597
|
+
return broker;
|
|
4598
|
+
});
|
|
4599
|
+
/** Called when a new signal is opened (position entry confirmed). */
|
|
4600
|
+
const commitOrderOpen = trycatch(async (payload) => {
|
|
4601
|
+
const broker = getBroker();
|
|
4602
|
+
return await broker.onOrderOpenCommit(payload);
|
|
4603
|
+
}, {
|
|
4604
|
+
fallback: (error) => {
|
|
4605
|
+
console.log(error);
|
|
4606
|
+
process.exit(-1);
|
|
4607
|
+
},
|
|
4608
|
+
});
|
|
4609
|
+
/** Called when a new signal is closed (take-profit, stop-loss, or manual close). */
|
|
4610
|
+
const commitOrderClose = trycatch(async (payload) => {
|
|
4611
|
+
const broker = getBroker();
|
|
4612
|
+
return await broker.onOrderCloseCommit(payload);
|
|
4613
|
+
}, {
|
|
4614
|
+
fallback: (error) => {
|
|
4615
|
+
console.log(error);
|
|
4616
|
+
process.exit(-1);
|
|
4617
|
+
},
|
|
4618
|
+
});
|
|
4619
|
+
/** Called when a partial profit close is committed. */
|
|
4620
|
+
const commitPartialProfit = trycatch(async (payload) => {
|
|
4621
|
+
const broker = getBroker();
|
|
4622
|
+
return await broker.onPartialProfitCommit(payload);
|
|
4623
|
+
}, {
|
|
4624
|
+
fallback: (error) => {
|
|
4625
|
+
console.log(error);
|
|
4626
|
+
process.exit(-1);
|
|
4627
|
+
},
|
|
4628
|
+
});
|
|
4629
|
+
/** Called when a partial loss close is committed. */
|
|
4630
|
+
const commitPartialLoss = trycatch(async (payload) => {
|
|
4631
|
+
const broker = getBroker();
|
|
4632
|
+
return await broker.onPartialLossCommit(payload);
|
|
4633
|
+
}, {
|
|
4634
|
+
fallback: (error) => {
|
|
4635
|
+
console.log(error);
|
|
4636
|
+
process.exit(-1);
|
|
4637
|
+
},
|
|
4638
|
+
});
|
|
4639
|
+
/** Called when a trailing stop update is committed. */
|
|
4640
|
+
const commitTrailingStop = trycatch(async (payload) => {
|
|
4641
|
+
const broker = getBroker();
|
|
4642
|
+
return await broker.onTrailingStopCommit(payload);
|
|
4643
|
+
}, {
|
|
4644
|
+
fallback: (error) => {
|
|
4645
|
+
console.log(error);
|
|
4646
|
+
process.exit(-1);
|
|
4647
|
+
},
|
|
4648
|
+
});
|
|
4649
|
+
/** Called when a trailing take-profit update is committed. */
|
|
4650
|
+
const commitTrailingTake = trycatch(async (payload) => {
|
|
4651
|
+
const broker = getBroker();
|
|
4652
|
+
return await broker.onTrailingTakeCommit(payload);
|
|
4653
|
+
}, {
|
|
4654
|
+
fallback: (error) => {
|
|
4655
|
+
console.log(error);
|
|
4656
|
+
process.exit(-1);
|
|
4657
|
+
},
|
|
4658
|
+
});
|
|
4659
|
+
/** Called when a breakeven stop is committed (stop loss moved to entry price). */
|
|
4660
|
+
const commitBreakeven = trycatch(async (payload) => {
|
|
4661
|
+
const broker = getBroker();
|
|
4662
|
+
return await broker.onBreakevenCommit(payload);
|
|
4663
|
+
}, {
|
|
4664
|
+
fallback: (error) => {
|
|
4665
|
+
console.log(error);
|
|
4666
|
+
process.exit(-1);
|
|
4667
|
+
},
|
|
4668
|
+
});
|
|
4669
|
+
/** Called when a DCA (average-buy) entry is committed. */
|
|
4670
|
+
const commitAverageBuy = trycatch(async (payload) => {
|
|
4671
|
+
const broker = getBroker();
|
|
4672
|
+
return await broker.onAverageBuyCommit(payload);
|
|
4673
|
+
}, {
|
|
4674
|
+
fallback: (error) => {
|
|
4675
|
+
console.log(error);
|
|
4676
|
+
process.exit(-1);
|
|
4677
|
+
},
|
|
4678
|
+
});
|
|
4679
|
+
const COMMITS = [
|
|
4680
|
+
"signal-open",
|
|
4681
|
+
"signal-close",
|
|
4682
|
+
"partial-profit",
|
|
4683
|
+
"partial-loss",
|
|
4684
|
+
"average-buy",
|
|
4685
|
+
"trailing-stop",
|
|
4686
|
+
"trailing-take",
|
|
4687
|
+
"breakeven",
|
|
4688
|
+
];
|
|
4689
|
+
const ZERO_PNL = {
|
|
4690
|
+
pnlPercentage: 0,
|
|
4691
|
+
priceOpen: 0,
|
|
4692
|
+
priceClose: 0,
|
|
4693
|
+
pnlCost: 0,
|
|
4694
|
+
pnlEntries: 0,
|
|
4695
|
+
};
|
|
4696
|
+
const main$6 = async () => {
|
|
4697
|
+
if (!getEntry(import.meta.url)) {
|
|
4698
|
+
return;
|
|
4699
|
+
}
|
|
4700
|
+
const { values } = getArgs();
|
|
4701
|
+
if (!values.brokerdebug) {
|
|
4702
|
+
return;
|
|
4703
|
+
}
|
|
4704
|
+
{
|
|
4705
|
+
const cwd = process.cwd();
|
|
4706
|
+
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
4707
|
+
}
|
|
4708
|
+
await cli.configConnectionService.loadConfig("setup.config");
|
|
4709
|
+
{
|
|
4710
|
+
const loader = await cli.configConnectionService.loadConfig("loader.config");
|
|
4711
|
+
try {
|
|
4712
|
+
if (typeof loader === "function") {
|
|
4713
|
+
await loader();
|
|
4714
|
+
}
|
|
4715
|
+
if (typeof loader?.loader === "function") {
|
|
4716
|
+
await loader.loader();
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
catch (error) {
|
|
4720
|
+
console.error("Module loader failed", error);
|
|
4721
|
+
process.exit(-1);
|
|
4722
|
+
}
|
|
4723
|
+
}
|
|
4724
|
+
await cli.moduleConnectionService.loadModule("brokerdebug.module");
|
|
4725
|
+
const [defaultExchangeName = null] = await listExchangeSchema();
|
|
4726
|
+
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
4727
|
+
const symbol = values.symbol || "BTCUSDT";
|
|
4728
|
+
const commit = values.commit || "signal-open";
|
|
4729
|
+
if (!COMMITS.includes(commit)) {
|
|
4730
|
+
console.error(`Unknown --commit value: ${commit}`);
|
|
4731
|
+
console.error(`Available: ${COMMITS.join(", ")}`);
|
|
4732
|
+
process.exit(1);
|
|
4733
|
+
}
|
|
4734
|
+
const candles = await Exchange.getCandles(symbol, "1m", 5, { exchangeName });
|
|
4735
|
+
const lastCandle = candles[candles.length - 1];
|
|
4736
|
+
const currentPrice = lastCandle.close;
|
|
4737
|
+
const priceTakeProfit = currentPrice * 1.02;
|
|
4738
|
+
const priceStopLoss = currentPrice * 0.98;
|
|
4739
|
+
const context = {
|
|
4740
|
+
strategyName: "broker-test",
|
|
4741
|
+
exchangeName: exchangeName,
|
|
4742
|
+
};
|
|
4743
|
+
const signalId = `broker-debug-${Date.now()}`;
|
|
4744
|
+
const when = alignToInterval(new Date(), "1m");
|
|
4745
|
+
{
|
|
4746
|
+
console.log(`symbol=${symbol} price=${currentPrice} commit=${commit}`);
|
|
4747
|
+
console.time(commit);
|
|
4748
|
+
}
|
|
4749
|
+
const run = async () => {
|
|
4750
|
+
if (commit === "signal-open") {
|
|
4751
|
+
await commitOrderOpen({
|
|
4752
|
+
type: "active",
|
|
4753
|
+
symbol,
|
|
4754
|
+
signalId,
|
|
4755
|
+
cost: 100,
|
|
4756
|
+
position: "long",
|
|
4757
|
+
priceOpen: currentPrice,
|
|
4758
|
+
priceTakeProfit,
|
|
4759
|
+
priceStopLoss,
|
|
4760
|
+
pnl: ZERO_PNL,
|
|
4761
|
+
peakProfit: ZERO_PNL,
|
|
4762
|
+
maxDrawdown: ZERO_PNL,
|
|
4763
|
+
attempt: 0,
|
|
4764
|
+
context,
|
|
4765
|
+
when,
|
|
4766
|
+
backtest: true,
|
|
4767
|
+
});
|
|
4768
|
+
}
|
|
4769
|
+
if (commit === "signal-close") {
|
|
4770
|
+
await commitOrderClose({
|
|
4771
|
+
symbol,
|
|
4772
|
+
signalId,
|
|
4773
|
+
cost: 100,
|
|
4774
|
+
position: "long",
|
|
4775
|
+
currentPrice,
|
|
4776
|
+
priceOpen: currentPrice,
|
|
4777
|
+
priceTakeProfit,
|
|
4778
|
+
priceStopLoss,
|
|
4779
|
+
totalEntries: 1,
|
|
4780
|
+
totalPartials: 0,
|
|
4781
|
+
attempt: 0,
|
|
4782
|
+
pnl: ZERO_PNL,
|
|
4783
|
+
peakProfit: ZERO_PNL,
|
|
4784
|
+
maxDrawdown: ZERO_PNL,
|
|
4785
|
+
context,
|
|
4786
|
+
when,
|
|
4787
|
+
backtest: true,
|
|
4788
|
+
});
|
|
4789
|
+
}
|
|
4790
|
+
if (commit === "partial-profit") {
|
|
4791
|
+
await commitPartialProfit({
|
|
4792
|
+
symbol,
|
|
4793
|
+
signalId,
|
|
4794
|
+
percentToClose: 50,
|
|
4795
|
+
cost: 50,
|
|
4796
|
+
currentPrice,
|
|
4797
|
+
position: "long",
|
|
4798
|
+
priceTakeProfit,
|
|
4799
|
+
priceStopLoss,
|
|
4800
|
+
context,
|
|
4801
|
+
when,
|
|
4802
|
+
backtest: true,
|
|
4803
|
+
});
|
|
4804
|
+
}
|
|
4805
|
+
if (commit === "partial-loss") {
|
|
4806
|
+
await commitPartialLoss({
|
|
4807
|
+
symbol,
|
|
4808
|
+
signalId,
|
|
4809
|
+
percentToClose: 50,
|
|
4810
|
+
cost: 50,
|
|
4811
|
+
currentPrice,
|
|
4812
|
+
position: "long",
|
|
4813
|
+
priceTakeProfit,
|
|
4814
|
+
priceStopLoss,
|
|
4815
|
+
context,
|
|
4816
|
+
when,
|
|
4817
|
+
backtest: true,
|
|
4818
|
+
});
|
|
4819
|
+
}
|
|
4820
|
+
if (commit === "average-buy") {
|
|
4821
|
+
await commitAverageBuy({
|
|
4822
|
+
symbol,
|
|
4823
|
+
signalId,
|
|
4824
|
+
currentPrice,
|
|
4825
|
+
cost: 100,
|
|
4826
|
+
position: "long",
|
|
4827
|
+
priceTakeProfit,
|
|
4828
|
+
priceStopLoss,
|
|
4829
|
+
context,
|
|
4830
|
+
when,
|
|
4831
|
+
backtest: true,
|
|
4832
|
+
});
|
|
4833
|
+
}
|
|
4834
|
+
if (commit === "trailing-stop") {
|
|
4835
|
+
await commitTrailingStop({
|
|
4836
|
+
symbol,
|
|
4837
|
+
signalId,
|
|
4838
|
+
percentShift: -50,
|
|
4839
|
+
currentPrice,
|
|
4840
|
+
newStopLossPrice: priceStopLoss + (currentPrice - priceStopLoss) * 0.5,
|
|
4841
|
+
takeProfitPrice: priceTakeProfit,
|
|
4842
|
+
position: "long",
|
|
4843
|
+
context,
|
|
4844
|
+
when,
|
|
4845
|
+
backtest: true,
|
|
4846
|
+
});
|
|
4847
|
+
}
|
|
4848
|
+
if (commit === "trailing-take") {
|
|
4849
|
+
await commitTrailingTake({
|
|
4850
|
+
symbol,
|
|
4851
|
+
signalId,
|
|
4852
|
+
percentShift: 50,
|
|
4853
|
+
currentPrice,
|
|
4854
|
+
newTakeProfitPrice: priceTakeProfit + (priceTakeProfit - currentPrice) * 0.5,
|
|
4855
|
+
takeProfitPrice: priceTakeProfit,
|
|
4856
|
+
position: "long",
|
|
4857
|
+
context,
|
|
4858
|
+
when,
|
|
4859
|
+
backtest: true,
|
|
4860
|
+
});
|
|
4861
|
+
}
|
|
4862
|
+
if (commit === "breakeven") {
|
|
4863
|
+
await commitBreakeven({
|
|
4864
|
+
symbol,
|
|
4865
|
+
signalId,
|
|
4866
|
+
currentPrice,
|
|
4867
|
+
newStopLossPrice: currentPrice,
|
|
4868
|
+
newTakeProfitPrice: priceTakeProfit,
|
|
4869
|
+
position: "long",
|
|
4870
|
+
context,
|
|
4871
|
+
when,
|
|
4872
|
+
backtest: true,
|
|
4873
|
+
});
|
|
4874
|
+
}
|
|
4875
|
+
};
|
|
4876
|
+
await runInMockContext(run, {
|
|
4877
|
+
when,
|
|
4878
|
+
strategyName: context.strategyName,
|
|
4879
|
+
exchangeName: context.exchangeName,
|
|
4880
|
+
frameName: "",
|
|
4881
|
+
symbol,
|
|
4882
|
+
backtest: true,
|
|
4883
|
+
});
|
|
4884
|
+
{
|
|
4885
|
+
console.log(`${commit} OK`);
|
|
4886
|
+
console.timeEnd(commit);
|
|
4887
|
+
}
|
|
4888
|
+
process.exit(0);
|
|
4889
|
+
};
|
|
4890
|
+
main$6();
|
|
4891
|
+
|
|
4892
|
+
const SIMULATOR_NAME$1 = "cli_simulator";
|
|
4893
|
+
const CONFIG_KEYS$1 = ["gridAxes", "reportOrder"];
|
|
4894
|
+
const validateConfig$1 = (config) => {
|
|
4895
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
4896
|
+
return "config must be a JSON object";
|
|
4897
|
+
}
|
|
4898
|
+
for (const key of Object.keys(config)) {
|
|
4899
|
+
if (!CONFIG_KEYS$1.includes(key)) {
|
|
4900
|
+
return `unknown key "${key}" (expected: ${CONFIG_KEYS$1.join(", ")})`;
|
|
4901
|
+
}
|
|
4902
|
+
}
|
|
4903
|
+
return null;
|
|
4904
|
+
};
|
|
4905
|
+
const IDEA_DIRECTIONS$1 = ["LONG", "SHORT", "NEUTRAL"];
|
|
4906
|
+
const validateIdea$1 = (idea, line) => {
|
|
4585
4907
|
if (typeof idea !== "object" || idea === null) {
|
|
4586
4908
|
return `line ${line}: not an object`;
|
|
4587
4909
|
}
|
|
@@ -4594,15 +4916,18 @@ const validateIdea = (idea, line) => {
|
|
|
4594
4916
|
if (typeof idea.symbol !== "string" || !idea.symbol) {
|
|
4595
4917
|
return `line ${line}: "symbol" must be a non-empty string`;
|
|
4596
4918
|
}
|
|
4597
|
-
if (!IDEA_DIRECTIONS.includes(idea.direction)) {
|
|
4598
|
-
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
4919
|
+
if (!IDEA_DIRECTIONS$1.includes(idea.direction)) {
|
|
4920
|
+
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS$1.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
4599
4921
|
}
|
|
4600
4922
|
if (typeof idea.author !== "string" || !idea.author) {
|
|
4601
4923
|
return `line ${line}: "author" must be a non-empty string`;
|
|
4602
4924
|
}
|
|
4603
4925
|
return null;
|
|
4604
4926
|
};
|
|
4605
|
-
const toMarkdown = (result) => {
|
|
4927
|
+
const toMarkdown$1 = (result) => {
|
|
4928
|
+
const buckets = Object.entries(result.reports).filter(([, bucket]) => bucket.reports.length > 0);
|
|
4929
|
+
const allReports = buckets.flatMap(([, bucket]) => bucket.reports);
|
|
4930
|
+
const profitable = allReports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
|
|
4606
4931
|
const lines = [];
|
|
4607
4932
|
lines.push(`# Simulator Report — ${result.symbol}`);
|
|
4608
4933
|
lines.push("");
|
|
@@ -4610,34 +4935,38 @@ const toMarkdown = (result) => {
|
|
|
4610
4935
|
lines.push(`| --- | --- |`);
|
|
4611
4936
|
lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
|
|
4612
4937
|
lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
|
|
4613
|
-
lines.push(`|
|
|
4614
|
-
lines.push(`| Grid points | ${result.reports.length} |`);
|
|
4938
|
+
lines.push(`| Profitable corridor | ${profitable} / ${allReports.length} grid points |`);
|
|
4615
4939
|
lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4940
|
+
// каждая метрика — самодостаточная корзина: свои победители и
|
|
4941
|
+
// свои словари банов, между собой не склеиваются
|
|
4942
|
+
for (const [metric, bucket] of buckets) {
|
|
4943
|
+
lines.push("");
|
|
4944
|
+
lines.push(`## Metric: ${metric}`);
|
|
4945
|
+
lines.push("");
|
|
4946
|
+
lines.push(`| Criterion | Stop% | Hold | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
|
|
4947
|
+
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
|
|
4948
|
+
for (const best of bucket.best) {
|
|
4949
|
+
if (!best.report) {
|
|
4950
|
+
lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
|
|
4951
|
+
continue;
|
|
4952
|
+
}
|
|
4953
|
+
const { point } = best.report;
|
|
4954
|
+
lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.holdMinutes / 60}h | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
|
|
4955
|
+
`${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
|
|
4956
|
+
}
|
|
4957
|
+
const sharpeBest = bucket.best.find(({ criterion }) => criterion === "sharpe");
|
|
4958
|
+
lines.push("");
|
|
4959
|
+
lines.push(`### Allowed authors (sharpe winner rule)`);
|
|
4960
|
+
lines.push("");
|
|
4961
|
+
lines.push(`| Author | Ideas | Hits | HitRate |`);
|
|
4962
|
+
lines.push(`| --- | --- | --- | --- |`);
|
|
4963
|
+
for (const stat of (sharpeBest?.authorStats ?? []).filter(({ banned }) => !banned)) {
|
|
4964
|
+
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
|
|
4625
4965
|
}
|
|
4626
|
-
const { point } = best.report;
|
|
4627
|
-
lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.trailingTakePercent} | ${point.holdMinutes / 60}h | ${point.minIdeasAligned} | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
|
|
4628
|
-
`${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
|
|
4629
|
-
}
|
|
4630
|
-
lines.push("");
|
|
4631
|
-
lines.push(`## Allowed authors (production whitelist)`);
|
|
4632
|
-
lines.push("");
|
|
4633
|
-
lines.push(`| Author | Ideas | Hits | HitRate |`);
|
|
4634
|
-
lines.push(`| --- | --- | --- | --- |`);
|
|
4635
|
-
for (const stat of result.authorStats.filter(({ banned }) => !banned)) {
|
|
4636
|
-
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
|
|
4637
4966
|
}
|
|
4638
4967
|
return lines.join("\n");
|
|
4639
4968
|
};
|
|
4640
|
-
const main$
|
|
4969
|
+
const main$5 = async () => {
|
|
4641
4970
|
if (!getEntry(import.meta.url)) {
|
|
4642
4971
|
return;
|
|
4643
4972
|
}
|
|
@@ -4650,6 +4979,33 @@ const main$4 = async () => {
|
|
|
4650
4979
|
console.error("Error: positional path to an ideas .jsonl file is required");
|
|
4651
4980
|
process.exit(1);
|
|
4652
4981
|
}
|
|
4982
|
+
// конфиг — позиционный JSON; без него подтягивается пустой объект
|
|
4983
|
+
// и схема живёт на дефолтах движка (оси, порядок отчёта)
|
|
4984
|
+
const [configPath = null] = positionals.filter((value) => value.endsWith(".json") && !value.endsWith(".jsonl"));
|
|
4985
|
+
let config = {};
|
|
4986
|
+
if (configPath) {
|
|
4987
|
+
let content;
|
|
4988
|
+
try {
|
|
4989
|
+
content = await readFile(resolve(configPath), "utf-8");
|
|
4990
|
+
}
|
|
4991
|
+
catch (error) {
|
|
4992
|
+
console.error(`Error: cannot read config file: ${configPath}`);
|
|
4993
|
+
process.exit(1);
|
|
4994
|
+
}
|
|
4995
|
+
try {
|
|
4996
|
+
config = JSON.parse(content);
|
|
4997
|
+
}
|
|
4998
|
+
catch {
|
|
4999
|
+
console.error(`Error: invalid JSON in config file: ${configPath}`);
|
|
5000
|
+
process.exit(1);
|
|
5001
|
+
}
|
|
5002
|
+
const problem = validateConfig$1(config);
|
|
5003
|
+
if (problem) {
|
|
5004
|
+
console.error(`Error: simulator config does not match the structure — ${problem}`);
|
|
5005
|
+
console.error(`Expected shape: { "gridAxes"?: ISimulatorGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }`);
|
|
5006
|
+
process.exit(1);
|
|
5007
|
+
}
|
|
5008
|
+
}
|
|
4653
5009
|
let ideas = [];
|
|
4654
5010
|
{
|
|
4655
5011
|
let content;
|
|
@@ -4674,7 +5030,7 @@ const main$4 = async () => {
|
|
|
4674
5030
|
console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
|
|
4675
5031
|
process.exit(1);
|
|
4676
5032
|
}
|
|
4677
|
-
const problem = validateIdea(idea, i + 1);
|
|
5033
|
+
const problem = validateIdea$1(idea, i + 1);
|
|
4678
5034
|
if (problem) {
|
|
4679
5035
|
console.error(`Error: ideas file does not match the idea structure — ${problem}`);
|
|
4680
5036
|
console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
|
|
@@ -4710,20 +5066,24 @@ const main$4 = async () => {
|
|
|
4710
5066
|
}
|
|
4711
5067
|
const [defaultExchangeName = null] = await listExchangeSchema();
|
|
4712
5068
|
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
5069
|
+
if (values.verbose) {
|
|
5070
|
+
overrideExchangeSchema({
|
|
5071
|
+
exchangeName,
|
|
5072
|
+
callbacks: {
|
|
5073
|
+
onCandleData(symbol, interval, since) {
|
|
5074
|
+
console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
|
|
5075
|
+
},
|
|
5076
|
+
},
|
|
5077
|
+
});
|
|
5078
|
+
}
|
|
4713
5079
|
const symbol = values.symbol || "BTCUSDT";
|
|
5080
|
+
// оси и порядок отчёта приходят позиционным конфигом потребителя;
|
|
5081
|
+
// пустой конфиг — дефолтная сетка движка из connection-сервиса
|
|
4714
5082
|
addSimulatorSchema({
|
|
4715
|
-
simulatorName: SIMULATOR_NAME,
|
|
5083
|
+
simulatorName: SIMULATOR_NAME$1,
|
|
4716
5084
|
exchangeName,
|
|
4717
|
-
gridAxes: {
|
|
4718
|
-
|
|
4719
|
-
trailingTakePercent: [0.5, 1, 1.5, 2, 3, 4],
|
|
4720
|
-
holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
|
|
4721
|
-
minIdeasAligned: [1, 2, 3],
|
|
4722
|
-
minAuthorTrack: [2, 3, 5],
|
|
4723
|
-
minAuthorHitRate: [0.5, 0.6],
|
|
4724
|
-
minWeightAligned: [0, 0.6, 1.2],
|
|
4725
|
-
profitLockPercent: [0, 1.5, 2.5],
|
|
4726
|
-
},
|
|
5085
|
+
...(config.gridAxes ? { gridAxes: config.gridAxes } : {}),
|
|
5086
|
+
...(config.reportOrder ? { reportOrder: config.reportOrder } : {}),
|
|
4727
5087
|
callbacks: {
|
|
4728
5088
|
onProgress: (symbol, stage, processed, total) => {
|
|
4729
5089
|
if (values.verbose) {
|
|
@@ -4775,9 +5135,8 @@ const main$4 = async () => {
|
|
|
4775
5135
|
if (values.verbose) {
|
|
4776
5136
|
console.log("onDone", {
|
|
4777
5137
|
symbol,
|
|
4778
|
-
reports: result.reports.length,
|
|
4779
|
-
|
|
4780
|
-
bannedAuthors: result.bannedAuthors.length,
|
|
5138
|
+
reports: Object.values(result.reports).flatMap((bucket) => bucket.reports).length,
|
|
5139
|
+
bans: Object.values(result.reports).flatMap((bucket) => bucket.bans).length,
|
|
4781
5140
|
});
|
|
4782
5141
|
}
|
|
4783
5142
|
},
|
|
@@ -4785,7 +5144,7 @@ const main$4 = async () => {
|
|
|
4785
5144
|
});
|
|
4786
5145
|
const result = await Simulator.run({
|
|
4787
5146
|
symbol,
|
|
4788
|
-
simulatorName: SIMULATOR_NAME,
|
|
5147
|
+
simulatorName: SIMULATOR_NAME$1,
|
|
4789
5148
|
ideas,
|
|
4790
5149
|
});
|
|
4791
5150
|
const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
|
|
@@ -4800,11 +5159,308 @@ const main$4 = async () => {
|
|
|
4800
5159
|
if (values.markdown) {
|
|
4801
5160
|
const filePath = resolve(dumpDir, `${dumpName}.md`);
|
|
4802
5161
|
await mkdir(dumpDir, { recursive: true });
|
|
4803
|
-
await writeFile(filePath, toMarkdown(result), "utf-8");
|
|
5162
|
+
await writeFile(filePath, toMarkdown$1(result), "utf-8");
|
|
4804
5163
|
console.log(`Saved: ${filePath}`);
|
|
4805
5164
|
process.exit(0);
|
|
4806
5165
|
}
|
|
4807
|
-
console.log(toMarkdown(result));
|
|
5166
|
+
console.log(toMarkdown$1(result));
|
|
5167
|
+
process.exit(0);
|
|
5168
|
+
};
|
|
5169
|
+
main$5();
|
|
5170
|
+
|
|
5171
|
+
const SIMULATOR_NAME = "cli_tune";
|
|
5172
|
+
const CONFIG_KEYS = ["point", "authorStats", "gridAxes", "reportOrder"];
|
|
5173
|
+
const POINT_NUMBER_FIELDS = [
|
|
5174
|
+
"hardStopPercent",
|
|
5175
|
+
"trailingTakePercent",
|
|
5176
|
+
"holdMinutes",
|
|
5177
|
+
"minAuthorTrack",
|
|
5178
|
+
"minAuthorHitRate",
|
|
5179
|
+
"profitLockPercent",
|
|
5180
|
+
];
|
|
5181
|
+
const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
|
|
5182
|
+
const validateIdea = (idea, line) => {
|
|
5183
|
+
if (typeof idea !== "object" || idea === null) {
|
|
5184
|
+
return `line ${line}: not an object`;
|
|
5185
|
+
}
|
|
5186
|
+
if (typeof idea.id !== "number") {
|
|
5187
|
+
return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
|
|
5188
|
+
}
|
|
5189
|
+
if (typeof idea.ts !== "number") {
|
|
5190
|
+
return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
|
|
5191
|
+
}
|
|
5192
|
+
if (typeof idea.symbol !== "string" || !idea.symbol) {
|
|
5193
|
+
return `line ${line}: "symbol" must be a non-empty string`;
|
|
5194
|
+
}
|
|
5195
|
+
if (!IDEA_DIRECTIONS.includes(idea.direction)) {
|
|
5196
|
+
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
5197
|
+
}
|
|
5198
|
+
if (typeof idea.author !== "string" || !idea.author) {
|
|
5199
|
+
return `line ${line}: "author" must be a non-empty string`;
|
|
5200
|
+
}
|
|
5201
|
+
return null;
|
|
5202
|
+
};
|
|
5203
|
+
const validateConfig = (config) => {
|
|
5204
|
+
if (typeof config !== "object" || config === null || Array.isArray(config)) {
|
|
5205
|
+
return "config must be a JSON object";
|
|
5206
|
+
}
|
|
5207
|
+
for (const key of Object.keys(config)) {
|
|
5208
|
+
if (!CONFIG_KEYS.includes(key)) {
|
|
5209
|
+
return `unknown key "${key}" (expected: ${CONFIG_KEYS.join(", ")})`;
|
|
5210
|
+
}
|
|
5211
|
+
}
|
|
5212
|
+
if (typeof config.point !== "object" || config.point === null) {
|
|
5213
|
+
return `"point" is required — the frozen grid point of a training run`;
|
|
5214
|
+
}
|
|
5215
|
+
for (const field of POINT_NUMBER_FIELDS) {
|
|
5216
|
+
if (typeof config.point[field] !== "number") {
|
|
5217
|
+
return `"point.${field}" must be a number, got ${typeof config.point[field]}`;
|
|
5218
|
+
}
|
|
5219
|
+
}
|
|
5220
|
+
if (typeof config.point.authorMetric !== "string") {
|
|
5221
|
+
return `"point.authorMetric" must be a string, got ${typeof config.point.authorMetric}`;
|
|
5222
|
+
}
|
|
5223
|
+
if (!Array.isArray(config.authorStats) || !config.authorStats.length) {
|
|
5224
|
+
return `"authorStats" is required — the frozen raw track record (non-empty array)`;
|
|
5225
|
+
}
|
|
5226
|
+
for (let i = 0; i < config.authorStats.length; i++) {
|
|
5227
|
+
const stat = config.authorStats[i];
|
|
5228
|
+
if (typeof stat !== "object" ||
|
|
5229
|
+
stat === null ||
|
|
5230
|
+
typeof stat.author !== "string" ||
|
|
5231
|
+
typeof stat.ideas !== "number" ||
|
|
5232
|
+
typeof stat.hits !== "number") {
|
|
5233
|
+
return `"authorStats[${i}]" must be { author: string, ideas: number, hits: number }`;
|
|
5234
|
+
}
|
|
5235
|
+
}
|
|
5236
|
+
return null;
|
|
5237
|
+
};
|
|
5238
|
+
/** Одноточечные оси, зеркалящие замороженную точку — сетка инертна. */
|
|
5239
|
+
const axesFromPoint = (point) => ({
|
|
5240
|
+
hardStopPercent: [point.hardStopPercent],
|
|
5241
|
+
trailingTakePercent: [point.trailingTakePercent],
|
|
5242
|
+
holdMinutes: [point.holdMinutes],
|
|
5243
|
+
minAuthorTrack: [point.minAuthorTrack],
|
|
5244
|
+
minAuthorHitRate: [point.minAuthorHitRate],
|
|
5245
|
+
profitLockPercent: [point.profitLockPercent],
|
|
5246
|
+
authorMetric: [point.authorMetric],
|
|
5247
|
+
});
|
|
5248
|
+
const pointLabel = (point) => `H=${point.hardStopPercent} TT=${point.trailingTakePercent} hold=${point.holdMinutes / 60}h ` +
|
|
5249
|
+
`track=${point.minAuthorTrack} rate=${point.minAuthorHitRate} ` +
|
|
5250
|
+
`lock=${point.profitLockPercent} metric=${point.authorMetric}`;
|
|
5251
|
+
const fmtRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
|
|
5252
|
+
const toMarkdown = (test) => {
|
|
5253
|
+
const lines = [];
|
|
5254
|
+
lines.push(`# Tune Report — ${test.symbol}`);
|
|
5255
|
+
lines.push("");
|
|
5256
|
+
lines.push(`ONE out-of-sample shot of a FROZEN training artifact: nothing is trained here — the point and the raw author track record come from the input config verbatim, bans are re-derived from the frozen numbers under the point's rule, unseen authors are banned by default.`);
|
|
5257
|
+
lines.push("");
|
|
5258
|
+
lines.push(`Frozen point: ${pointLabel(test.point)}`);
|
|
5259
|
+
lines.push("");
|
|
5260
|
+
lines.push(`## Out-of-sample (${test.ideasDirectional} directional ideas)`);
|
|
5261
|
+
lines.push("");
|
|
5262
|
+
lines.push(`| Metric | Value |`);
|
|
5263
|
+
lines.push(`| --- | --- |`);
|
|
5264
|
+
lines.push(`| Trades (skipped busy) | ${test.report.trades} (${test.report.skippedBusy}) |`);
|
|
5265
|
+
lines.push(`| PNL | ${test.report.totalPnlPercent.toFixed(2)}% |`);
|
|
5266
|
+
lines.push(`| Win rate | ${(test.report.winRate * 100).toFixed(0)}% |`);
|
|
5267
|
+
lines.push(`| Profit factor | ${fmtRatio(test.report.profitFactor)} |`);
|
|
5268
|
+
lines.push(`| Max series drawdown | ${test.report.maxSeriesDrawdownPercent.toFixed(2)}% |`);
|
|
5269
|
+
lines.push(`| Sharpe / Sortino | ${fmtRatio(test.report.sharpe)} / ${fmtRatio(test.report.sortino)} |`);
|
|
5270
|
+
lines.push(`| Recovery factor | ${fmtRatio(test.report.recoveryFactor)} |`);
|
|
5271
|
+
lines.push(`| Exits | ${Object.entries(test.report.exitReasons).map(([reason, count]) => `${reason}=${count}`).join(", ")} |`);
|
|
5272
|
+
lines.push("");
|
|
5273
|
+
lines.push(`## Test trades`);
|
|
5274
|
+
lines.push("");
|
|
5275
|
+
lines.push(`| Direction | Exit | PNL% | Hold | Entry (UTC) |`);
|
|
5276
|
+
lines.push(`| --- | --- | --- | --- | --- |`);
|
|
5277
|
+
for (const trade of test.trades) {
|
|
5278
|
+
lines.push(`| ${trade.direction} | ${trade.exitReason} | ${trade.pnlPercent.toFixed(2)} | ${trade.holdMinutesActual}m | ${new Date(trade.entryTimestamp).toISOString()} |`);
|
|
5279
|
+
}
|
|
5280
|
+
lines.push("");
|
|
5281
|
+
lines.push(`## Frozen author track record (re-derived bans under the point's rule)`);
|
|
5282
|
+
lines.push("");
|
|
5283
|
+
lines.push(`| Author | Ideas | Hits | Banned |`);
|
|
5284
|
+
lines.push(`| --- | --- | --- | --- |`);
|
|
5285
|
+
for (const stat of test.authorStats) {
|
|
5286
|
+
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${stat.banned ? "yes" : ""} |`);
|
|
5287
|
+
}
|
|
5288
|
+
return lines.join("\n");
|
|
5289
|
+
};
|
|
5290
|
+
const main$4 = async () => {
|
|
5291
|
+
if (!getEntry(import.meta.url)) {
|
|
5292
|
+
return;
|
|
5293
|
+
}
|
|
5294
|
+
const { values, positionals } = getArgs();
|
|
5295
|
+
if (!values.tune) {
|
|
5296
|
+
return;
|
|
5297
|
+
}
|
|
5298
|
+
const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
|
|
5299
|
+
if (!ideasPath) {
|
|
5300
|
+
console.error("Error: positional path to an ideas .jsonl file is required");
|
|
5301
|
+
process.exit(1);
|
|
5302
|
+
}
|
|
5303
|
+
// конфиг — позиционный JSON; без него подтягивается пустой объект
|
|
5304
|
+
// (и валидация честно скажет, что замороженной точки нет)
|
|
5305
|
+
const [configPath = null] = positionals.filter((value) => value.endsWith(".json") && !value.endsWith(".jsonl"));
|
|
5306
|
+
let config = {};
|
|
5307
|
+
if (configPath) {
|
|
5308
|
+
let content;
|
|
5309
|
+
try {
|
|
5310
|
+
content = await readFile(resolve(configPath), "utf-8");
|
|
5311
|
+
}
|
|
5312
|
+
catch (error) {
|
|
5313
|
+
console.error(`Error: cannot read config file: ${configPath}`);
|
|
5314
|
+
process.exit(1);
|
|
5315
|
+
}
|
|
5316
|
+
try {
|
|
5317
|
+
config = JSON.parse(content);
|
|
5318
|
+
}
|
|
5319
|
+
catch {
|
|
5320
|
+
console.error(`Error: invalid JSON in config file: ${configPath}`);
|
|
5321
|
+
process.exit(1);
|
|
5322
|
+
}
|
|
5323
|
+
}
|
|
5324
|
+
{
|
|
5325
|
+
const problem = validateConfig(config);
|
|
5326
|
+
if (problem) {
|
|
5327
|
+
console.error(`Error: tune config does not match the structure — ${problem}`);
|
|
5328
|
+
console.error(`Expected shape: { "point": ISimulatorGridPoint, "authorStats": [{ "author", "ideas", "hits" }], "gridAxes"?, "reportOrder"? }`);
|
|
5329
|
+
process.exit(1);
|
|
5330
|
+
}
|
|
5331
|
+
}
|
|
5332
|
+
let ideas = [];
|
|
5333
|
+
{
|
|
5334
|
+
let content;
|
|
5335
|
+
try {
|
|
5336
|
+
content = await readFile(resolve(ideasPath), "utf-8");
|
|
5337
|
+
}
|
|
5338
|
+
catch (error) {
|
|
5339
|
+
console.error(`Error: cannot read ideas file: ${ideasPath}`);
|
|
5340
|
+
process.exit(1);
|
|
5341
|
+
}
|
|
5342
|
+
const rows = content.split("\n").filter(Boolean);
|
|
5343
|
+
if (!rows.length) {
|
|
5344
|
+
console.error(`Error: ideas file is empty: ${ideasPath}`);
|
|
5345
|
+
process.exit(1);
|
|
5346
|
+
}
|
|
5347
|
+
for (let i = 0; i < rows.length; i++) {
|
|
5348
|
+
let idea;
|
|
5349
|
+
try {
|
|
5350
|
+
idea = JSON.parse(rows[i]);
|
|
5351
|
+
}
|
|
5352
|
+
catch {
|
|
5353
|
+
console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
|
|
5354
|
+
process.exit(1);
|
|
5355
|
+
}
|
|
5356
|
+
const problem = validateIdea(idea, i + 1);
|
|
5357
|
+
if (problem) {
|
|
5358
|
+
console.error(`Error: ideas file does not match the idea structure — ${problem}`);
|
|
5359
|
+
console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
|
|
5360
|
+
process.exit(1);
|
|
5361
|
+
}
|
|
5362
|
+
ideas.push(idea);
|
|
5363
|
+
}
|
|
5364
|
+
}
|
|
5365
|
+
{
|
|
5366
|
+
const cwd = process.cwd();
|
|
5367
|
+
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
5368
|
+
}
|
|
5369
|
+
await cli.configConnectionService.loadConfig("setup.config");
|
|
5370
|
+
{
|
|
5371
|
+
const loader = await cli.configConnectionService.loadConfig("loader.config");
|
|
5372
|
+
try {
|
|
5373
|
+
if (typeof loader === "function") {
|
|
5374
|
+
await loader();
|
|
5375
|
+
}
|
|
5376
|
+
if (typeof loader?.loader === "function") {
|
|
5377
|
+
await loader.loader();
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5380
|
+
catch (error) {
|
|
5381
|
+
console.error("Module loader failed", error);
|
|
5382
|
+
process.exit(-1);
|
|
5383
|
+
}
|
|
5384
|
+
}
|
|
5385
|
+
await cli.moduleConnectionService.loadModule("tune.module");
|
|
5386
|
+
{
|
|
5387
|
+
await cli.exchangeSchemaService.addSchema();
|
|
5388
|
+
await cli.symbolSchemaService.addSchema();
|
|
5389
|
+
}
|
|
5390
|
+
const [defaultExchangeName = null] = await listExchangeSchema();
|
|
5391
|
+
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
5392
|
+
if (values.verbose) {
|
|
5393
|
+
overrideExchangeSchema({
|
|
5394
|
+
exchangeName,
|
|
5395
|
+
callbacks: {
|
|
5396
|
+
onCandleData(symbol, interval, since) {
|
|
5397
|
+
console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
|
|
5398
|
+
},
|
|
5399
|
+
},
|
|
5400
|
+
});
|
|
5401
|
+
}
|
|
5402
|
+
const symbol = values.symbol || "BTCUSDT";
|
|
5403
|
+
// сетка для test() инертна (оценивается ровно замороженная точка):
|
|
5404
|
+
// без явных осей в конфиге они одноточечно зеркалят точку
|
|
5405
|
+
addSimulatorSchema({
|
|
5406
|
+
simulatorName: SIMULATOR_NAME,
|
|
5407
|
+
exchangeName,
|
|
5408
|
+
gridAxes: config.gridAxes ?? axesFromPoint(config.point),
|
|
5409
|
+
...(config.reportOrder ? { reportOrder: config.reportOrder } : {}),
|
|
5410
|
+
callbacks: {
|
|
5411
|
+
onProgress: (symbol, stage, processed, total) => {
|
|
5412
|
+
if (values.verbose) {
|
|
5413
|
+
console.log("onProgress", { symbol, stage, processed, total });
|
|
5414
|
+
}
|
|
5415
|
+
},
|
|
5416
|
+
onIdeas: (symbol, ideasTotal, ideasDirectional) => {
|
|
5417
|
+
if (values.verbose) {
|
|
5418
|
+
console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
|
|
5419
|
+
}
|
|
5420
|
+
},
|
|
5421
|
+
onProfiles: (symbol, profiles, truncatedCount) => {
|
|
5422
|
+
if (values.verbose) {
|
|
5423
|
+
console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
|
|
5424
|
+
}
|
|
5425
|
+
},
|
|
5426
|
+
onTestDone: (symbol, result) => {
|
|
5427
|
+
if (values.verbose) {
|
|
5428
|
+
console.log("onTestDone", {
|
|
5429
|
+
symbol,
|
|
5430
|
+
trades: result.report.trades,
|
|
5431
|
+
pnl: +result.report.totalPnlPercent.toFixed(2),
|
|
5432
|
+
});
|
|
5433
|
+
}
|
|
5434
|
+
},
|
|
5435
|
+
},
|
|
5436
|
+
});
|
|
5437
|
+
// только out-of-sample: обучения нет, точка и трек-рекорд приходят
|
|
5438
|
+
// из конфига verbatim — test() перевыводит баны под правило точки,
|
|
5439
|
+
// невиданные авторы забанены по умолчанию
|
|
5440
|
+
const test = await Simulator.test({
|
|
5441
|
+
symbol,
|
|
5442
|
+
simulatorName: SIMULATOR_NAME,
|
|
5443
|
+
ideas,
|
|
5444
|
+
point: config.point,
|
|
5445
|
+
authorStats: config.authorStats,
|
|
5446
|
+
});
|
|
5447
|
+
const dumpName = values.output || `tune_${symbol}_${Date.now()}`;
|
|
5448
|
+
const dumpDir = join(process.cwd(), "dump");
|
|
5449
|
+
if (values.json) {
|
|
5450
|
+
const filePath = resolve(dumpDir, `${dumpName}.json`);
|
|
5451
|
+
await mkdir(dumpDir, { recursive: true });
|
|
5452
|
+
await writeFile(filePath, JSON.stringify(test, null, 2), "utf-8");
|
|
5453
|
+
console.log(`Saved: ${filePath}`);
|
|
5454
|
+
process.exit(0);
|
|
5455
|
+
}
|
|
5456
|
+
if (values.markdown) {
|
|
5457
|
+
const filePath = resolve(dumpDir, `${dumpName}.md`);
|
|
5458
|
+
await mkdir(dumpDir, { recursive: true });
|
|
5459
|
+
await writeFile(filePath, toMarkdown(test), "utf-8");
|
|
5460
|
+
console.log(`Saved: ${filePath}`);
|
|
5461
|
+
process.exit(0);
|
|
5462
|
+
}
|
|
5463
|
+
console.log(toMarkdown(test));
|
|
4808
5464
|
process.exit(0);
|
|
4809
5465
|
};
|
|
4810
5466
|
main$4();
|
|
@@ -5005,7 +5661,8 @@ Modes:
|
|
|
5005
5661
|
--dump Fetch and save raw OHLCV candles
|
|
5006
5662
|
--pnldebug Simulate PnL per minute for a given entry price and direction
|
|
5007
5663
|
--brokerdebug Fire a single broker commit against the live broker adapter
|
|
5008
|
-
--simulator <ideas.jsonl>
|
|
5664
|
+
--simulator <ideas.jsonl> [config.json] Feasibility probe over crowd ideas: is there a profitable corridor at all
|
|
5665
|
+
--tune <ideas.jsonl> <config.json> ONE out-of-sample shot of a frozen training artifact (point + author track)
|
|
5009
5666
|
--flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
|
|
5010
5667
|
--init Scaffold a new project in the current directory
|
|
5011
5668
|
--docker Scaffold a Docker workspace for running strategies in a container
|
|
@@ -5135,16 +5792,22 @@ Simulator flags (--simulator):
|
|
|
5135
5792
|
--markdown Save summary report to ./dump/<output>.md
|
|
5136
5793
|
--verbose Log every simulator lifecycle callback to the console
|
|
5137
5794
|
|
|
5138
|
-
|
|
5795
|
+
Positionals: path to an ideas .jsonl file — one idea per line with the exact shape
|
|
5139
5796
|
{ "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL",
|
|
5140
|
-
"author": string }
|
|
5141
|
-
|
|
5797
|
+
"author": string } — and an OPTIONAL config .json with the shape
|
|
5798
|
+
{ "gridAxes"?: ISimulatorGridAxes, "reportOrder"?: "sharpe"|"sortino"|"pnl"|"recovery" }.
|
|
5799
|
+
Both files are validated BEFORE any work; a mismatch aborts the run with an error.
|
|
5800
|
+
No config -> an empty object is used and the engine defaults apply (the full
|
|
5801
|
+
default grid axes and reportOrder "sharpe" of the connection service).
|
|
5142
5802
|
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5803
|
+
A FEASIBILITY PROBE, not an out-of-sample shot (that is --tune): the grid from
|
|
5804
|
+
the config (or the engine default) answers one question — does the feed contain
|
|
5805
|
+
a profitable corridor at all. One candle pass per idea to the grid's longest
|
|
5806
|
+
hold, flood dedupe (one idea per author per direction per 8h), default-ban
|
|
5807
|
+
author filter graded inside each point's own hold window, time-based
|
|
5808
|
+
Sharpe/Sortino, per-metric buckets with their own ranking winners and ban
|
|
5809
|
+
dictionaries. Ideas of other symbols are filtered out — one shared feed serves
|
|
5810
|
+
any --symbol.
|
|
5148
5811
|
|
|
5149
5812
|
No output flag → print the Markdown summary to stdout. With --verbose every
|
|
5150
5813
|
simulator lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
|
|
@@ -5153,6 +5816,33 @@ Simulator flags (--simulator):
|
|
|
5153
5816
|
Module file ./modules/simulator.module is loaded automatically if it exists
|
|
5154
5817
|
(register your exchange there); without it CCXT Binance is used by default.
|
|
5155
5818
|
|
|
5819
|
+
Tune flags (--tune):
|
|
5820
|
+
|
|
5821
|
+
--symbol <string> Trading pair to test (default: BTCUSDT)
|
|
5822
|
+
--exchange <string> Exchange name (default: first registered)
|
|
5823
|
+
--output <string> Output file base name (default: tune_{SYMBOL}_{TIMESTAMP})
|
|
5824
|
+
--json Save the full ISimulatorTestResult to ./dump/<output>.json
|
|
5825
|
+
--markdown Save the out-of-sample report to ./dump/<output>.md
|
|
5826
|
+
--verbose Log simulator lifecycle callbacks to the console
|
|
5827
|
+
|
|
5828
|
+
Positionals: path to an ideas .jsonl file (same shape and validation as
|
|
5829
|
+
--simulator) and a config .json carrying the FROZEN training artifact:
|
|
5830
|
+
{ "point": ISimulatorGridPoint, "authorStats": [{ "author", "ideas", "hits" }],
|
|
5831
|
+
"gridAxes"?, "reportOrder"? }. The point and authorStats are REQUIRED — without
|
|
5832
|
+
them there is nothing to test and the run aborts with an error. gridAxes are
|
|
5833
|
+
optional: by default they mirror the frozen point one value per axis (the grid
|
|
5834
|
+
is inert for a test).
|
|
5835
|
+
|
|
5836
|
+
ONE OUT-OF-SAMPLE SHOT, no training: the CLI never runs the sweep — pick your
|
|
5837
|
+
candidate elsewhere (a Simulator.run of your own), freeze its point and raw
|
|
5838
|
+
author track record into the config, and fire it once here via Simulator.test.
|
|
5839
|
+
Bans are re-derived from the frozen numbers under the point's rule; authors
|
|
5840
|
+
unseen in the config are banned by default. The report carries the result with
|
|
5841
|
+
the trade list and the frozen track record with re-derived ban flags.
|
|
5842
|
+
|
|
5843
|
+
Module file ./modules/tune.module is loaded automatically if it exists
|
|
5844
|
+
(register your exchange there); without it CCXT Binance is used by default.
|
|
5845
|
+
|
|
5156
5846
|
Flush flags (--flush):
|
|
5157
5847
|
|
|
5158
5848
|
One or more positional entry points. For each entry point the following
|
|
@@ -5186,7 +5876,8 @@ Module hooks (loaded automatically by each mode):
|
|
|
5186
5876
|
modules/dump.module --dump Exchange schema for candle dumps
|
|
5187
5877
|
modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
|
|
5188
5878
|
modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
|
|
5189
|
-
modules/simulator.module --simulator Exchange schema for crowd-ideas
|
|
5879
|
+
modules/simulator.module --simulator Exchange schema for the crowd-ideas feasibility probe
|
|
5880
|
+
modules/tune.module --tune Exchange schema for the walk-forward parameter search
|
|
5190
5881
|
|
|
5191
5882
|
--flush has no associated module. It only removes dump subdirectories.
|
|
5192
5883
|
|
|
@@ -5216,7 +5907,9 @@ Examples:
|
|
|
5216
5907
|
node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
|
|
5217
5908
|
node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
|
|
5218
5909
|
node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
|
|
5219
|
-
node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output
|
|
5910
|
+
node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_probe ./assets/tv-ideas.normalized.jsonl ./assets/probe.config.json
|
|
5911
|
+
node ${ENTRY_PATH} --tune --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
|
|
5912
|
+
node ${ENTRY_PATH} --tune --symbol BTCUSDT --markdown --output jun_2026_tune ./assets/tv-ideas.normalized.jsonl ./assets/tune.config.json
|
|
5220
5913
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
|
|
5221
5914
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
|
|
5222
5915
|
node ${ENTRY_PATH} --init --output my-trading-bot
|
|
@@ -5230,7 +5923,7 @@ const main$1 = async () => {
|
|
|
5230
5923
|
if (!values.help) {
|
|
5231
5924
|
return;
|
|
5232
5925
|
}
|
|
5233
|
-
process.stdout.write(`@backtest-kit/cli ${"
|
|
5926
|
+
process.stdout.write(`@backtest-kit/cli ${"17.0.0"}\n\n`);
|
|
5234
5927
|
process.stdout.write(HELP_TEXT);
|
|
5235
5928
|
process.exit(0);
|
|
5236
5929
|
};
|
|
@@ -5244,7 +5937,7 @@ const main = async () => {
|
|
|
5244
5937
|
if (!values.version) {
|
|
5245
5938
|
return;
|
|
5246
5939
|
}
|
|
5247
|
-
process.stdout.write(`@backtest-kit/cli ${"
|
|
5940
|
+
process.stdout.write(`@backtest-kit/cli ${"17.0.0"}\n`);
|
|
5248
5941
|
process.exit(0);
|
|
5249
5942
|
};
|
|
5250
5943
|
main();
|