@backtest-kit/cli 16.4.0 → 16.6.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 +901 -88
- package/build/index.mjs +906 -93
- package/config/notification.config.mjs +4 -0
- package/docker/package.json +6 -6
- package/package.json +13 -13
- package/template/order-continue.mustache +17 -0
- package/template/{signal-close.mustache → order-fill-close.mustache} +1 -1
- package/template/{signal-open.mustache → order-fill-open.mustache} +2 -1
- package/template/order-reject-close.mustache +17 -0
- package/template/order-reject-open.mustache +16 -0
- package/template/order-stop.mustache +18 -0
- package/template/project/package.mustache +5 -5
- package/types.d.ts +33 -7
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,14 @@ const getArgs = functoolsKit.singleshot(() => {
|
|
|
895
895
|
type: "boolean",
|
|
896
896
|
default: false,
|
|
897
897
|
},
|
|
898
|
+
tune: {
|
|
899
|
+
type: "boolean",
|
|
900
|
+
default: false,
|
|
901
|
+
},
|
|
902
|
+
split: {
|
|
903
|
+
type: "string",
|
|
904
|
+
default: "",
|
|
905
|
+
},
|
|
898
906
|
commit: {
|
|
899
907
|
type: "string",
|
|
900
908
|
default: "",
|
|
@@ -2341,6 +2349,13 @@ const STOP_BOT_FN = functoolsKit.singleshot(async () => {
|
|
|
2341
2349
|
const { stopBot } = await getTelegram();
|
|
2342
2350
|
stopBot();
|
|
2343
2351
|
});
|
|
2352
|
+
/**
|
|
2353
|
+
* Throttle window for the per-tick order_continue decision channel: at most one
|
|
2354
|
+
* Telegram notification per signalId per 15 minutes via functools-kit ttl. The
|
|
2355
|
+
* terminal order_stop is NOT throttled (fires once per signal) and clears the
|
|
2356
|
+
* throttle slot for its signalId.
|
|
2357
|
+
*/
|
|
2358
|
+
const ORDER_CHECK_NOTIFY_TTL = 15 * 60 * 1000;
|
|
2344
2359
|
class TelegramLogicService {
|
|
2345
2360
|
constructor() {
|
|
2346
2361
|
this.loggerService = inject(TYPES.loggerService);
|
|
@@ -2489,11 +2504,68 @@ class TelegramLogicService {
|
|
|
2489
2504
|
markdown,
|
|
2490
2505
|
});
|
|
2491
2506
|
};
|
|
2492
|
-
this.
|
|
2493
|
-
this.loggerService.log("telegramLogicService
|
|
2507
|
+
this.notifyOrderFillOpen = functoolsKit.trycatch(async (event) => {
|
|
2508
|
+
this.loggerService.log("telegramLogicService notifyOrderFillOpen", {
|
|
2509
|
+
event,
|
|
2510
|
+
});
|
|
2511
|
+
const markdown = await this.telegramTemplateService.getOrderFillOpenMarkdown(event);
|
|
2512
|
+
if (!markdown) {
|
|
2513
|
+
return;
|
|
2514
|
+
}
|
|
2515
|
+
await this.telegramWebService.publishNotify({
|
|
2516
|
+
symbol: event.symbol,
|
|
2517
|
+
markdown,
|
|
2518
|
+
});
|
|
2519
|
+
});
|
|
2520
|
+
this.notifyOrderFillClose = functoolsKit.trycatch(async (event) => {
|
|
2521
|
+
this.loggerService.log("telegramLogicService notifyOrderFillClose", {
|
|
2522
|
+
event,
|
|
2523
|
+
});
|
|
2524
|
+
const markdown = await this.telegramTemplateService.getOrderFillCloseMarkdown(event);
|
|
2525
|
+
if (!markdown) {
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
await this.telegramWebService.publishNotify({
|
|
2529
|
+
symbol: event.symbol,
|
|
2530
|
+
markdown,
|
|
2531
|
+
});
|
|
2532
|
+
});
|
|
2533
|
+
this.notifyOrderRejectOpen = functoolsKit.trycatch(async (event) => {
|
|
2534
|
+
this.loggerService.log("telegramLogicService notifyOrderRejectOpen", {
|
|
2535
|
+
event,
|
|
2536
|
+
});
|
|
2537
|
+
const markdown = await this.telegramTemplateService.getOrderRejectOpenMarkdown(event);
|
|
2538
|
+
if (!markdown) {
|
|
2539
|
+
return;
|
|
2540
|
+
}
|
|
2541
|
+
await this.telegramWebService.publishNotify({
|
|
2542
|
+
symbol: event.symbol,
|
|
2543
|
+
markdown,
|
|
2544
|
+
});
|
|
2545
|
+
});
|
|
2546
|
+
this.notifyOrderRejectClose = functoolsKit.trycatch(async (event) => {
|
|
2547
|
+
this.loggerService.log("telegramLogicService notifyOrderRejectClose", {
|
|
2548
|
+
event,
|
|
2549
|
+
});
|
|
2550
|
+
const markdown = await this.telegramTemplateService.getOrderRejectCloseMarkdown(event);
|
|
2551
|
+
if (!markdown) {
|
|
2552
|
+
return;
|
|
2553
|
+
}
|
|
2554
|
+
await this.telegramWebService.publishNotify({
|
|
2555
|
+
symbol: event.symbol,
|
|
2556
|
+
markdown,
|
|
2557
|
+
});
|
|
2558
|
+
});
|
|
2559
|
+
/**
|
|
2560
|
+
* Post-verdict order-check CONTINUE (order_continue). Fires every live tick the
|
|
2561
|
+
* order survives the check, so it is throttled via functools-kit ttl:
|
|
2562
|
+
* one message per signalId per ORDER_CHECK_NOTIFY_TTL (15 minutes).
|
|
2563
|
+
*/
|
|
2564
|
+
this.notifyOrderContinue = functoolsKit.ttl(functoolsKit.trycatch(async (event) => {
|
|
2565
|
+
this.loggerService.log("telegramLogicService notifyOrderContinue", {
|
|
2494
2566
|
event,
|
|
2495
2567
|
});
|
|
2496
|
-
const markdown = await this.telegramTemplateService.
|
|
2568
|
+
const markdown = await this.telegramTemplateService.getOrderContinueMarkdown(event);
|
|
2497
2569
|
if (!markdown) {
|
|
2498
2570
|
return;
|
|
2499
2571
|
}
|
|
@@ -2501,12 +2573,21 @@ class TelegramLogicService {
|
|
|
2501
2573
|
symbol: event.symbol,
|
|
2502
2574
|
markdown,
|
|
2503
2575
|
});
|
|
2576
|
+
}), {
|
|
2577
|
+
key: ([event]) => event.signalId,
|
|
2578
|
+
timeout: ORDER_CHECK_NOTIFY_TTL,
|
|
2504
2579
|
});
|
|
2505
|
-
|
|
2506
|
-
|
|
2580
|
+
/**
|
|
2581
|
+
* Post-verdict order-check STOP (order_stop). Terminal — fires once per signal,
|
|
2582
|
+
* so no throttle; drops the continue throttle slot for this signalId
|
|
2583
|
+
* (the signal leaves monitoring, the ttl entry would only leak otherwise).
|
|
2584
|
+
*/
|
|
2585
|
+
this.notifyOrderStop = functoolsKit.trycatch(async (event) => {
|
|
2586
|
+
this.loggerService.log("telegramLogicService notifyOrderStop", {
|
|
2507
2587
|
event,
|
|
2508
2588
|
});
|
|
2509
|
-
|
|
2589
|
+
this.notifyOrderContinue.clear(event.signalId);
|
|
2590
|
+
const markdown = await this.telegramTemplateService.getOrderStopMarkdown(event);
|
|
2510
2591
|
if (!markdown) {
|
|
2511
2592
|
return;
|
|
2512
2593
|
}
|
|
@@ -2611,26 +2692,54 @@ class TelegramLogicService {
|
|
|
2611
2692
|
return;
|
|
2612
2693
|
}
|
|
2613
2694
|
});
|
|
2614
|
-
|
|
2695
|
+
// Broker-CONFIRMED fills only (post-verdict orderFillSubject): a rejected or
|
|
2696
|
+
// transient gate attempt never reaches this channel, so "Order Filled" is
|
|
2697
|
+
// guaranteed truthful. The pre-verdict syncSubject is NOT consumed here.
|
|
2698
|
+
const unOrderFill = BacktestKit.listenOrderFill(async (event) => {
|
|
2615
2699
|
if (event.action === "signal-open") {
|
|
2616
|
-
// type "
|
|
2617
|
-
//
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2700
|
+
// Both open outcomes notify: type "active" — the order opened at once
|
|
2701
|
+
// (immediate fill, no schedule phase), type "schedule" — the resting
|
|
2702
|
+
// order was placed on the exchange. Either way the broker confirmed a
|
|
2703
|
+
// real order; the template's Order field carries the type.
|
|
2704
|
+
await this.notifyOrderFillOpen(event);
|
|
2705
|
+
return;
|
|
2706
|
+
}
|
|
2707
|
+
if (event.action === "signal-close") {
|
|
2708
|
+
await this.notifyOrderFillClose(event);
|
|
2709
|
+
return;
|
|
2710
|
+
}
|
|
2711
|
+
});
|
|
2712
|
+
// Terminal broker rejection (post-verdict orderRejectSubject): exactly one
|
|
2713
|
+
// event per dropped order attempt — the engine consumes the rejected signal
|
|
2714
|
+
// id, so this cannot repeat per-tick for the same signal. Dispatched by
|
|
2715
|
+
// action to the dedicated open/close notifications (single responsibility).
|
|
2716
|
+
const unOrderReject = BacktestKit.listenOrderReject(async (event) => {
|
|
2717
|
+
if (event.action === "signal-open") {
|
|
2718
|
+
await this.notifyOrderRejectOpen(event);
|
|
2622
2719
|
return;
|
|
2623
2720
|
}
|
|
2624
2721
|
if (event.action === "signal-close") {
|
|
2625
|
-
await this.
|
|
2722
|
+
await this.notifyOrderRejectClose(event);
|
|
2626
2723
|
return;
|
|
2627
2724
|
}
|
|
2628
2725
|
});
|
|
2726
|
+
// Post-verdict check decisions: CONTINUE fires every surviving tick and is
|
|
2727
|
+
// throttled to one message per signalId per 15 minutes via ttl; terminal
|
|
2728
|
+
// STOP fires once per signal, unthrottled.
|
|
2729
|
+
const unOrderContinue = BacktestKit.listenOrderContinue(async (event) => {
|
|
2730
|
+
await this.notifyOrderContinue(event);
|
|
2731
|
+
});
|
|
2732
|
+
const unOrderStop = BacktestKit.listenOrderStop(async (event) => {
|
|
2733
|
+
await this.notifyOrderStop(event);
|
|
2734
|
+
});
|
|
2629
2735
|
const unSignalNotify = BacktestKit.listenSignalNotify(async (event) => {
|
|
2630
2736
|
await this.notifySignalInfo(event);
|
|
2631
2737
|
});
|
|
2738
|
+
const unGc = functoolsKit.Source.fromInterval(90000).connect(() => {
|
|
2739
|
+
this.notifyOrderContinue.gc();
|
|
2740
|
+
});
|
|
2632
2741
|
const unConnect = () => this.connect.clear();
|
|
2633
|
-
const unListen = functoolsKit.compose(() => unRisk(), () => unSignal(), () => unCommit(), () =>
|
|
2742
|
+
const unListen = functoolsKit.compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unGc(), () => unSignalNotify(), () => unConnect());
|
|
2634
2743
|
return () => {
|
|
2635
2744
|
STOP_BOT_FN();
|
|
2636
2745
|
unListen();
|
|
@@ -2783,25 +2892,65 @@ class TelegramTemplateService {
|
|
|
2783
2892
|
}
|
|
2784
2893
|
return await RENDER_TEMPLATE_FN("average-buy.mustache", event, this);
|
|
2785
2894
|
};
|
|
2786
|
-
this.
|
|
2787
|
-
this.loggerService.log("telegramTemplateService
|
|
2895
|
+
this.getOrderFillOpenMarkdown = async (event) => {
|
|
2896
|
+
this.loggerService.log("telegramTemplateService getOrderFillOpenMarkdown", {
|
|
2897
|
+
event,
|
|
2898
|
+
});
|
|
2899
|
+
const adapter = await this.getTelegramAdapter();
|
|
2900
|
+
if (adapter?.getOrderFillOpenMarkdown) {
|
|
2901
|
+
return await adapter.getOrderFillOpenMarkdown(event);
|
|
2902
|
+
}
|
|
2903
|
+
return await RENDER_TEMPLATE_FN("order-fill-open.mustache", event, this);
|
|
2904
|
+
};
|
|
2905
|
+
this.getOrderFillCloseMarkdown = async (event) => {
|
|
2906
|
+
this.loggerService.log("telegramTemplateService getOrderFillCloseMarkdown", {
|
|
2907
|
+
event,
|
|
2908
|
+
});
|
|
2909
|
+
const adapter = await this.getTelegramAdapter();
|
|
2910
|
+
if (adapter?.getOrderFillCloseMarkdown) {
|
|
2911
|
+
return await adapter.getOrderFillCloseMarkdown(event);
|
|
2912
|
+
}
|
|
2913
|
+
return await RENDER_TEMPLATE_FN("order-fill-close.mustache", event, this);
|
|
2914
|
+
};
|
|
2915
|
+
this.getOrderRejectOpenMarkdown = async (event) => {
|
|
2916
|
+
this.loggerService.log("telegramTemplateService getOrderRejectOpenMarkdown", {
|
|
2917
|
+
event,
|
|
2918
|
+
});
|
|
2919
|
+
const adapter = await this.getTelegramAdapter();
|
|
2920
|
+
if (adapter?.getOrderRejectOpenMarkdown) {
|
|
2921
|
+
return await adapter.getOrderRejectOpenMarkdown(event);
|
|
2922
|
+
}
|
|
2923
|
+
return await RENDER_TEMPLATE_FN("order-reject-open.mustache", event, this);
|
|
2924
|
+
};
|
|
2925
|
+
this.getOrderRejectCloseMarkdown = async (event) => {
|
|
2926
|
+
this.loggerService.log("telegramTemplateService getOrderRejectCloseMarkdown", {
|
|
2927
|
+
event,
|
|
2928
|
+
});
|
|
2929
|
+
const adapter = await this.getTelegramAdapter();
|
|
2930
|
+
if (adapter?.getOrderRejectCloseMarkdown) {
|
|
2931
|
+
return await adapter.getOrderRejectCloseMarkdown(event);
|
|
2932
|
+
}
|
|
2933
|
+
return await RENDER_TEMPLATE_FN("order-reject-close.mustache", event, this);
|
|
2934
|
+
};
|
|
2935
|
+
this.getOrderContinueMarkdown = async (event) => {
|
|
2936
|
+
this.loggerService.log("telegramTemplateService getOrderContinueMarkdown", {
|
|
2788
2937
|
event,
|
|
2789
2938
|
});
|
|
2790
2939
|
const adapter = await this.getTelegramAdapter();
|
|
2791
|
-
if (adapter?.
|
|
2792
|
-
return await adapter.
|
|
2940
|
+
if (adapter?.getOrderContinueMarkdown) {
|
|
2941
|
+
return await adapter.getOrderContinueMarkdown(event);
|
|
2793
2942
|
}
|
|
2794
|
-
return await RENDER_TEMPLATE_FN("
|
|
2943
|
+
return await RENDER_TEMPLATE_FN("order-continue.mustache", event, this);
|
|
2795
2944
|
};
|
|
2796
|
-
this.
|
|
2797
|
-
this.loggerService.log("telegramTemplateService
|
|
2945
|
+
this.getOrderStopMarkdown = async (event) => {
|
|
2946
|
+
this.loggerService.log("telegramTemplateService getOrderStopMarkdown", {
|
|
2798
2947
|
event,
|
|
2799
2948
|
});
|
|
2800
2949
|
const adapter = await this.getTelegramAdapter();
|
|
2801
|
-
if (adapter?.
|
|
2802
|
-
return await adapter.
|
|
2950
|
+
if (adapter?.getOrderStopMarkdown) {
|
|
2951
|
+
return await adapter.getOrderStopMarkdown(event);
|
|
2803
2952
|
}
|
|
2804
|
-
return await RENDER_TEMPLATE_FN("
|
|
2953
|
+
return await RENDER_TEMPLATE_FN("order-stop.mustache", event, this);
|
|
2805
2954
|
};
|
|
2806
2955
|
this.getCancelScheduledMarkdown = async (event) => {
|
|
2807
2956
|
this.loggerService.log("telegramTemplateService getCancelScheduledMarkdown", {
|
|
@@ -3538,14 +3687,14 @@ const cli = {
|
|
|
3538
3687
|
};
|
|
3539
3688
|
init();
|
|
3540
3689
|
|
|
3541
|
-
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "flush", "init", "docker", "help", "version"];
|
|
3690
|
+
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "tune", "flush", "init", "docker", "help", "version"];
|
|
3542
3691
|
const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
|
|
3543
3692
|
const HELP_TEXT$1 = `
|
|
3544
3693
|
Example:
|
|
3545
3694
|
|
|
3546
3695
|
node ${ENTRY_PATH$1} --help
|
|
3547
3696
|
`.trimStart();
|
|
3548
|
-
const main$
|
|
3697
|
+
const main$k = async () => {
|
|
3549
3698
|
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)))) {
|
|
3550
3699
|
return;
|
|
3551
3700
|
}
|
|
@@ -3553,14 +3702,14 @@ const main$i = async () => {
|
|
|
3553
3702
|
if (MODES.some((mode) => values[mode])) {
|
|
3554
3703
|
return;
|
|
3555
3704
|
}
|
|
3556
|
-
process.stdout.write(`@backtest-kit/cli ${"16.
|
|
3705
|
+
process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
|
|
3557
3706
|
process.stdout.write("\n");
|
|
3558
3707
|
process.stdout.write(`Run with --help to see available commands.\n`);
|
|
3559
3708
|
process.stdout.write("\n");
|
|
3560
3709
|
process.stdout.write(HELP_TEXT$1);
|
|
3561
3710
|
process.exit(0);
|
|
3562
3711
|
};
|
|
3563
|
-
main$
|
|
3712
|
+
main$k();
|
|
3564
3713
|
|
|
3565
3714
|
const notifyShutdown = functoolsKit.singleshot(async () => {
|
|
3566
3715
|
console.log("Graceful shutdown initiated.");
|
|
@@ -3576,7 +3725,7 @@ const flush = async (entryPoint) => {
|
|
|
3576
3725
|
console.log(`Removed: ${target}`);
|
|
3577
3726
|
}
|
|
3578
3727
|
};
|
|
3579
|
-
const main$
|
|
3728
|
+
const main$j = async () => {
|
|
3580
3729
|
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)))) {
|
|
3581
3730
|
return;
|
|
3582
3731
|
}
|
|
@@ -3593,7 +3742,7 @@ const main$h = async () => {
|
|
|
3593
3742
|
}
|
|
3594
3743
|
process.exit(0);
|
|
3595
3744
|
};
|
|
3596
|
-
main$
|
|
3745
|
+
main$j();
|
|
3597
3746
|
|
|
3598
3747
|
const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
|
|
3599
3748
|
process.off("SIGINT", BEFORE_EXIT_FN$6);
|
|
@@ -3616,7 +3765,7 @@ const BEFORE_EXIT_FN$6 = functoolsKit.singleshot(async () => {
|
|
|
3616
3765
|
const listenGracefulShutdown$6 = functoolsKit.singleshot(() => {
|
|
3617
3766
|
process.on("SIGINT", BEFORE_EXIT_FN$6);
|
|
3618
3767
|
});
|
|
3619
|
-
const main$
|
|
3768
|
+
const main$i = async () => {
|
|
3620
3769
|
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)))) {
|
|
3621
3770
|
return;
|
|
3622
3771
|
}
|
|
@@ -3634,7 +3783,7 @@ const main$g = async () => {
|
|
|
3634
3783
|
await cli.backtestMainService.connect();
|
|
3635
3784
|
listenGracefulShutdown$6();
|
|
3636
3785
|
};
|
|
3637
|
-
main$
|
|
3786
|
+
main$i();
|
|
3638
3787
|
|
|
3639
3788
|
const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
|
|
3640
3789
|
process.off("SIGINT", BEFORE_EXIT_FN$5);
|
|
@@ -3653,7 +3802,7 @@ const BEFORE_EXIT_FN$5 = functoolsKit.singleshot(async () => {
|
|
|
3653
3802
|
const listenGracefulShutdown$5 = functoolsKit.singleshot(() => {
|
|
3654
3803
|
process.on("SIGINT", BEFORE_EXIT_FN$5);
|
|
3655
3804
|
});
|
|
3656
|
-
const main$
|
|
3805
|
+
const main$h = async () => {
|
|
3657
3806
|
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)))) {
|
|
3658
3807
|
return;
|
|
3659
3808
|
}
|
|
@@ -3672,7 +3821,7 @@ const main$f = async () => {
|
|
|
3672
3821
|
listenGracefulShutdown$5();
|
|
3673
3822
|
await cli.walkerMainService.connect();
|
|
3674
3823
|
};
|
|
3675
|
-
main$
|
|
3824
|
+
main$h();
|
|
3676
3825
|
|
|
3677
3826
|
const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
3678
3827
|
process.off("SIGINT", BEFORE_EXIT_FN$4);
|
|
@@ -3694,7 +3843,7 @@ const BEFORE_EXIT_FN$4 = functoolsKit.singleshot(async () => {
|
|
|
3694
3843
|
const listenGracefulShutdown$4 = functoolsKit.singleshot(() => {
|
|
3695
3844
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
3696
3845
|
});
|
|
3697
|
-
const main$
|
|
3846
|
+
const main$g = async () => {
|
|
3698
3847
|
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
3699
3848
|
return;
|
|
3700
3849
|
}
|
|
@@ -3708,7 +3857,7 @@ const main$e = async () => {
|
|
|
3708
3857
|
await cli.paperMainService.connect();
|
|
3709
3858
|
listenGracefulShutdown$4();
|
|
3710
3859
|
};
|
|
3711
|
-
main$
|
|
3860
|
+
main$g();
|
|
3712
3861
|
|
|
3713
3862
|
const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
3714
3863
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -3730,7 +3879,7 @@ const BEFORE_EXIT_FN$3 = functoolsKit.singleshot(async () => {
|
|
|
3730
3879
|
const listenGracefulShutdown$3 = functoolsKit.singleshot(() => {
|
|
3731
3880
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
3732
3881
|
});
|
|
3733
|
-
const main$
|
|
3882
|
+
const main$f = async () => {
|
|
3734
3883
|
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)))) {
|
|
3735
3884
|
return;
|
|
3736
3885
|
}
|
|
@@ -3744,7 +3893,7 @@ const main$d = async () => {
|
|
|
3744
3893
|
await cli.liveMainService.connect();
|
|
3745
3894
|
listenGracefulShutdown$3();
|
|
3746
3895
|
};
|
|
3747
|
-
main$
|
|
3896
|
+
main$f();
|
|
3748
3897
|
|
|
3749
3898
|
const stopBacktestList$1 = async () => {
|
|
3750
3899
|
for (const item of await BacktestKit.Backtest.list()) {
|
|
@@ -3808,7 +3957,7 @@ const BEFORE_EXIT_FN$2 = functoolsKit.singleshot(async () => {
|
|
|
3808
3957
|
const listenGracefulShutdown$2 = functoolsKit.singleshot(() => {
|
|
3809
3958
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
3810
3959
|
});
|
|
3811
|
-
const main$
|
|
3960
|
+
const main$e = async () => {
|
|
3812
3961
|
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)))) {
|
|
3813
3962
|
return;
|
|
3814
3963
|
}
|
|
@@ -3864,7 +4013,7 @@ const main$c = async () => {
|
|
|
3864
4013
|
listenGracefulShutdown$2();
|
|
3865
4014
|
await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
|
|
3866
4015
|
};
|
|
3867
|
-
main$
|
|
4016
|
+
main$e();
|
|
3868
4017
|
|
|
3869
4018
|
const MODE_MODULE = {
|
|
3870
4019
|
backtest: "backtest.module",
|
|
@@ -3967,7 +4116,7 @@ const createGracefulShutdown = (mode) => {
|
|
|
3967
4116
|
process.on("SIGINT", handler);
|
|
3968
4117
|
});
|
|
3969
4118
|
};
|
|
3970
|
-
const main$
|
|
4119
|
+
const main$d = async () => {
|
|
3971
4120
|
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)))) {
|
|
3972
4121
|
return;
|
|
3973
4122
|
}
|
|
@@ -4043,7 +4192,7 @@ const main$b = async () => {
|
|
|
4043
4192
|
}
|
|
4044
4193
|
await getEntrySubject().next(absolutePath);
|
|
4045
4194
|
};
|
|
4046
|
-
main$
|
|
4195
|
+
main$d();
|
|
4047
4196
|
|
|
4048
4197
|
const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
4049
4198
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -4054,7 +4203,7 @@ const BEFORE_EXIT_FN$1 = functoolsKit.singleshot(async () => {
|
|
|
4054
4203
|
const listenGracefulShutdown$1 = functoolsKit.singleshot(() => {
|
|
4055
4204
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
4056
4205
|
});
|
|
4057
|
-
const main$
|
|
4206
|
+
const main$c = async () => {
|
|
4058
4207
|
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)))) {
|
|
4059
4208
|
return;
|
|
4060
4209
|
}
|
|
@@ -4064,7 +4213,7 @@ const main$a = async () => {
|
|
|
4064
4213
|
}
|
|
4065
4214
|
listenGracefulShutdown$1();
|
|
4066
4215
|
};
|
|
4067
|
-
main$
|
|
4216
|
+
main$c();
|
|
4068
4217
|
|
|
4069
4218
|
const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
4070
4219
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -4075,7 +4224,7 @@ const BEFORE_EXIT_FN = functoolsKit.singleshot(async () => {
|
|
|
4075
4224
|
const listenGracefulShutdown = functoolsKit.singleshot(() => {
|
|
4076
4225
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
4077
4226
|
});
|
|
4078
|
-
const main$
|
|
4227
|
+
const main$b = async () => {
|
|
4079
4228
|
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)))) {
|
|
4080
4229
|
return;
|
|
4081
4230
|
}
|
|
@@ -4085,7 +4234,7 @@ const main$9 = async () => {
|
|
|
4085
4234
|
}
|
|
4086
4235
|
listenGracefulShutdown();
|
|
4087
4236
|
};
|
|
4088
|
-
main$
|
|
4237
|
+
main$b();
|
|
4089
4238
|
|
|
4090
4239
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
4091
4240
|
const keys = Object.keys(schema);
|
|
@@ -4107,7 +4256,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
4107
4256
|
}
|
|
4108
4257
|
return rows;
|
|
4109
4258
|
};
|
|
4110
|
-
const main$
|
|
4259
|
+
const main$a = async () => {
|
|
4111
4260
|
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)))) {
|
|
4112
4261
|
return;
|
|
4113
4262
|
}
|
|
@@ -4207,9 +4356,9 @@ const main$8 = async () => {
|
|
|
4207
4356
|
console.log(await BacktestKitPinets.toMarkdown(signalId, plots, signalSchema));
|
|
4208
4357
|
process.exit(0);
|
|
4209
4358
|
};
|
|
4210
|
-
main$
|
|
4359
|
+
main$a();
|
|
4211
4360
|
|
|
4212
|
-
const main$
|
|
4361
|
+
const main$9 = async () => {
|
|
4213
4362
|
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)))) {
|
|
4214
4363
|
return;
|
|
4215
4364
|
}
|
|
@@ -4272,9 +4421,9 @@ const main$7 = async () => {
|
|
|
4272
4421
|
};
|
|
4273
4422
|
process.on("SIGINT", beforeExit);
|
|
4274
4423
|
};
|
|
4275
|
-
main$
|
|
4424
|
+
main$9();
|
|
4276
4425
|
|
|
4277
|
-
const main$
|
|
4426
|
+
const main$8 = async () => {
|
|
4278
4427
|
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)))) {
|
|
4279
4428
|
return;
|
|
4280
4429
|
}
|
|
@@ -4355,9 +4504,9 @@ const main$6 = async () => {
|
|
|
4355
4504
|
console.log(JSON.stringify(candles, null, 2));
|
|
4356
4505
|
process.exit(0);
|
|
4357
4506
|
};
|
|
4358
|
-
main$
|
|
4507
|
+
main$8();
|
|
4359
4508
|
|
|
4360
|
-
const main$
|
|
4509
|
+
const main$7 = async () => {
|
|
4361
4510
|
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)))) {
|
|
4362
4511
|
return;
|
|
4363
4512
|
}
|
|
@@ -4471,11 +4620,314 @@ const main$5 = async () => {
|
|
|
4471
4620
|
}
|
|
4472
4621
|
process.exit(0);
|
|
4473
4622
|
};
|
|
4474
|
-
main$
|
|
4623
|
+
main$7();
|
|
4475
4624
|
|
|
4476
|
-
const
|
|
4477
|
-
|
|
4478
|
-
|
|
4625
|
+
const getBroker = functoolsKit.singleshot(() => {
|
|
4626
|
+
// Reaches into BrokerAdapter internals on purpose: the public commit*
|
|
4627
|
+
// methods skip when payload.backtest=true and require enable(), while
|
|
4628
|
+
// brokerdebug must fire exactly one commit at the raw adapter proxy.
|
|
4629
|
+
const broker = BacktestKit.Broker["getInstance"]();
|
|
4630
|
+
if (!broker) {
|
|
4631
|
+
throw new Error("Broker instance is not initialized.");
|
|
4632
|
+
}
|
|
4633
|
+
return broker;
|
|
4634
|
+
});
|
|
4635
|
+
/** Called when a new signal is opened (position entry confirmed). */
|
|
4636
|
+
const commitOrderOpen = functoolsKit.trycatch(async (payload) => {
|
|
4637
|
+
const broker = getBroker();
|
|
4638
|
+
return await broker.onOrderOpenCommit(payload);
|
|
4639
|
+
}, {
|
|
4640
|
+
fallback: (error) => {
|
|
4641
|
+
console.log(error);
|
|
4642
|
+
process.exit(-1);
|
|
4643
|
+
},
|
|
4644
|
+
});
|
|
4645
|
+
/** Called when a new signal is closed (take-profit, stop-loss, or manual close). */
|
|
4646
|
+
const commitOrderClose = functoolsKit.trycatch(async (payload) => {
|
|
4647
|
+
const broker = getBroker();
|
|
4648
|
+
return await broker.onOrderCloseCommit(payload);
|
|
4649
|
+
}, {
|
|
4650
|
+
fallback: (error) => {
|
|
4651
|
+
console.log(error);
|
|
4652
|
+
process.exit(-1);
|
|
4653
|
+
},
|
|
4654
|
+
});
|
|
4655
|
+
/** Called when a partial profit close is committed. */
|
|
4656
|
+
const commitPartialProfit = functoolsKit.trycatch(async (payload) => {
|
|
4657
|
+
const broker = getBroker();
|
|
4658
|
+
return await broker.onPartialProfitCommit(payload);
|
|
4659
|
+
}, {
|
|
4660
|
+
fallback: (error) => {
|
|
4661
|
+
console.log(error);
|
|
4662
|
+
process.exit(-1);
|
|
4663
|
+
},
|
|
4664
|
+
});
|
|
4665
|
+
/** Called when a partial loss close is committed. */
|
|
4666
|
+
const commitPartialLoss = functoolsKit.trycatch(async (payload) => {
|
|
4667
|
+
const broker = getBroker();
|
|
4668
|
+
return await broker.onPartialLossCommit(payload);
|
|
4669
|
+
}, {
|
|
4670
|
+
fallback: (error) => {
|
|
4671
|
+
console.log(error);
|
|
4672
|
+
process.exit(-1);
|
|
4673
|
+
},
|
|
4674
|
+
});
|
|
4675
|
+
/** Called when a trailing stop update is committed. */
|
|
4676
|
+
const commitTrailingStop = functoolsKit.trycatch(async (payload) => {
|
|
4677
|
+
const broker = getBroker();
|
|
4678
|
+
return await broker.onTrailingStopCommit(payload);
|
|
4679
|
+
}, {
|
|
4680
|
+
fallback: (error) => {
|
|
4681
|
+
console.log(error);
|
|
4682
|
+
process.exit(-1);
|
|
4683
|
+
},
|
|
4684
|
+
});
|
|
4685
|
+
/** Called when a trailing take-profit update is committed. */
|
|
4686
|
+
const commitTrailingTake = functoolsKit.trycatch(async (payload) => {
|
|
4687
|
+
const broker = getBroker();
|
|
4688
|
+
return await broker.onTrailingTakeCommit(payload);
|
|
4689
|
+
}, {
|
|
4690
|
+
fallback: (error) => {
|
|
4691
|
+
console.log(error);
|
|
4692
|
+
process.exit(-1);
|
|
4693
|
+
},
|
|
4694
|
+
});
|
|
4695
|
+
/** Called when a breakeven stop is committed (stop loss moved to entry price). */
|
|
4696
|
+
const commitBreakeven = functoolsKit.trycatch(async (payload) => {
|
|
4697
|
+
const broker = getBroker();
|
|
4698
|
+
return await broker.onBreakevenCommit(payload);
|
|
4699
|
+
}, {
|
|
4700
|
+
fallback: (error) => {
|
|
4701
|
+
console.log(error);
|
|
4702
|
+
process.exit(-1);
|
|
4703
|
+
},
|
|
4704
|
+
});
|
|
4705
|
+
/** Called when a DCA (average-buy) entry is committed. */
|
|
4706
|
+
const commitAverageBuy = functoolsKit.trycatch(async (payload) => {
|
|
4707
|
+
const broker = getBroker();
|
|
4708
|
+
return await broker.onAverageBuyCommit(payload);
|
|
4709
|
+
}, {
|
|
4710
|
+
fallback: (error) => {
|
|
4711
|
+
console.log(error);
|
|
4712
|
+
process.exit(-1);
|
|
4713
|
+
},
|
|
4714
|
+
});
|
|
4715
|
+
const COMMITS = [
|
|
4716
|
+
"signal-open",
|
|
4717
|
+
"signal-close",
|
|
4718
|
+
"partial-profit",
|
|
4719
|
+
"partial-loss",
|
|
4720
|
+
"average-buy",
|
|
4721
|
+
"trailing-stop",
|
|
4722
|
+
"trailing-take",
|
|
4723
|
+
"breakeven",
|
|
4724
|
+
];
|
|
4725
|
+
const ZERO_PNL = {
|
|
4726
|
+
pnlPercentage: 0,
|
|
4727
|
+
priceOpen: 0,
|
|
4728
|
+
priceClose: 0,
|
|
4729
|
+
pnlCost: 0,
|
|
4730
|
+
pnlEntries: 0,
|
|
4731
|
+
};
|
|
4732
|
+
const main$6 = async () => {
|
|
4733
|
+
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
4734
|
+
return;
|
|
4735
|
+
}
|
|
4736
|
+
const { values } = getArgs();
|
|
4737
|
+
if (!values.brokerdebug) {
|
|
4738
|
+
return;
|
|
4739
|
+
}
|
|
4740
|
+
{
|
|
4741
|
+
const cwd = process.cwd();
|
|
4742
|
+
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
4743
|
+
}
|
|
4744
|
+
await cli.configConnectionService.loadConfig("setup.config");
|
|
4745
|
+
{
|
|
4746
|
+
const loader = await cli.configConnectionService.loadConfig("loader.config");
|
|
4747
|
+
try {
|
|
4748
|
+
if (typeof loader === "function") {
|
|
4749
|
+
await loader();
|
|
4750
|
+
}
|
|
4751
|
+
if (typeof loader?.loader === "function") {
|
|
4752
|
+
await loader.loader();
|
|
4753
|
+
}
|
|
4754
|
+
}
|
|
4755
|
+
catch (error) {
|
|
4756
|
+
console.error("Module loader failed", error);
|
|
4757
|
+
process.exit(-1);
|
|
4758
|
+
}
|
|
4759
|
+
}
|
|
4760
|
+
await cli.moduleConnectionService.loadModule("brokerdebug.module");
|
|
4761
|
+
const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
|
|
4762
|
+
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
4763
|
+
const symbol = values.symbol || "BTCUSDT";
|
|
4764
|
+
const commit = values.commit || "signal-open";
|
|
4765
|
+
if (!COMMITS.includes(commit)) {
|
|
4766
|
+
console.error(`Unknown --commit value: ${commit}`);
|
|
4767
|
+
console.error(`Available: ${COMMITS.join(", ")}`);
|
|
4768
|
+
process.exit(1);
|
|
4769
|
+
}
|
|
4770
|
+
const candles = await BacktestKit.Exchange.getCandles(symbol, "1m", 5, { exchangeName });
|
|
4771
|
+
const lastCandle = candles[candles.length - 1];
|
|
4772
|
+
const currentPrice = lastCandle.close;
|
|
4773
|
+
const priceTakeProfit = currentPrice * 1.02;
|
|
4774
|
+
const priceStopLoss = currentPrice * 0.98;
|
|
4775
|
+
const context = {
|
|
4776
|
+
strategyName: "broker-test",
|
|
4777
|
+
exchangeName: exchangeName,
|
|
4778
|
+
};
|
|
4779
|
+
const signalId = `broker-debug-${Date.now()}`;
|
|
4780
|
+
const when = BacktestKit.alignToInterval(new Date(), "1m");
|
|
4781
|
+
{
|
|
4782
|
+
console.log(`symbol=${symbol} price=${currentPrice} commit=${commit}`);
|
|
4783
|
+
console.time(commit);
|
|
4784
|
+
}
|
|
4785
|
+
const run = async () => {
|
|
4786
|
+
if (commit === "signal-open") {
|
|
4787
|
+
await commitOrderOpen({
|
|
4788
|
+
type: "active",
|
|
4789
|
+
symbol,
|
|
4790
|
+
signalId,
|
|
4791
|
+
cost: 100,
|
|
4792
|
+
position: "long",
|
|
4793
|
+
priceOpen: currentPrice,
|
|
4794
|
+
priceTakeProfit,
|
|
4795
|
+
priceStopLoss,
|
|
4796
|
+
pnl: ZERO_PNL,
|
|
4797
|
+
peakProfit: ZERO_PNL,
|
|
4798
|
+
maxDrawdown: ZERO_PNL,
|
|
4799
|
+
attempt: 0,
|
|
4800
|
+
context,
|
|
4801
|
+
when,
|
|
4802
|
+
backtest: true,
|
|
4803
|
+
});
|
|
4804
|
+
}
|
|
4805
|
+
if (commit === "signal-close") {
|
|
4806
|
+
await commitOrderClose({
|
|
4807
|
+
symbol,
|
|
4808
|
+
signalId,
|
|
4809
|
+
cost: 100,
|
|
4810
|
+
position: "long",
|
|
4811
|
+
currentPrice,
|
|
4812
|
+
priceOpen: currentPrice,
|
|
4813
|
+
priceTakeProfit,
|
|
4814
|
+
priceStopLoss,
|
|
4815
|
+
totalEntries: 1,
|
|
4816
|
+
totalPartials: 0,
|
|
4817
|
+
attempt: 0,
|
|
4818
|
+
pnl: ZERO_PNL,
|
|
4819
|
+
peakProfit: ZERO_PNL,
|
|
4820
|
+
maxDrawdown: ZERO_PNL,
|
|
4821
|
+
context,
|
|
4822
|
+
when,
|
|
4823
|
+
backtest: true,
|
|
4824
|
+
});
|
|
4825
|
+
}
|
|
4826
|
+
if (commit === "partial-profit") {
|
|
4827
|
+
await commitPartialProfit({
|
|
4828
|
+
symbol,
|
|
4829
|
+
signalId,
|
|
4830
|
+
percentToClose: 50,
|
|
4831
|
+
cost: 50,
|
|
4832
|
+
currentPrice,
|
|
4833
|
+
position: "long",
|
|
4834
|
+
priceTakeProfit,
|
|
4835
|
+
priceStopLoss,
|
|
4836
|
+
context,
|
|
4837
|
+
when,
|
|
4838
|
+
backtest: true,
|
|
4839
|
+
});
|
|
4840
|
+
}
|
|
4841
|
+
if (commit === "partial-loss") {
|
|
4842
|
+
await commitPartialLoss({
|
|
4843
|
+
symbol,
|
|
4844
|
+
signalId,
|
|
4845
|
+
percentToClose: 50,
|
|
4846
|
+
cost: 50,
|
|
4847
|
+
currentPrice,
|
|
4848
|
+
position: "long",
|
|
4849
|
+
priceTakeProfit,
|
|
4850
|
+
priceStopLoss,
|
|
4851
|
+
context,
|
|
4852
|
+
when,
|
|
4853
|
+
backtest: true,
|
|
4854
|
+
});
|
|
4855
|
+
}
|
|
4856
|
+
if (commit === "average-buy") {
|
|
4857
|
+
await commitAverageBuy({
|
|
4858
|
+
symbol,
|
|
4859
|
+
signalId,
|
|
4860
|
+
currentPrice,
|
|
4861
|
+
cost: 100,
|
|
4862
|
+
position: "long",
|
|
4863
|
+
priceTakeProfit,
|
|
4864
|
+
priceStopLoss,
|
|
4865
|
+
context,
|
|
4866
|
+
when,
|
|
4867
|
+
backtest: true,
|
|
4868
|
+
});
|
|
4869
|
+
}
|
|
4870
|
+
if (commit === "trailing-stop") {
|
|
4871
|
+
await commitTrailingStop({
|
|
4872
|
+
symbol,
|
|
4873
|
+
signalId,
|
|
4874
|
+
percentShift: -50,
|
|
4875
|
+
currentPrice,
|
|
4876
|
+
newStopLossPrice: priceStopLoss + (currentPrice - priceStopLoss) * 0.5,
|
|
4877
|
+
takeProfitPrice: priceTakeProfit,
|
|
4878
|
+
position: "long",
|
|
4879
|
+
context,
|
|
4880
|
+
when,
|
|
4881
|
+
backtest: true,
|
|
4882
|
+
});
|
|
4883
|
+
}
|
|
4884
|
+
if (commit === "trailing-take") {
|
|
4885
|
+
await commitTrailingTake({
|
|
4886
|
+
symbol,
|
|
4887
|
+
signalId,
|
|
4888
|
+
percentShift: 50,
|
|
4889
|
+
currentPrice,
|
|
4890
|
+
newTakeProfitPrice: priceTakeProfit + (priceTakeProfit - currentPrice) * 0.5,
|
|
4891
|
+
takeProfitPrice: priceTakeProfit,
|
|
4892
|
+
position: "long",
|
|
4893
|
+
context,
|
|
4894
|
+
when,
|
|
4895
|
+
backtest: true,
|
|
4896
|
+
});
|
|
4897
|
+
}
|
|
4898
|
+
if (commit === "breakeven") {
|
|
4899
|
+
await commitBreakeven({
|
|
4900
|
+
symbol,
|
|
4901
|
+
signalId,
|
|
4902
|
+
currentPrice,
|
|
4903
|
+
newStopLossPrice: currentPrice,
|
|
4904
|
+
newTakeProfitPrice: priceTakeProfit,
|
|
4905
|
+
position: "long",
|
|
4906
|
+
context,
|
|
4907
|
+
when,
|
|
4908
|
+
backtest: true,
|
|
4909
|
+
});
|
|
4910
|
+
}
|
|
4911
|
+
};
|
|
4912
|
+
await BacktestKit.runInMockContext(run, {
|
|
4913
|
+
when,
|
|
4914
|
+
strategyName: context.strategyName,
|
|
4915
|
+
exchangeName: context.exchangeName,
|
|
4916
|
+
frameName: "",
|
|
4917
|
+
symbol,
|
|
4918
|
+
backtest: true,
|
|
4919
|
+
});
|
|
4920
|
+
{
|
|
4921
|
+
console.log(`${commit} OK`);
|
|
4922
|
+
console.timeEnd(commit);
|
|
4923
|
+
}
|
|
4924
|
+
process.exit(0);
|
|
4925
|
+
};
|
|
4926
|
+
main$6();
|
|
4927
|
+
|
|
4928
|
+
const SIMULATOR_NAME$1 = "cli_simulator";
|
|
4929
|
+
const IDEA_DIRECTIONS$1 = ["LONG", "SHORT", "NEUTRAL"];
|
|
4930
|
+
const validateIdea$1 = (idea, line) => {
|
|
4479
4931
|
if (typeof idea !== "object" || idea === null) {
|
|
4480
4932
|
return `line ${line}: not an object`;
|
|
4481
4933
|
}
|
|
@@ -4488,15 +4940,16 @@ const validateIdea = (idea, line) => {
|
|
|
4488
4940
|
if (typeof idea.symbol !== "string" || !idea.symbol) {
|
|
4489
4941
|
return `line ${line}: "symbol" must be a non-empty string`;
|
|
4490
4942
|
}
|
|
4491
|
-
if (!IDEA_DIRECTIONS.includes(idea.direction)) {
|
|
4492
|
-
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
4943
|
+
if (!IDEA_DIRECTIONS$1.includes(idea.direction)) {
|
|
4944
|
+
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS$1.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
4493
4945
|
}
|
|
4494
4946
|
if (typeof idea.author !== "string" || !idea.author) {
|
|
4495
4947
|
return `line ${line}: "author" must be a non-empty string`;
|
|
4496
4948
|
}
|
|
4497
4949
|
return null;
|
|
4498
4950
|
};
|
|
4499
|
-
const toMarkdown = (result) => {
|
|
4951
|
+
const toMarkdown$1 = (result) => {
|
|
4952
|
+
const profitable = result.reports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
|
|
4500
4953
|
const lines = [];
|
|
4501
4954
|
lines.push(`# Simulator Report — ${result.symbol}`);
|
|
4502
4955
|
lines.push("");
|
|
@@ -4505,33 +4958,36 @@ const toMarkdown = (result) => {
|
|
|
4505
4958
|
lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
|
|
4506
4959
|
lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
|
|
4507
4960
|
lines.push(`| Authors allowed / banned | ${result.allowedAuthors.length} / ${result.bannedAuthors.length} |`);
|
|
4508
|
-
lines.push(`|
|
|
4961
|
+
lines.push(`| Profitable corridor | ${profitable} / ${result.reports.length} grid points |`);
|
|
4509
4962
|
lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
|
|
4510
4963
|
lines.push("");
|
|
4511
4964
|
lines.push(`## Ranking winners`);
|
|
4512
4965
|
lines.push("");
|
|
4513
|
-
lines.push(`| Criterion | Stop% |
|
|
4514
|
-
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
|
|
4966
|
+
lines.push(`| Criterion | Stop% | Hold | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
|
|
4967
|
+
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
|
|
4515
4968
|
for (const best of result.best) {
|
|
4516
4969
|
if (!best.report) {
|
|
4517
|
-
lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | —
|
|
4970
|
+
lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
|
|
4518
4971
|
continue;
|
|
4519
4972
|
}
|
|
4520
4973
|
const { point } = best.report;
|
|
4521
|
-
lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.
|
|
4974
|
+
lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.holdMinutes / 60}h | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
|
|
4522
4975
|
`${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
|
|
4523
4976
|
}
|
|
4524
4977
|
lines.push("");
|
|
4525
|
-
lines.push(`## Allowed authors (
|
|
4978
|
+
lines.push(`## Allowed authors (whitelist of the sharpe winner rule)`);
|
|
4526
4979
|
lines.push("");
|
|
4527
4980
|
lines.push(`| Author | Ideas | Hits | HitRate |`);
|
|
4528
4981
|
lines.push(`| --- | --- | --- | --- |`);
|
|
4529
|
-
|
|
4982
|
+
// артефакт авторов живёт по-победительно в best[] — ран-левел
|
|
4983
|
+
// authorStats в ISimulatorResult больше нет
|
|
4984
|
+
const sharpeBest = result.best.find(({ criterion }) => criterion === "sharpe");
|
|
4985
|
+
for (const stat of (sharpeBest?.authorStats ?? []).filter(({ banned }) => !banned)) {
|
|
4530
4986
|
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
|
|
4531
4987
|
}
|
|
4532
4988
|
return lines.join("\n");
|
|
4533
4989
|
};
|
|
4534
|
-
const main$
|
|
4990
|
+
const main$5 = async () => {
|
|
4535
4991
|
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)))) {
|
|
4536
4992
|
return;
|
|
4537
4993
|
}
|
|
@@ -4568,7 +5024,7 @@ const main$4 = async () => {
|
|
|
4568
5024
|
console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
|
|
4569
5025
|
process.exit(1);
|
|
4570
5026
|
}
|
|
4571
|
-
const problem = validateIdea(idea, i + 1);
|
|
5027
|
+
const problem = validateIdea$1(idea, i + 1);
|
|
4572
5028
|
if (problem) {
|
|
4573
5029
|
console.error(`Error: ideas file does not match the idea structure — ${problem}`);
|
|
4574
5030
|
console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
|
|
@@ -4604,11 +5060,48 @@ const main$4 = async () => {
|
|
|
4604
5060
|
}
|
|
4605
5061
|
const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
|
|
4606
5062
|
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
5063
|
+
if (values.verbose) {
|
|
5064
|
+
BacktestKit.overrideExchangeSchema({
|
|
5065
|
+
exchangeName,
|
|
5066
|
+
callbacks: {
|
|
5067
|
+
onCandleData(symbol, interval, since) {
|
|
5068
|
+
console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
|
|
5069
|
+
},
|
|
5070
|
+
},
|
|
5071
|
+
});
|
|
5072
|
+
}
|
|
4607
5073
|
const symbol = values.symbol || "BTCUSDT";
|
|
5074
|
+
// Проба осуществимости, НЕ поиск заработка (подбор параметров — у
|
|
5075
|
+
// --tune): собирающая прибыль механика выключена, остаётся вопрос
|
|
5076
|
+
// "есть ли прибыльный коридор" по стопу x холду x правилу бана —
|
|
5077
|
+
// 48 точек, быстрый вердикт
|
|
4608
5078
|
BacktestKit.addSimulatorSchema({
|
|
4609
|
-
simulatorName: SIMULATOR_NAME,
|
|
5079
|
+
simulatorName: SIMULATOR_NAME$1,
|
|
4610
5080
|
exchangeName,
|
|
5081
|
+
gridAxes: {
|
|
5082
|
+
// грубая шкала катастрофы: коридор должен быть широким, не точкой
|
|
5083
|
+
hardStopPercent: [2, 3, 5, 7],
|
|
5084
|
+
// инертен: проба не собирает прибыль, выход — по времени или стопу
|
|
5085
|
+
trailingTakePercent: [100],
|
|
5086
|
+
holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
|
|
5087
|
+
// одного проверенного автора достаточно — консенсус не перебираем
|
|
5088
|
+
minIdeasAligned: [1],
|
|
5089
|
+
// правило бана — единственная перебираемая "умность" пробы
|
|
5090
|
+
minAuthorTrack: [3, 5],
|
|
5091
|
+
minAuthorHitRate: [0.5, 0.6],
|
|
5092
|
+
minAuthorWilson: [0],
|
|
5093
|
+
minWeightAligned: [0],
|
|
5094
|
+
profitLockPercent: [0],
|
|
5095
|
+
authorMetric: ["close"],
|
|
5096
|
+
banCriteria: ["sharpe", "pnl"],
|
|
5097
|
+
},
|
|
5098
|
+
reportOrder: "sharpe",
|
|
4611
5099
|
callbacks: {
|
|
5100
|
+
onProgress: (symbol, stage, processed, total) => {
|
|
5101
|
+
if (values.verbose) {
|
|
5102
|
+
console.log("onProgress", { symbol, stage, processed, total });
|
|
5103
|
+
}
|
|
5104
|
+
},
|
|
4612
5105
|
onIdeas: (symbol, ideasTotal, ideasDirectional) => {
|
|
4613
5106
|
if (values.verbose) {
|
|
4614
5107
|
console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
|
|
@@ -4664,7 +5157,7 @@ const main$4 = async () => {
|
|
|
4664
5157
|
});
|
|
4665
5158
|
const result = await BacktestKit.Simulator.run({
|
|
4666
5159
|
symbol,
|
|
4667
|
-
simulatorName: SIMULATOR_NAME,
|
|
5160
|
+
simulatorName: SIMULATOR_NAME$1,
|
|
4668
5161
|
ideas,
|
|
4669
5162
|
});
|
|
4670
5163
|
const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
|
|
@@ -4679,11 +5172,299 @@ const main$4 = async () => {
|
|
|
4679
5172
|
if (values.markdown) {
|
|
4680
5173
|
const filePath = path.resolve(dumpDir, `${dumpName}.md`);
|
|
4681
5174
|
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
4682
|
-
await fs$1.writeFile(filePath, toMarkdown(result), "utf-8");
|
|
5175
|
+
await fs$1.writeFile(filePath, toMarkdown$1(result), "utf-8");
|
|
4683
5176
|
console.log(`Saved: ${filePath}`);
|
|
4684
5177
|
process.exit(0);
|
|
4685
5178
|
}
|
|
4686
|
-
console.log(toMarkdown(result));
|
|
5179
|
+
console.log(toMarkdown$1(result));
|
|
5180
|
+
process.exit(0);
|
|
5181
|
+
};
|
|
5182
|
+
main$5();
|
|
5183
|
+
|
|
5184
|
+
const SIMULATOR_NAME = "cli_tune";
|
|
5185
|
+
/** Доля времени фида, уходящая в обучение; хвост — один OOS-выстрел. */
|
|
5186
|
+
const DEFAULT_TRAIN_SPLIT = 0.7;
|
|
5187
|
+
const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
|
|
5188
|
+
const validateIdea = (idea, line) => {
|
|
5189
|
+
if (typeof idea !== "object" || idea === null) {
|
|
5190
|
+
return `line ${line}: not an object`;
|
|
5191
|
+
}
|
|
5192
|
+
if (typeof idea.id !== "number") {
|
|
5193
|
+
return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
|
|
5194
|
+
}
|
|
5195
|
+
if (typeof idea.ts !== "number") {
|
|
5196
|
+
return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
|
|
5197
|
+
}
|
|
5198
|
+
if (typeof idea.symbol !== "string" || !idea.symbol) {
|
|
5199
|
+
return `line ${line}: "symbol" must be a non-empty string`;
|
|
5200
|
+
}
|
|
5201
|
+
if (!IDEA_DIRECTIONS.includes(idea.direction)) {
|
|
5202
|
+
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
5203
|
+
}
|
|
5204
|
+
if (typeof idea.author !== "string" || !idea.author) {
|
|
5205
|
+
return `line ${line}: "author" must be a non-empty string`;
|
|
5206
|
+
}
|
|
5207
|
+
return null;
|
|
5208
|
+
};
|
|
5209
|
+
const pointLabel = (point) => `H=${point.hardStopPercent} TT=${point.trailingTakePercent} hold=${point.holdMinutes / 60}h N=${point.minIdeasAligned} ` +
|
|
5210
|
+
`track=${point.minAuthorTrack} rate=${point.minAuthorHitRate} wilson=${point.minAuthorWilson} W=${point.minWeightAligned} ` +
|
|
5211
|
+
`lock=${point.profitLockPercent} metric=${point.authorMetric}`;
|
|
5212
|
+
const fmtRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
|
|
5213
|
+
const toMarkdown = (train, test, trainSplit) => {
|
|
5214
|
+
const lines = [];
|
|
5215
|
+
lines.push(`# Tune Report — ${train.symbol}`);
|
|
5216
|
+
lines.push("");
|
|
5217
|
+
lines.push(`Walk-forward: train on the first ${Math.round(trainSplit * 100)}% of the feed time range, ONE out-of-sample shot of the frozen sharpe winner on the tail.`);
|
|
5218
|
+
lines.push("");
|
|
5219
|
+
lines.push(`## Train (${train.ideasDirectional} directional ideas, ${train.reports.length} grid points)`);
|
|
5220
|
+
lines.push("");
|
|
5221
|
+
lines.push(`| Criterion | Point | Trades | PNL% | WinRate | DD% | Sharpe | Sortino |`);
|
|
5222
|
+
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- |`);
|
|
5223
|
+
for (const best of train.best) {
|
|
5224
|
+
if (!best.report) {
|
|
5225
|
+
lines.push(`| ${best.criterion} | — | — | — | — | — | — | — |`);
|
|
5226
|
+
continue;
|
|
5227
|
+
}
|
|
5228
|
+
lines.push(`| ${best.criterion} | ${pointLabel(best.report.point)} | ${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ` +
|
|
5229
|
+
`${(best.report.winRate * 100).toFixed(0)}% | ${best.report.maxSeriesDrawdownPercent.toFixed(2)} | ${fmtRatio(best.report.sharpe)} | ${fmtRatio(best.report.sortino)} |`);
|
|
5230
|
+
}
|
|
5231
|
+
lines.push("");
|
|
5232
|
+
lines.push(`## Out-of-sample (frozen sharpe winner, ${test.ideasDirectional} tail ideas)`);
|
|
5233
|
+
lines.push("");
|
|
5234
|
+
lines.push(`Frozen point: ${pointLabel(test.point)}`);
|
|
5235
|
+
lines.push("");
|
|
5236
|
+
lines.push(`| Metric | Value |`);
|
|
5237
|
+
lines.push(`| --- | --- |`);
|
|
5238
|
+
lines.push(`| Trades (skipped busy) | ${test.report.trades} (${test.report.skippedBusy}) |`);
|
|
5239
|
+
lines.push(`| PNL | ${test.report.totalPnlPercent.toFixed(2)}% |`);
|
|
5240
|
+
lines.push(`| Win rate | ${(test.report.winRate * 100).toFixed(0)}% |`);
|
|
5241
|
+
lines.push(`| Profit factor | ${fmtRatio(test.report.profitFactor)} |`);
|
|
5242
|
+
lines.push(`| Max series drawdown | ${test.report.maxSeriesDrawdownPercent.toFixed(2)}% |`);
|
|
5243
|
+
lines.push(`| Sharpe / Sortino | ${fmtRatio(test.report.sharpe)} / ${fmtRatio(test.report.sortino)} |`);
|
|
5244
|
+
lines.push(`| Recovery factor | ${fmtRatio(test.report.recoveryFactor)} |`);
|
|
5245
|
+
lines.push(`| Exits | ${Object.entries(test.report.exitReasons).map(([reason, count]) => `${reason}=${count}`).join(", ")} |`);
|
|
5246
|
+
lines.push("");
|
|
5247
|
+
lines.push(`## Test trades`);
|
|
5248
|
+
lines.push("");
|
|
5249
|
+
lines.push(`| Direction | Exit | PNL% | Hold | Entry (UTC) |`);
|
|
5250
|
+
lines.push(`| --- | --- | --- | --- | --- |`);
|
|
5251
|
+
for (const trade of test.trades) {
|
|
5252
|
+
lines.push(`| ${trade.direction} | ${trade.exitReason} | ${trade.pnlPercent.toFixed(2)} | ${trade.holdMinutesActual}m | ${new Date(trade.entryTimestamp).toISOString()} |`);
|
|
5253
|
+
}
|
|
5254
|
+
lines.push("");
|
|
5255
|
+
lines.push(`## Frozen author track record (train range, sharpe winner rule)`);
|
|
5256
|
+
lines.push("");
|
|
5257
|
+
lines.push(`Freeze these raw numbers for production \`Simulator.test\` — banned flags and vote weights are re-derived from them under the frozen point's rule; an author absent from the list is banned by default.`);
|
|
5258
|
+
lines.push("");
|
|
5259
|
+
lines.push(`| Author | Ideas | Hits | Banned |`);
|
|
5260
|
+
lines.push(`| --- | --- | --- | --- |`);
|
|
5261
|
+
for (const stat of test.authorStats) {
|
|
5262
|
+
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${stat.banned ? "yes" : ""} |`);
|
|
5263
|
+
}
|
|
5264
|
+
return lines.join("\n");
|
|
5265
|
+
};
|
|
5266
|
+
const main$4 = async () => {
|
|
5267
|
+
if (!getEntry((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))) {
|
|
5268
|
+
return;
|
|
5269
|
+
}
|
|
5270
|
+
const { values, positionals } = getArgs();
|
|
5271
|
+
if (!values.tune) {
|
|
5272
|
+
return;
|
|
5273
|
+
}
|
|
5274
|
+
const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
|
|
5275
|
+
if (!ideasPath) {
|
|
5276
|
+
console.error("Error: positional path to an ideas .jsonl file is required");
|
|
5277
|
+
process.exit(1);
|
|
5278
|
+
}
|
|
5279
|
+
let ideas = [];
|
|
5280
|
+
{
|
|
5281
|
+
let content;
|
|
5282
|
+
try {
|
|
5283
|
+
content = await fs$1.readFile(path.resolve(ideasPath), "utf-8");
|
|
5284
|
+
}
|
|
5285
|
+
catch (error) {
|
|
5286
|
+
console.error(`Error: cannot read ideas file: ${ideasPath}`);
|
|
5287
|
+
process.exit(1);
|
|
5288
|
+
}
|
|
5289
|
+
const rows = content.split("\n").filter(Boolean);
|
|
5290
|
+
if (!rows.length) {
|
|
5291
|
+
console.error(`Error: ideas file is empty: ${ideasPath}`);
|
|
5292
|
+
process.exit(1);
|
|
5293
|
+
}
|
|
5294
|
+
for (let i = 0; i < rows.length; i++) {
|
|
5295
|
+
let idea;
|
|
5296
|
+
try {
|
|
5297
|
+
idea = JSON.parse(rows[i]);
|
|
5298
|
+
}
|
|
5299
|
+
catch {
|
|
5300
|
+
console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
|
|
5301
|
+
process.exit(1);
|
|
5302
|
+
}
|
|
5303
|
+
const problem = validateIdea(idea, i + 1);
|
|
5304
|
+
if (problem) {
|
|
5305
|
+
console.error(`Error: ideas file does not match the idea structure — ${problem}`);
|
|
5306
|
+
console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
|
|
5307
|
+
process.exit(1);
|
|
5308
|
+
}
|
|
5309
|
+
ideas.push(idea);
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
const trainSplit = values.split
|
|
5313
|
+
? parseFloat(values.split)
|
|
5314
|
+
: DEFAULT_TRAIN_SPLIT;
|
|
5315
|
+
if (!Number.isFinite(trainSplit) || trainSplit <= 0 || trainSplit >= 1) {
|
|
5316
|
+
console.error(`Error: --split must be a fraction between 0 and 1, got ${values.split}`);
|
|
5317
|
+
process.exit(1);
|
|
5318
|
+
}
|
|
5319
|
+
{
|
|
5320
|
+
const cwd = process.cwd();
|
|
5321
|
+
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
5322
|
+
}
|
|
5323
|
+
await cli.configConnectionService.loadConfig("setup.config");
|
|
5324
|
+
{
|
|
5325
|
+
const loader = await cli.configConnectionService.loadConfig("loader.config");
|
|
5326
|
+
try {
|
|
5327
|
+
if (typeof loader === "function") {
|
|
5328
|
+
await loader();
|
|
5329
|
+
}
|
|
5330
|
+
if (typeof loader?.loader === "function") {
|
|
5331
|
+
await loader.loader();
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
5334
|
+
catch (error) {
|
|
5335
|
+
console.error("Module loader failed", error);
|
|
5336
|
+
process.exit(-1);
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5339
|
+
await cli.moduleConnectionService.loadModule("tune.module");
|
|
5340
|
+
{
|
|
5341
|
+
await cli.exchangeSchemaService.addSchema();
|
|
5342
|
+
await cli.symbolSchemaService.addSchema();
|
|
5343
|
+
}
|
|
5344
|
+
const [defaultExchangeName = null] = await BacktestKit.listExchangeSchema();
|
|
5345
|
+
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
5346
|
+
if (values.verbose) {
|
|
5347
|
+
BacktestKit.overrideExchangeSchema({
|
|
5348
|
+
exchangeName,
|
|
5349
|
+
callbacks: {
|
|
5350
|
+
onCandleData(symbol, interval, since) {
|
|
5351
|
+
console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
|
|
5352
|
+
},
|
|
5353
|
+
},
|
|
5354
|
+
});
|
|
5355
|
+
}
|
|
5356
|
+
const symbol = values.symbol || "BTCUSDT";
|
|
5357
|
+
// Подбор параметров: полная сетка с собирающей прибыль механикой —
|
|
5358
|
+
// замок, трейлинг, взвешенный консенсус, Вильсон, обе метрики бана.
|
|
5359
|
+
// Честность обеспечивается сплитом: обучение видит только голову,
|
|
5360
|
+
// хвост тратится на один выстрел замороженного sharpe-победителя
|
|
5361
|
+
BacktestKit.addSimulatorSchema({
|
|
5362
|
+
simulatorName: SIMULATOR_NAME,
|
|
5363
|
+
exchangeName,
|
|
5364
|
+
gridAxes: {
|
|
5365
|
+
// стопы < 2% сидят внутри медианного шейкаута и не выигрывают
|
|
5366
|
+
hardStopPercent: [2, 2.5, 3, 4, 5, 7],
|
|
5367
|
+
trailingTakePercent: [1, 1.5, 2, 3, 4],
|
|
5368
|
+
holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
|
|
5369
|
+
minIdeasAligned: [1, 2],
|
|
5370
|
+
minAuthorTrack: [2, 3, 5],
|
|
5371
|
+
minAuthorHitRate: [0.5, 0.6],
|
|
5372
|
+
minAuthorWilson: [0, 0.6],
|
|
5373
|
+
minWeightAligned: [0, 0.6, 1.2],
|
|
5374
|
+
profitLockPercent: [0, 1.5, 2.5],
|
|
5375
|
+
authorMetric: ["close", "reach"],
|
|
5376
|
+
banCriteria: ["sharpe"],
|
|
5377
|
+
},
|
|
5378
|
+
reportOrder: "sharpe",
|
|
5379
|
+
callbacks: {
|
|
5380
|
+
onProgress: (symbol, stage, processed, total) => {
|
|
5381
|
+
if (values.verbose) {
|
|
5382
|
+
console.log("onProgress", { symbol, stage, processed, total });
|
|
5383
|
+
}
|
|
5384
|
+
},
|
|
5385
|
+
onIdeas: (symbol, ideasTotal, ideasDirectional) => {
|
|
5386
|
+
if (values.verbose) {
|
|
5387
|
+
console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
|
|
5388
|
+
}
|
|
5389
|
+
},
|
|
5390
|
+
onProfiles: (symbol, profiles, truncatedCount) => {
|
|
5391
|
+
if (values.verbose) {
|
|
5392
|
+
console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
|
|
5393
|
+
}
|
|
5394
|
+
},
|
|
5395
|
+
onAuthorsTrained: (symbol, stats, bannedIdeas) => {
|
|
5396
|
+
if (values.verbose) {
|
|
5397
|
+
console.log("onAuthorsTrained", {
|
|
5398
|
+
symbol,
|
|
5399
|
+
authors: stats.length,
|
|
5400
|
+
banned: stats.filter(({ banned }) => banned).length,
|
|
5401
|
+
bannedIdeas,
|
|
5402
|
+
});
|
|
5403
|
+
}
|
|
5404
|
+
},
|
|
5405
|
+
onRanking: (symbol, criterion, _sorted, best) => {
|
|
5406
|
+
if (values.verbose) {
|
|
5407
|
+
console.log("onRanking", {
|
|
5408
|
+
symbol,
|
|
5409
|
+
criterion,
|
|
5410
|
+
point: best.report?.point ?? null,
|
|
5411
|
+
pnl: best.report ? +best.report.totalPnlPercent.toFixed(2) : null,
|
|
5412
|
+
});
|
|
5413
|
+
}
|
|
5414
|
+
},
|
|
5415
|
+
onTestDone: (symbol, result) => {
|
|
5416
|
+
if (values.verbose) {
|
|
5417
|
+
console.log("onTestDone", {
|
|
5418
|
+
symbol,
|
|
5419
|
+
trades: result.report.trades,
|
|
5420
|
+
pnl: +result.report.totalPnlPercent.toFixed(2),
|
|
5421
|
+
});
|
|
5422
|
+
}
|
|
5423
|
+
},
|
|
5424
|
+
},
|
|
5425
|
+
});
|
|
5426
|
+
// обучение видит ТОЛЬКО голову ленты; хвост — один OOS-выстрел
|
|
5427
|
+
const sorted = [...ideas].sort((a, b) => a.ts - b.ts);
|
|
5428
|
+
const cutoff = sorted[0].ts + (sorted[sorted.length - 1].ts - sorted[0].ts) * trainSplit;
|
|
5429
|
+
const trainIdeas = sorted.filter(({ ts }) => ts < cutoff);
|
|
5430
|
+
const testIdeas = sorted.filter(({ ts }) => ts >= cutoff);
|
|
5431
|
+
const train = await BacktestKit.Simulator.run({
|
|
5432
|
+
symbol,
|
|
5433
|
+
simulatorName: SIMULATOR_NAME,
|
|
5434
|
+
ideas: trainIdeas,
|
|
5435
|
+
});
|
|
5436
|
+
const sharpeBest = train.best.find(({ criterion }) => criterion === "sharpe");
|
|
5437
|
+
if (!sharpeBest?.report) {
|
|
5438
|
+
console.error("Error: training produced no sharpe winner — nothing to freeze for the out-of-sample shot");
|
|
5439
|
+
process.exit(1);
|
|
5440
|
+
}
|
|
5441
|
+
// заморозка: точка и трек-рекорд sharpe-победителя как есть — из
|
|
5442
|
+
// замороженных статов test() читает только author/ideas/hits и сам
|
|
5443
|
+
// перевыводит баны и веса под правило точки
|
|
5444
|
+
const test = await BacktestKit.Simulator.test({
|
|
5445
|
+
symbol,
|
|
5446
|
+
simulatorName: SIMULATOR_NAME,
|
|
5447
|
+
ideas: testIdeas,
|
|
5448
|
+
point: sharpeBest.report.point,
|
|
5449
|
+
authorStats: sharpeBest.authorStats,
|
|
5450
|
+
});
|
|
5451
|
+
const dumpName = values.output || `tune_${symbol}_${Date.now()}`;
|
|
5452
|
+
const dumpDir = path.join(process.cwd(), "dump");
|
|
5453
|
+
if (values.json) {
|
|
5454
|
+
const filePath = path.resolve(dumpDir, `${dumpName}.json`);
|
|
5455
|
+
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
5456
|
+
await fs$1.writeFile(filePath, JSON.stringify({ trainSplit, train, test }, null, 2), "utf-8");
|
|
5457
|
+
console.log(`Saved: ${filePath}`);
|
|
5458
|
+
process.exit(0);
|
|
5459
|
+
}
|
|
5460
|
+
if (values.markdown) {
|
|
5461
|
+
const filePath = path.resolve(dumpDir, `${dumpName}.md`);
|
|
5462
|
+
await fs$1.mkdir(dumpDir, { recursive: true });
|
|
5463
|
+
await fs$1.writeFile(filePath, toMarkdown(train, test, trainSplit), "utf-8");
|
|
5464
|
+
console.log(`Saved: ${filePath}`);
|
|
5465
|
+
process.exit(0);
|
|
5466
|
+
}
|
|
5467
|
+
console.log(toMarkdown(train, test, trainSplit));
|
|
4687
5468
|
process.exit(0);
|
|
4688
5469
|
};
|
|
4689
5470
|
main$4();
|
|
@@ -4884,7 +5665,8 @@ Modes:
|
|
|
4884
5665
|
--dump Fetch and save raw OHLCV candles
|
|
4885
5666
|
--pnldebug Simulate PnL per minute for a given entry price and direction
|
|
4886
5667
|
--brokerdebug Fire a single broker commit against the live broker adapter
|
|
4887
|
-
--simulator <ideas.jsonl>
|
|
5668
|
+
--simulator <ideas.jsonl> Feasibility probe over crowd ideas: is there a profitable corridor at all
|
|
5669
|
+
--tune <ideas.jsonl> Walk-forward parameter search: train on the head, one frozen shot on the tail
|
|
4888
5670
|
--flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
|
|
4889
5671
|
--init Scaffold a new project in the current directory
|
|
4890
5672
|
--docker Scaffold a Docker workspace for running strategies in a container
|
|
@@ -5019,19 +5801,47 @@ Simulator flags (--simulator):
|
|
|
5019
5801
|
"author": string }. The file is validated BEFORE any work; a line that does not
|
|
5020
5802
|
match the structure aborts the run with an error.
|
|
5021
5803
|
|
|
5022
|
-
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
|
|
5804
|
+
A FEASIBILITY PROBE, not a parameter search (that is --tune): the profit-harvesting
|
|
5805
|
+
machinery is off (no profit lock, inert trailing, no weighted consensus), and a
|
|
5806
|
+
fast 48-point grid of hard stop x hold x ban rule answers one question — does the
|
|
5807
|
+
feed contain a profitable corridor at all. One 5-day candle pass per idea, flood
|
|
5808
|
+
dedupe (one idea per author per direction per 8h), default-ban author filter,
|
|
5809
|
+
time-based Sharpe/Sortino, four ranking winners. The report carries the corridor
|
|
5810
|
+
share and the whitelist of the sharpe winner's ban rule as evidence.
|
|
5026
5811
|
Ideas of other symbols are filtered out — one shared feed serves any --symbol.
|
|
5027
5812
|
|
|
5028
5813
|
No output flag → print the Markdown summary to stdout. With --verbose every
|
|
5029
|
-
simulator lifecycle callback (onIdeas, onProfiles, onAuthorsTrained,
|
|
5030
|
-
onRanking, onDone) is logged to the console as it fires.
|
|
5814
|
+
simulator lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
|
|
5815
|
+
onGridPoint, onRanking, onDone) is logged to the console as it fires.
|
|
5031
5816
|
|
|
5032
5817
|
Module file ./modules/simulator.module is loaded automatically if it exists
|
|
5033
5818
|
(register your exchange there); without it CCXT Binance is used by default.
|
|
5034
5819
|
|
|
5820
|
+
Tune flags (--tune):
|
|
5821
|
+
|
|
5822
|
+
--symbol <string> Trading pair to tune (default: BTCUSDT)
|
|
5823
|
+
--exchange <string> Exchange name (default: first registered)
|
|
5824
|
+
--split <string> Train share of the feed time range, 0..1 (default: 0.7)
|
|
5825
|
+
--output <string> Output file base name (default: tune_{SYMBOL}_{TIMESTAMP})
|
|
5826
|
+
--json Save { trainSplit, train, test } to ./dump/<output>.json
|
|
5827
|
+
--markdown Save walk-forward report to ./dump/<output>.md
|
|
5828
|
+
--verbose Log simulator lifecycle callbacks to the console
|
|
5829
|
+
|
|
5830
|
+
Positional: path to an ideas .jsonl file — same shape and validation as --simulator.
|
|
5831
|
+
|
|
5832
|
+
The PARAMETER SEARCH counterpart of the --simulator probe: the full grid with the
|
|
5833
|
+
profit-harvesting machinery on — profit lock, trailing take, weighted consensus,
|
|
5834
|
+
Wilson-bound ban, both author-hit metrics (close/reach). Honesty is structural:
|
|
5835
|
+
training sees ONLY the head of the feed (--split of its time range), then the
|
|
5836
|
+
sharpe winner is frozen — point and raw author track record — and fired exactly
|
|
5837
|
+
once on the tail via Simulator.test. Nothing is trained on the tail; authors
|
|
5838
|
+
unseen in training are banned by default. The report carries the train winners,
|
|
5839
|
+
the out-of-sample result with the trade list, and the frozen author track record
|
|
5840
|
+
to hardcode for production Simulator.test.
|
|
5841
|
+
|
|
5842
|
+
Module file ./modules/tune.module is loaded automatically if it exists
|
|
5843
|
+
(register your exchange there); without it CCXT Binance is used by default.
|
|
5844
|
+
|
|
5035
5845
|
Flush flags (--flush):
|
|
5036
5846
|
|
|
5037
5847
|
One or more positional entry points. For each entry point the following
|
|
@@ -5065,7 +5875,8 @@ Module hooks (loaded automatically by each mode):
|
|
|
5065
5875
|
modules/dump.module --dump Exchange schema for candle dumps
|
|
5066
5876
|
modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
|
|
5067
5877
|
modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
|
|
5068
|
-
modules/simulator.module --simulator Exchange schema for crowd-ideas
|
|
5878
|
+
modules/simulator.module --simulator Exchange schema for the crowd-ideas feasibility probe
|
|
5879
|
+
modules/tune.module --tune Exchange schema for the walk-forward parameter search
|
|
5069
5880
|
|
|
5070
5881
|
--flush has no associated module. It only removes dump subdirectories.
|
|
5071
5882
|
|
|
@@ -5094,8 +5905,10 @@ Examples:
|
|
|
5094
5905
|
node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
|
|
5095
5906
|
node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
|
|
5096
5907
|
node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
|
|
5097
|
-
node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/
|
|
5098
|
-
node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output
|
|
5908
|
+
node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
|
|
5909
|
+
node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_probe ./assets/tv-ideas.normalized.jsonl
|
|
5910
|
+
node ${ENTRY_PATH} --tune --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
|
|
5911
|
+
node ${ENTRY_PATH} --tune --symbol BTCUSDT --split 0.7 --markdown --output jun_2026_tune ./assets/tv-ideas.normalized.jsonl
|
|
5099
5912
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
|
|
5100
5913
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
|
|
5101
5914
|
node ${ENTRY_PATH} --init --output my-trading-bot
|
|
@@ -5109,7 +5922,7 @@ const main$1 = async () => {
|
|
|
5109
5922
|
if (!values.help) {
|
|
5110
5923
|
return;
|
|
5111
5924
|
}
|
|
5112
|
-
process.stdout.write(`@backtest-kit/cli ${"16.
|
|
5925
|
+
process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n\n`);
|
|
5113
5926
|
process.stdout.write(HELP_TEXT);
|
|
5114
5927
|
process.exit(0);
|
|
5115
5928
|
};
|
|
@@ -5123,7 +5936,7 @@ const main = async () => {
|
|
|
5123
5936
|
if (!values.version) {
|
|
5124
5937
|
return;
|
|
5125
5938
|
}
|
|
5126
|
-
process.stdout.write(`@backtest-kit/cli ${"16.
|
|
5939
|
+
process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
|
|
5127
5940
|
process.exit(0);
|
|
5128
5941
|
};
|
|
5129
5942
|
main();
|