@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.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,
|
|
4
|
-
import { singleshot, sleep, getErrorMessage, errorData, BehaviorSubject, Subject, str, compose, createAwaiter, execpool, queued, randomString, TIMEOUT_SYMBOL, typo, retry, trycatch, 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,14 @@ const getArgs = singleshot(() => {
|
|
|
870
870
|
type: "boolean",
|
|
871
871
|
default: false,
|
|
872
872
|
},
|
|
873
|
+
tune: {
|
|
874
|
+
type: "boolean",
|
|
875
|
+
default: false,
|
|
876
|
+
},
|
|
877
|
+
split: {
|
|
878
|
+
type: "string",
|
|
879
|
+
default: "",
|
|
880
|
+
},
|
|
873
881
|
commit: {
|
|
874
882
|
type: "string",
|
|
875
883
|
default: "",
|
|
@@ -2316,6 +2324,13 @@ const STOP_BOT_FN = singleshot(async () => {
|
|
|
2316
2324
|
const { stopBot } = await getTelegram();
|
|
2317
2325
|
stopBot();
|
|
2318
2326
|
});
|
|
2327
|
+
/**
|
|
2328
|
+
* Throttle window for the per-tick order_continue decision channel: at most one
|
|
2329
|
+
* Telegram notification per signalId per 15 minutes via functools-kit ttl. The
|
|
2330
|
+
* terminal order_stop is NOT throttled (fires once per signal) and clears the
|
|
2331
|
+
* throttle slot for its signalId.
|
|
2332
|
+
*/
|
|
2333
|
+
const ORDER_CHECK_NOTIFY_TTL = 15 * 60 * 1000;
|
|
2319
2334
|
class TelegramLogicService {
|
|
2320
2335
|
constructor() {
|
|
2321
2336
|
this.loggerService = inject(TYPES.loggerService);
|
|
@@ -2464,11 +2479,68 @@ class TelegramLogicService {
|
|
|
2464
2479
|
markdown,
|
|
2465
2480
|
});
|
|
2466
2481
|
};
|
|
2467
|
-
this.
|
|
2468
|
-
this.loggerService.log("telegramLogicService
|
|
2482
|
+
this.notifyOrderFillOpen = trycatch(async (event) => {
|
|
2483
|
+
this.loggerService.log("telegramLogicService notifyOrderFillOpen", {
|
|
2484
|
+
event,
|
|
2485
|
+
});
|
|
2486
|
+
const markdown = await this.telegramTemplateService.getOrderFillOpenMarkdown(event);
|
|
2487
|
+
if (!markdown) {
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
await this.telegramWebService.publishNotify({
|
|
2491
|
+
symbol: event.symbol,
|
|
2492
|
+
markdown,
|
|
2493
|
+
});
|
|
2494
|
+
});
|
|
2495
|
+
this.notifyOrderFillClose = trycatch(async (event) => {
|
|
2496
|
+
this.loggerService.log("telegramLogicService notifyOrderFillClose", {
|
|
2497
|
+
event,
|
|
2498
|
+
});
|
|
2499
|
+
const markdown = await this.telegramTemplateService.getOrderFillCloseMarkdown(event);
|
|
2500
|
+
if (!markdown) {
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
await this.telegramWebService.publishNotify({
|
|
2504
|
+
symbol: event.symbol,
|
|
2505
|
+
markdown,
|
|
2506
|
+
});
|
|
2507
|
+
});
|
|
2508
|
+
this.notifyOrderRejectOpen = trycatch(async (event) => {
|
|
2509
|
+
this.loggerService.log("telegramLogicService notifyOrderRejectOpen", {
|
|
2510
|
+
event,
|
|
2511
|
+
});
|
|
2512
|
+
const markdown = await this.telegramTemplateService.getOrderRejectOpenMarkdown(event);
|
|
2513
|
+
if (!markdown) {
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
await this.telegramWebService.publishNotify({
|
|
2517
|
+
symbol: event.symbol,
|
|
2518
|
+
markdown,
|
|
2519
|
+
});
|
|
2520
|
+
});
|
|
2521
|
+
this.notifyOrderRejectClose = trycatch(async (event) => {
|
|
2522
|
+
this.loggerService.log("telegramLogicService notifyOrderRejectClose", {
|
|
2523
|
+
event,
|
|
2524
|
+
});
|
|
2525
|
+
const markdown = await this.telegramTemplateService.getOrderRejectCloseMarkdown(event);
|
|
2526
|
+
if (!markdown) {
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
await this.telegramWebService.publishNotify({
|
|
2530
|
+
symbol: event.symbol,
|
|
2531
|
+
markdown,
|
|
2532
|
+
});
|
|
2533
|
+
});
|
|
2534
|
+
/**
|
|
2535
|
+
* Post-verdict order-check CONTINUE (order_continue). Fires every live tick the
|
|
2536
|
+
* order survives the check, so it is throttled via functools-kit ttl:
|
|
2537
|
+
* one message per signalId per ORDER_CHECK_NOTIFY_TTL (15 minutes).
|
|
2538
|
+
*/
|
|
2539
|
+
this.notifyOrderContinue = ttl(trycatch(async (event) => {
|
|
2540
|
+
this.loggerService.log("telegramLogicService notifyOrderContinue", {
|
|
2469
2541
|
event,
|
|
2470
2542
|
});
|
|
2471
|
-
const markdown = await this.telegramTemplateService.
|
|
2543
|
+
const markdown = await this.telegramTemplateService.getOrderContinueMarkdown(event);
|
|
2472
2544
|
if (!markdown) {
|
|
2473
2545
|
return;
|
|
2474
2546
|
}
|
|
@@ -2476,12 +2548,21 @@ class TelegramLogicService {
|
|
|
2476
2548
|
symbol: event.symbol,
|
|
2477
2549
|
markdown,
|
|
2478
2550
|
});
|
|
2551
|
+
}), {
|
|
2552
|
+
key: ([event]) => event.signalId,
|
|
2553
|
+
timeout: ORDER_CHECK_NOTIFY_TTL,
|
|
2479
2554
|
});
|
|
2480
|
-
|
|
2481
|
-
|
|
2555
|
+
/**
|
|
2556
|
+
* Post-verdict order-check STOP (order_stop). Terminal — fires once per signal,
|
|
2557
|
+
* so no throttle; drops the continue throttle slot for this signalId
|
|
2558
|
+
* (the signal leaves monitoring, the ttl entry would only leak otherwise).
|
|
2559
|
+
*/
|
|
2560
|
+
this.notifyOrderStop = trycatch(async (event) => {
|
|
2561
|
+
this.loggerService.log("telegramLogicService notifyOrderStop", {
|
|
2482
2562
|
event,
|
|
2483
2563
|
});
|
|
2484
|
-
|
|
2564
|
+
this.notifyOrderContinue.clear(event.signalId);
|
|
2565
|
+
const markdown = await this.telegramTemplateService.getOrderStopMarkdown(event);
|
|
2485
2566
|
if (!markdown) {
|
|
2486
2567
|
return;
|
|
2487
2568
|
}
|
|
@@ -2586,26 +2667,54 @@ class TelegramLogicService {
|
|
|
2586
2667
|
return;
|
|
2587
2668
|
}
|
|
2588
2669
|
});
|
|
2589
|
-
|
|
2670
|
+
// Broker-CONFIRMED fills only (post-verdict orderFillSubject): a rejected or
|
|
2671
|
+
// transient gate attempt never reaches this channel, so "Order Filled" is
|
|
2672
|
+
// guaranteed truthful. The pre-verdict syncSubject is NOT consumed here.
|
|
2673
|
+
const unOrderFill = listenOrderFill(async (event) => {
|
|
2590
2674
|
if (event.action === "signal-open") {
|
|
2591
|
-
// type "
|
|
2592
|
-
//
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2675
|
+
// Both open outcomes notify: type "active" — the order opened at once
|
|
2676
|
+
// (immediate fill, no schedule phase), type "schedule" — the resting
|
|
2677
|
+
// order was placed on the exchange. Either way the broker confirmed a
|
|
2678
|
+
// real order; the template's Order field carries the type.
|
|
2679
|
+
await this.notifyOrderFillOpen(event);
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
if (event.action === "signal-close") {
|
|
2683
|
+
await this.notifyOrderFillClose(event);
|
|
2684
|
+
return;
|
|
2685
|
+
}
|
|
2686
|
+
});
|
|
2687
|
+
// Terminal broker rejection (post-verdict orderRejectSubject): exactly one
|
|
2688
|
+
// event per dropped order attempt — the engine consumes the rejected signal
|
|
2689
|
+
// id, so this cannot repeat per-tick for the same signal. Dispatched by
|
|
2690
|
+
// action to the dedicated open/close notifications (single responsibility).
|
|
2691
|
+
const unOrderReject = listenOrderReject(async (event) => {
|
|
2692
|
+
if (event.action === "signal-open") {
|
|
2693
|
+
await this.notifyOrderRejectOpen(event);
|
|
2597
2694
|
return;
|
|
2598
2695
|
}
|
|
2599
2696
|
if (event.action === "signal-close") {
|
|
2600
|
-
await this.
|
|
2697
|
+
await this.notifyOrderRejectClose(event);
|
|
2601
2698
|
return;
|
|
2602
2699
|
}
|
|
2603
2700
|
});
|
|
2701
|
+
// Post-verdict check decisions: CONTINUE fires every surviving tick and is
|
|
2702
|
+
// throttled to one message per signalId per 15 minutes via ttl; terminal
|
|
2703
|
+
// STOP fires once per signal, unthrottled.
|
|
2704
|
+
const unOrderContinue = listenOrderContinue(async (event) => {
|
|
2705
|
+
await this.notifyOrderContinue(event);
|
|
2706
|
+
});
|
|
2707
|
+
const unOrderStop = listenOrderStop(async (event) => {
|
|
2708
|
+
await this.notifyOrderStop(event);
|
|
2709
|
+
});
|
|
2604
2710
|
const unSignalNotify = listenSignalNotify(async (event) => {
|
|
2605
2711
|
await this.notifySignalInfo(event);
|
|
2606
2712
|
});
|
|
2713
|
+
const unGc = Source.fromInterval(90000).connect(() => {
|
|
2714
|
+
this.notifyOrderContinue.gc();
|
|
2715
|
+
});
|
|
2607
2716
|
const unConnect = () => this.connect.clear();
|
|
2608
|
-
const unListen = compose(() => unRisk(), () => unSignal(), () => unCommit(), () =>
|
|
2717
|
+
const unListen = compose(() => unRisk(), () => unSignal(), () => unCommit(), () => unOrderFill(), () => unOrderReject(), () => unOrderContinue(), () => unOrderStop(), () => this.notifyOrderContinue.clear(), () => unGc(), () => unSignalNotify(), () => unConnect());
|
|
2609
2718
|
return () => {
|
|
2610
2719
|
STOP_BOT_FN();
|
|
2611
2720
|
unListen();
|
|
@@ -2758,25 +2867,65 @@ class TelegramTemplateService {
|
|
|
2758
2867
|
}
|
|
2759
2868
|
return await RENDER_TEMPLATE_FN("average-buy.mustache", event, this);
|
|
2760
2869
|
};
|
|
2761
|
-
this.
|
|
2762
|
-
this.loggerService.log("telegramTemplateService
|
|
2870
|
+
this.getOrderFillOpenMarkdown = async (event) => {
|
|
2871
|
+
this.loggerService.log("telegramTemplateService getOrderFillOpenMarkdown", {
|
|
2872
|
+
event,
|
|
2873
|
+
});
|
|
2874
|
+
const adapter = await this.getTelegramAdapter();
|
|
2875
|
+
if (adapter?.getOrderFillOpenMarkdown) {
|
|
2876
|
+
return await adapter.getOrderFillOpenMarkdown(event);
|
|
2877
|
+
}
|
|
2878
|
+
return await RENDER_TEMPLATE_FN("order-fill-open.mustache", event, this);
|
|
2879
|
+
};
|
|
2880
|
+
this.getOrderFillCloseMarkdown = async (event) => {
|
|
2881
|
+
this.loggerService.log("telegramTemplateService getOrderFillCloseMarkdown", {
|
|
2882
|
+
event,
|
|
2883
|
+
});
|
|
2884
|
+
const adapter = await this.getTelegramAdapter();
|
|
2885
|
+
if (adapter?.getOrderFillCloseMarkdown) {
|
|
2886
|
+
return await adapter.getOrderFillCloseMarkdown(event);
|
|
2887
|
+
}
|
|
2888
|
+
return await RENDER_TEMPLATE_FN("order-fill-close.mustache", event, this);
|
|
2889
|
+
};
|
|
2890
|
+
this.getOrderRejectOpenMarkdown = async (event) => {
|
|
2891
|
+
this.loggerService.log("telegramTemplateService getOrderRejectOpenMarkdown", {
|
|
2892
|
+
event,
|
|
2893
|
+
});
|
|
2894
|
+
const adapter = await this.getTelegramAdapter();
|
|
2895
|
+
if (adapter?.getOrderRejectOpenMarkdown) {
|
|
2896
|
+
return await adapter.getOrderRejectOpenMarkdown(event);
|
|
2897
|
+
}
|
|
2898
|
+
return await RENDER_TEMPLATE_FN("order-reject-open.mustache", event, this);
|
|
2899
|
+
};
|
|
2900
|
+
this.getOrderRejectCloseMarkdown = async (event) => {
|
|
2901
|
+
this.loggerService.log("telegramTemplateService getOrderRejectCloseMarkdown", {
|
|
2902
|
+
event,
|
|
2903
|
+
});
|
|
2904
|
+
const adapter = await this.getTelegramAdapter();
|
|
2905
|
+
if (adapter?.getOrderRejectCloseMarkdown) {
|
|
2906
|
+
return await adapter.getOrderRejectCloseMarkdown(event);
|
|
2907
|
+
}
|
|
2908
|
+
return await RENDER_TEMPLATE_FN("order-reject-close.mustache", event, this);
|
|
2909
|
+
};
|
|
2910
|
+
this.getOrderContinueMarkdown = async (event) => {
|
|
2911
|
+
this.loggerService.log("telegramTemplateService getOrderContinueMarkdown", {
|
|
2763
2912
|
event,
|
|
2764
2913
|
});
|
|
2765
2914
|
const adapter = await this.getTelegramAdapter();
|
|
2766
|
-
if (adapter?.
|
|
2767
|
-
return await adapter.
|
|
2915
|
+
if (adapter?.getOrderContinueMarkdown) {
|
|
2916
|
+
return await adapter.getOrderContinueMarkdown(event);
|
|
2768
2917
|
}
|
|
2769
|
-
return await RENDER_TEMPLATE_FN("
|
|
2918
|
+
return await RENDER_TEMPLATE_FN("order-continue.mustache", event, this);
|
|
2770
2919
|
};
|
|
2771
|
-
this.
|
|
2772
|
-
this.loggerService.log("telegramTemplateService
|
|
2920
|
+
this.getOrderStopMarkdown = async (event) => {
|
|
2921
|
+
this.loggerService.log("telegramTemplateService getOrderStopMarkdown", {
|
|
2773
2922
|
event,
|
|
2774
2923
|
});
|
|
2775
2924
|
const adapter = await this.getTelegramAdapter();
|
|
2776
|
-
if (adapter?.
|
|
2777
|
-
return await adapter.
|
|
2925
|
+
if (adapter?.getOrderStopMarkdown) {
|
|
2926
|
+
return await adapter.getOrderStopMarkdown(event);
|
|
2778
2927
|
}
|
|
2779
|
-
return await RENDER_TEMPLATE_FN("
|
|
2928
|
+
return await RENDER_TEMPLATE_FN("order-stop.mustache", event, this);
|
|
2780
2929
|
};
|
|
2781
2930
|
this.getCancelScheduledMarkdown = async (event) => {
|
|
2782
2931
|
this.loggerService.log("telegramTemplateService getCancelScheduledMarkdown", {
|
|
@@ -3506,14 +3655,14 @@ const cli = {
|
|
|
3506
3655
|
};
|
|
3507
3656
|
init();
|
|
3508
3657
|
|
|
3509
|
-
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "flush", "init", "docker", "help", "version"];
|
|
3658
|
+
const MODES = ["backtest", "walker", "paper", "live", "main", "pine", "editor", "dump", "pnldebug", "brokerdebug", "simulator", "tune", "flush", "init", "docker", "help", "version"];
|
|
3510
3659
|
const ENTRY_PATH$1 = "./node_modules/@backtest-kit/cli/build/index.mjs";
|
|
3511
3660
|
const HELP_TEXT$1 = `
|
|
3512
3661
|
Example:
|
|
3513
3662
|
|
|
3514
3663
|
node ${ENTRY_PATH$1} --help
|
|
3515
3664
|
`.trimStart();
|
|
3516
|
-
const main$
|
|
3665
|
+
const main$k = async () => {
|
|
3517
3666
|
if (!getEntry(import.meta.url)) {
|
|
3518
3667
|
return;
|
|
3519
3668
|
}
|
|
@@ -3521,14 +3670,14 @@ const main$i = async () => {
|
|
|
3521
3670
|
if (MODES.some((mode) => values[mode])) {
|
|
3522
3671
|
return;
|
|
3523
3672
|
}
|
|
3524
|
-
process.stdout.write(`@backtest-kit/cli ${"16.
|
|
3673
|
+
process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
|
|
3525
3674
|
process.stdout.write("\n");
|
|
3526
3675
|
process.stdout.write(`Run with --help to see available commands.\n`);
|
|
3527
3676
|
process.stdout.write("\n");
|
|
3528
3677
|
process.stdout.write(HELP_TEXT$1);
|
|
3529
3678
|
process.exit(0);
|
|
3530
3679
|
};
|
|
3531
|
-
main$
|
|
3680
|
+
main$k();
|
|
3532
3681
|
|
|
3533
3682
|
const notifyShutdown = singleshot(async () => {
|
|
3534
3683
|
console.log("Graceful shutdown initiated.");
|
|
@@ -3544,7 +3693,7 @@ const flush = async (entryPoint) => {
|
|
|
3544
3693
|
console.log(`Removed: ${target}`);
|
|
3545
3694
|
}
|
|
3546
3695
|
};
|
|
3547
|
-
const main$
|
|
3696
|
+
const main$j = async () => {
|
|
3548
3697
|
if (!getEntry(import.meta.url)) {
|
|
3549
3698
|
return;
|
|
3550
3699
|
}
|
|
@@ -3561,7 +3710,7 @@ const main$h = async () => {
|
|
|
3561
3710
|
}
|
|
3562
3711
|
process.exit(0);
|
|
3563
3712
|
};
|
|
3564
|
-
main$
|
|
3713
|
+
main$j();
|
|
3565
3714
|
|
|
3566
3715
|
const BEFORE_EXIT_FN$6 = singleshot(async () => {
|
|
3567
3716
|
process.off("SIGINT", BEFORE_EXIT_FN$6);
|
|
@@ -3584,7 +3733,7 @@ const BEFORE_EXIT_FN$6 = singleshot(async () => {
|
|
|
3584
3733
|
const listenGracefulShutdown$6 = singleshot(() => {
|
|
3585
3734
|
process.on("SIGINT", BEFORE_EXIT_FN$6);
|
|
3586
3735
|
});
|
|
3587
|
-
const main$
|
|
3736
|
+
const main$i = async () => {
|
|
3588
3737
|
if (!getEntry(import.meta.url)) {
|
|
3589
3738
|
return;
|
|
3590
3739
|
}
|
|
@@ -3602,7 +3751,7 @@ const main$g = async () => {
|
|
|
3602
3751
|
await cli.backtestMainService.connect();
|
|
3603
3752
|
listenGracefulShutdown$6();
|
|
3604
3753
|
};
|
|
3605
|
-
main$
|
|
3754
|
+
main$i();
|
|
3606
3755
|
|
|
3607
3756
|
const BEFORE_EXIT_FN$5 = singleshot(async () => {
|
|
3608
3757
|
process.off("SIGINT", BEFORE_EXIT_FN$5);
|
|
@@ -3621,7 +3770,7 @@ const BEFORE_EXIT_FN$5 = singleshot(async () => {
|
|
|
3621
3770
|
const listenGracefulShutdown$5 = singleshot(() => {
|
|
3622
3771
|
process.on("SIGINT", BEFORE_EXIT_FN$5);
|
|
3623
3772
|
});
|
|
3624
|
-
const main$
|
|
3773
|
+
const main$h = async () => {
|
|
3625
3774
|
if (!getEntry(import.meta.url)) {
|
|
3626
3775
|
return;
|
|
3627
3776
|
}
|
|
@@ -3640,7 +3789,7 @@ const main$f = async () => {
|
|
|
3640
3789
|
listenGracefulShutdown$5();
|
|
3641
3790
|
await cli.walkerMainService.connect();
|
|
3642
3791
|
};
|
|
3643
|
-
main$
|
|
3792
|
+
main$h();
|
|
3644
3793
|
|
|
3645
3794
|
const BEFORE_EXIT_FN$4 = singleshot(async () => {
|
|
3646
3795
|
process.off("SIGINT", BEFORE_EXIT_FN$4);
|
|
@@ -3662,7 +3811,7 @@ const BEFORE_EXIT_FN$4 = singleshot(async () => {
|
|
|
3662
3811
|
const listenGracefulShutdown$4 = singleshot(() => {
|
|
3663
3812
|
process.on("SIGINT", BEFORE_EXIT_FN$4);
|
|
3664
3813
|
});
|
|
3665
|
-
const main$
|
|
3814
|
+
const main$g = async () => {
|
|
3666
3815
|
if (!getEntry(import.meta.url)) {
|
|
3667
3816
|
return;
|
|
3668
3817
|
}
|
|
@@ -3676,7 +3825,7 @@ const main$e = async () => {
|
|
|
3676
3825
|
await cli.paperMainService.connect();
|
|
3677
3826
|
listenGracefulShutdown$4();
|
|
3678
3827
|
};
|
|
3679
|
-
main$
|
|
3828
|
+
main$g();
|
|
3680
3829
|
|
|
3681
3830
|
const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
3682
3831
|
process.off("SIGINT", BEFORE_EXIT_FN$3);
|
|
@@ -3698,7 +3847,7 @@ const BEFORE_EXIT_FN$3 = singleshot(async () => {
|
|
|
3698
3847
|
const listenGracefulShutdown$3 = singleshot(() => {
|
|
3699
3848
|
process.on("SIGINT", BEFORE_EXIT_FN$3);
|
|
3700
3849
|
});
|
|
3701
|
-
const main$
|
|
3850
|
+
const main$f = async () => {
|
|
3702
3851
|
if (!getEntry(import.meta.url)) {
|
|
3703
3852
|
return;
|
|
3704
3853
|
}
|
|
@@ -3712,7 +3861,7 @@ const main$d = async () => {
|
|
|
3712
3861
|
await cli.liveMainService.connect();
|
|
3713
3862
|
listenGracefulShutdown$3();
|
|
3714
3863
|
};
|
|
3715
|
-
main$
|
|
3864
|
+
main$f();
|
|
3716
3865
|
|
|
3717
3866
|
const stopBacktestList$1 = async () => {
|
|
3718
3867
|
for (const item of await Backtest.list()) {
|
|
@@ -3776,7 +3925,7 @@ const BEFORE_EXIT_FN$2 = singleshot(async () => {
|
|
|
3776
3925
|
const listenGracefulShutdown$2 = singleshot(() => {
|
|
3777
3926
|
process.on("SIGINT", BEFORE_EXIT_FN$2);
|
|
3778
3927
|
});
|
|
3779
|
-
const main$
|
|
3928
|
+
const main$e = async () => {
|
|
3780
3929
|
if (!getEntry(import.meta.url)) {
|
|
3781
3930
|
return;
|
|
3782
3931
|
}
|
|
@@ -3832,7 +3981,7 @@ const main$c = async () => {
|
|
|
3832
3981
|
listenGracefulShutdown$2();
|
|
3833
3982
|
await cli.resolveService.attachEntry(path.resolve(cwd, entryPoint));
|
|
3834
3983
|
};
|
|
3835
|
-
main$
|
|
3984
|
+
main$e();
|
|
3836
3985
|
|
|
3837
3986
|
const MODE_MODULE = {
|
|
3838
3987
|
backtest: "backtest.module",
|
|
@@ -3935,7 +4084,7 @@ const createGracefulShutdown = (mode) => {
|
|
|
3935
4084
|
process.on("SIGINT", handler);
|
|
3936
4085
|
});
|
|
3937
4086
|
};
|
|
3938
|
-
const main$
|
|
4087
|
+
const main$d = async () => {
|
|
3939
4088
|
if (!getEntry(import.meta.url)) {
|
|
3940
4089
|
return;
|
|
3941
4090
|
}
|
|
@@ -4011,7 +4160,7 @@ const main$b = async () => {
|
|
|
4011
4160
|
}
|
|
4012
4161
|
await getEntrySubject().next(absolutePath);
|
|
4013
4162
|
};
|
|
4014
|
-
main$
|
|
4163
|
+
main$d();
|
|
4015
4164
|
|
|
4016
4165
|
const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
4017
4166
|
process.off("SIGINT", BEFORE_EXIT_FN$1);
|
|
@@ -4022,7 +4171,7 @@ const BEFORE_EXIT_FN$1 = singleshot(async () => {
|
|
|
4022
4171
|
const listenGracefulShutdown$1 = singleshot(() => {
|
|
4023
4172
|
process.on("SIGINT", BEFORE_EXIT_FN$1);
|
|
4024
4173
|
});
|
|
4025
|
-
const main$
|
|
4174
|
+
const main$c = async () => {
|
|
4026
4175
|
if (!getEntry(import.meta.url)) {
|
|
4027
4176
|
return;
|
|
4028
4177
|
}
|
|
@@ -4032,7 +4181,7 @@ const main$a = async () => {
|
|
|
4032
4181
|
}
|
|
4033
4182
|
listenGracefulShutdown$1();
|
|
4034
4183
|
};
|
|
4035
|
-
main$
|
|
4184
|
+
main$c();
|
|
4036
4185
|
|
|
4037
4186
|
const BEFORE_EXIT_FN = singleshot(async () => {
|
|
4038
4187
|
process.off("SIGINT", BEFORE_EXIT_FN);
|
|
@@ -4043,7 +4192,7 @@ const BEFORE_EXIT_FN = singleshot(async () => {
|
|
|
4043
4192
|
const listenGracefulShutdown = singleshot(() => {
|
|
4044
4193
|
process.on("SIGINT", BEFORE_EXIT_FN);
|
|
4045
4194
|
});
|
|
4046
|
-
const main$
|
|
4195
|
+
const main$b = async () => {
|
|
4047
4196
|
if (!getEntry(import.meta.url)) {
|
|
4048
4197
|
return;
|
|
4049
4198
|
}
|
|
@@ -4053,7 +4202,7 @@ const main$9 = async () => {
|
|
|
4053
4202
|
}
|
|
4054
4203
|
listenGracefulShutdown();
|
|
4055
4204
|
};
|
|
4056
|
-
main$
|
|
4205
|
+
main$b();
|
|
4057
4206
|
|
|
4058
4207
|
const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
4059
4208
|
const keys = Object.keys(schema);
|
|
@@ -4075,7 +4224,7 @@ const EXTRACT_ROWS_FN = (plots, schema) => {
|
|
|
4075
4224
|
}
|
|
4076
4225
|
return rows;
|
|
4077
4226
|
};
|
|
4078
|
-
const main$
|
|
4227
|
+
const main$a = async () => {
|
|
4079
4228
|
if (!getEntry(import.meta.url)) {
|
|
4080
4229
|
return;
|
|
4081
4230
|
}
|
|
@@ -4167,17 +4316,17 @@ const main$8 = async () => {
|
|
|
4167
4316
|
if (values.markdown) {
|
|
4168
4317
|
const filePath = resolve(dumpDir, `${dumpName}.md`);
|
|
4169
4318
|
await mkdir(dumpDir, { recursive: true });
|
|
4170
|
-
await writeFile(filePath, await toMarkdown$
|
|
4319
|
+
await writeFile(filePath, await toMarkdown$2(signalId, plots, signalSchema), "utf-8");
|
|
4171
4320
|
console.log(`Saved: ${filePath}`);
|
|
4172
4321
|
process.exit(0);
|
|
4173
4322
|
return;
|
|
4174
4323
|
}
|
|
4175
|
-
console.log(await toMarkdown$
|
|
4324
|
+
console.log(await toMarkdown$2(signalId, plots, signalSchema));
|
|
4176
4325
|
process.exit(0);
|
|
4177
4326
|
};
|
|
4178
|
-
main$
|
|
4327
|
+
main$a();
|
|
4179
4328
|
|
|
4180
|
-
const main$
|
|
4329
|
+
const main$9 = async () => {
|
|
4181
4330
|
if (!getEntry(import.meta.url)) {
|
|
4182
4331
|
return;
|
|
4183
4332
|
}
|
|
@@ -4240,9 +4389,9 @@ const main$7 = async () => {
|
|
|
4240
4389
|
};
|
|
4241
4390
|
process.on("SIGINT", beforeExit);
|
|
4242
4391
|
};
|
|
4243
|
-
main$
|
|
4392
|
+
main$9();
|
|
4244
4393
|
|
|
4245
|
-
const main$
|
|
4394
|
+
const main$8 = async () => {
|
|
4246
4395
|
if (!getEntry(import.meta.url)) {
|
|
4247
4396
|
return;
|
|
4248
4397
|
}
|
|
@@ -4323,9 +4472,9 @@ const main$6 = async () => {
|
|
|
4323
4472
|
console.log(JSON.stringify(candles, null, 2));
|
|
4324
4473
|
process.exit(0);
|
|
4325
4474
|
};
|
|
4326
|
-
main$
|
|
4475
|
+
main$8();
|
|
4327
4476
|
|
|
4328
|
-
const main$
|
|
4477
|
+
const main$7 = async () => {
|
|
4329
4478
|
if (!getEntry(import.meta.url)) {
|
|
4330
4479
|
return;
|
|
4331
4480
|
}
|
|
@@ -4439,11 +4588,314 @@ const main$5 = async () => {
|
|
|
4439
4588
|
}
|
|
4440
4589
|
process.exit(0);
|
|
4441
4590
|
};
|
|
4442
|
-
main$
|
|
4591
|
+
main$7();
|
|
4443
4592
|
|
|
4444
|
-
const
|
|
4445
|
-
|
|
4446
|
-
|
|
4593
|
+
const getBroker = singleshot(() => {
|
|
4594
|
+
// Reaches into BrokerAdapter internals on purpose: the public commit*
|
|
4595
|
+
// methods skip when payload.backtest=true and require enable(), while
|
|
4596
|
+
// brokerdebug must fire exactly one commit at the raw adapter proxy.
|
|
4597
|
+
const broker = Broker["getInstance"]();
|
|
4598
|
+
if (!broker) {
|
|
4599
|
+
throw new Error("Broker instance is not initialized.");
|
|
4600
|
+
}
|
|
4601
|
+
return broker;
|
|
4602
|
+
});
|
|
4603
|
+
/** Called when a new signal is opened (position entry confirmed). */
|
|
4604
|
+
const commitOrderOpen = trycatch(async (payload) => {
|
|
4605
|
+
const broker = getBroker();
|
|
4606
|
+
return await broker.onOrderOpenCommit(payload);
|
|
4607
|
+
}, {
|
|
4608
|
+
fallback: (error) => {
|
|
4609
|
+
console.log(error);
|
|
4610
|
+
process.exit(-1);
|
|
4611
|
+
},
|
|
4612
|
+
});
|
|
4613
|
+
/** Called when a new signal is closed (take-profit, stop-loss, or manual close). */
|
|
4614
|
+
const commitOrderClose = trycatch(async (payload) => {
|
|
4615
|
+
const broker = getBroker();
|
|
4616
|
+
return await broker.onOrderCloseCommit(payload);
|
|
4617
|
+
}, {
|
|
4618
|
+
fallback: (error) => {
|
|
4619
|
+
console.log(error);
|
|
4620
|
+
process.exit(-1);
|
|
4621
|
+
},
|
|
4622
|
+
});
|
|
4623
|
+
/** Called when a partial profit close is committed. */
|
|
4624
|
+
const commitPartialProfit = trycatch(async (payload) => {
|
|
4625
|
+
const broker = getBroker();
|
|
4626
|
+
return await broker.onPartialProfitCommit(payload);
|
|
4627
|
+
}, {
|
|
4628
|
+
fallback: (error) => {
|
|
4629
|
+
console.log(error);
|
|
4630
|
+
process.exit(-1);
|
|
4631
|
+
},
|
|
4632
|
+
});
|
|
4633
|
+
/** Called when a partial loss close is committed. */
|
|
4634
|
+
const commitPartialLoss = trycatch(async (payload) => {
|
|
4635
|
+
const broker = getBroker();
|
|
4636
|
+
return await broker.onPartialLossCommit(payload);
|
|
4637
|
+
}, {
|
|
4638
|
+
fallback: (error) => {
|
|
4639
|
+
console.log(error);
|
|
4640
|
+
process.exit(-1);
|
|
4641
|
+
},
|
|
4642
|
+
});
|
|
4643
|
+
/** Called when a trailing stop update is committed. */
|
|
4644
|
+
const commitTrailingStop = trycatch(async (payload) => {
|
|
4645
|
+
const broker = getBroker();
|
|
4646
|
+
return await broker.onTrailingStopCommit(payload);
|
|
4647
|
+
}, {
|
|
4648
|
+
fallback: (error) => {
|
|
4649
|
+
console.log(error);
|
|
4650
|
+
process.exit(-1);
|
|
4651
|
+
},
|
|
4652
|
+
});
|
|
4653
|
+
/** Called when a trailing take-profit update is committed. */
|
|
4654
|
+
const commitTrailingTake = trycatch(async (payload) => {
|
|
4655
|
+
const broker = getBroker();
|
|
4656
|
+
return await broker.onTrailingTakeCommit(payload);
|
|
4657
|
+
}, {
|
|
4658
|
+
fallback: (error) => {
|
|
4659
|
+
console.log(error);
|
|
4660
|
+
process.exit(-1);
|
|
4661
|
+
},
|
|
4662
|
+
});
|
|
4663
|
+
/** Called when a breakeven stop is committed (stop loss moved to entry price). */
|
|
4664
|
+
const commitBreakeven = trycatch(async (payload) => {
|
|
4665
|
+
const broker = getBroker();
|
|
4666
|
+
return await broker.onBreakevenCommit(payload);
|
|
4667
|
+
}, {
|
|
4668
|
+
fallback: (error) => {
|
|
4669
|
+
console.log(error);
|
|
4670
|
+
process.exit(-1);
|
|
4671
|
+
},
|
|
4672
|
+
});
|
|
4673
|
+
/** Called when a DCA (average-buy) entry is committed. */
|
|
4674
|
+
const commitAverageBuy = trycatch(async (payload) => {
|
|
4675
|
+
const broker = getBroker();
|
|
4676
|
+
return await broker.onAverageBuyCommit(payload);
|
|
4677
|
+
}, {
|
|
4678
|
+
fallback: (error) => {
|
|
4679
|
+
console.log(error);
|
|
4680
|
+
process.exit(-1);
|
|
4681
|
+
},
|
|
4682
|
+
});
|
|
4683
|
+
const COMMITS = [
|
|
4684
|
+
"signal-open",
|
|
4685
|
+
"signal-close",
|
|
4686
|
+
"partial-profit",
|
|
4687
|
+
"partial-loss",
|
|
4688
|
+
"average-buy",
|
|
4689
|
+
"trailing-stop",
|
|
4690
|
+
"trailing-take",
|
|
4691
|
+
"breakeven",
|
|
4692
|
+
];
|
|
4693
|
+
const ZERO_PNL = {
|
|
4694
|
+
pnlPercentage: 0,
|
|
4695
|
+
priceOpen: 0,
|
|
4696
|
+
priceClose: 0,
|
|
4697
|
+
pnlCost: 0,
|
|
4698
|
+
pnlEntries: 0,
|
|
4699
|
+
};
|
|
4700
|
+
const main$6 = async () => {
|
|
4701
|
+
if (!getEntry(import.meta.url)) {
|
|
4702
|
+
return;
|
|
4703
|
+
}
|
|
4704
|
+
const { values } = getArgs();
|
|
4705
|
+
if (!values.brokerdebug) {
|
|
4706
|
+
return;
|
|
4707
|
+
}
|
|
4708
|
+
{
|
|
4709
|
+
const cwd = process.cwd();
|
|
4710
|
+
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
4711
|
+
}
|
|
4712
|
+
await cli.configConnectionService.loadConfig("setup.config");
|
|
4713
|
+
{
|
|
4714
|
+
const loader = await cli.configConnectionService.loadConfig("loader.config");
|
|
4715
|
+
try {
|
|
4716
|
+
if (typeof loader === "function") {
|
|
4717
|
+
await loader();
|
|
4718
|
+
}
|
|
4719
|
+
if (typeof loader?.loader === "function") {
|
|
4720
|
+
await loader.loader();
|
|
4721
|
+
}
|
|
4722
|
+
}
|
|
4723
|
+
catch (error) {
|
|
4724
|
+
console.error("Module loader failed", error);
|
|
4725
|
+
process.exit(-1);
|
|
4726
|
+
}
|
|
4727
|
+
}
|
|
4728
|
+
await cli.moduleConnectionService.loadModule("brokerdebug.module");
|
|
4729
|
+
const [defaultExchangeName = null] = await listExchangeSchema();
|
|
4730
|
+
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
4731
|
+
const symbol = values.symbol || "BTCUSDT";
|
|
4732
|
+
const commit = values.commit || "signal-open";
|
|
4733
|
+
if (!COMMITS.includes(commit)) {
|
|
4734
|
+
console.error(`Unknown --commit value: ${commit}`);
|
|
4735
|
+
console.error(`Available: ${COMMITS.join(", ")}`);
|
|
4736
|
+
process.exit(1);
|
|
4737
|
+
}
|
|
4738
|
+
const candles = await Exchange.getCandles(symbol, "1m", 5, { exchangeName });
|
|
4739
|
+
const lastCandle = candles[candles.length - 1];
|
|
4740
|
+
const currentPrice = lastCandle.close;
|
|
4741
|
+
const priceTakeProfit = currentPrice * 1.02;
|
|
4742
|
+
const priceStopLoss = currentPrice * 0.98;
|
|
4743
|
+
const context = {
|
|
4744
|
+
strategyName: "broker-test",
|
|
4745
|
+
exchangeName: exchangeName,
|
|
4746
|
+
};
|
|
4747
|
+
const signalId = `broker-debug-${Date.now()}`;
|
|
4748
|
+
const when = alignToInterval(new Date(), "1m");
|
|
4749
|
+
{
|
|
4750
|
+
console.log(`symbol=${symbol} price=${currentPrice} commit=${commit}`);
|
|
4751
|
+
console.time(commit);
|
|
4752
|
+
}
|
|
4753
|
+
const run = async () => {
|
|
4754
|
+
if (commit === "signal-open") {
|
|
4755
|
+
await commitOrderOpen({
|
|
4756
|
+
type: "active",
|
|
4757
|
+
symbol,
|
|
4758
|
+
signalId,
|
|
4759
|
+
cost: 100,
|
|
4760
|
+
position: "long",
|
|
4761
|
+
priceOpen: currentPrice,
|
|
4762
|
+
priceTakeProfit,
|
|
4763
|
+
priceStopLoss,
|
|
4764
|
+
pnl: ZERO_PNL,
|
|
4765
|
+
peakProfit: ZERO_PNL,
|
|
4766
|
+
maxDrawdown: ZERO_PNL,
|
|
4767
|
+
attempt: 0,
|
|
4768
|
+
context,
|
|
4769
|
+
when,
|
|
4770
|
+
backtest: true,
|
|
4771
|
+
});
|
|
4772
|
+
}
|
|
4773
|
+
if (commit === "signal-close") {
|
|
4774
|
+
await commitOrderClose({
|
|
4775
|
+
symbol,
|
|
4776
|
+
signalId,
|
|
4777
|
+
cost: 100,
|
|
4778
|
+
position: "long",
|
|
4779
|
+
currentPrice,
|
|
4780
|
+
priceOpen: currentPrice,
|
|
4781
|
+
priceTakeProfit,
|
|
4782
|
+
priceStopLoss,
|
|
4783
|
+
totalEntries: 1,
|
|
4784
|
+
totalPartials: 0,
|
|
4785
|
+
attempt: 0,
|
|
4786
|
+
pnl: ZERO_PNL,
|
|
4787
|
+
peakProfit: ZERO_PNL,
|
|
4788
|
+
maxDrawdown: ZERO_PNL,
|
|
4789
|
+
context,
|
|
4790
|
+
when,
|
|
4791
|
+
backtest: true,
|
|
4792
|
+
});
|
|
4793
|
+
}
|
|
4794
|
+
if (commit === "partial-profit") {
|
|
4795
|
+
await commitPartialProfit({
|
|
4796
|
+
symbol,
|
|
4797
|
+
signalId,
|
|
4798
|
+
percentToClose: 50,
|
|
4799
|
+
cost: 50,
|
|
4800
|
+
currentPrice,
|
|
4801
|
+
position: "long",
|
|
4802
|
+
priceTakeProfit,
|
|
4803
|
+
priceStopLoss,
|
|
4804
|
+
context,
|
|
4805
|
+
when,
|
|
4806
|
+
backtest: true,
|
|
4807
|
+
});
|
|
4808
|
+
}
|
|
4809
|
+
if (commit === "partial-loss") {
|
|
4810
|
+
await commitPartialLoss({
|
|
4811
|
+
symbol,
|
|
4812
|
+
signalId,
|
|
4813
|
+
percentToClose: 50,
|
|
4814
|
+
cost: 50,
|
|
4815
|
+
currentPrice,
|
|
4816
|
+
position: "long",
|
|
4817
|
+
priceTakeProfit,
|
|
4818
|
+
priceStopLoss,
|
|
4819
|
+
context,
|
|
4820
|
+
when,
|
|
4821
|
+
backtest: true,
|
|
4822
|
+
});
|
|
4823
|
+
}
|
|
4824
|
+
if (commit === "average-buy") {
|
|
4825
|
+
await commitAverageBuy({
|
|
4826
|
+
symbol,
|
|
4827
|
+
signalId,
|
|
4828
|
+
currentPrice,
|
|
4829
|
+
cost: 100,
|
|
4830
|
+
position: "long",
|
|
4831
|
+
priceTakeProfit,
|
|
4832
|
+
priceStopLoss,
|
|
4833
|
+
context,
|
|
4834
|
+
when,
|
|
4835
|
+
backtest: true,
|
|
4836
|
+
});
|
|
4837
|
+
}
|
|
4838
|
+
if (commit === "trailing-stop") {
|
|
4839
|
+
await commitTrailingStop({
|
|
4840
|
+
symbol,
|
|
4841
|
+
signalId,
|
|
4842
|
+
percentShift: -50,
|
|
4843
|
+
currentPrice,
|
|
4844
|
+
newStopLossPrice: priceStopLoss + (currentPrice - priceStopLoss) * 0.5,
|
|
4845
|
+
takeProfitPrice: priceTakeProfit,
|
|
4846
|
+
position: "long",
|
|
4847
|
+
context,
|
|
4848
|
+
when,
|
|
4849
|
+
backtest: true,
|
|
4850
|
+
});
|
|
4851
|
+
}
|
|
4852
|
+
if (commit === "trailing-take") {
|
|
4853
|
+
await commitTrailingTake({
|
|
4854
|
+
symbol,
|
|
4855
|
+
signalId,
|
|
4856
|
+
percentShift: 50,
|
|
4857
|
+
currentPrice,
|
|
4858
|
+
newTakeProfitPrice: priceTakeProfit + (priceTakeProfit - currentPrice) * 0.5,
|
|
4859
|
+
takeProfitPrice: priceTakeProfit,
|
|
4860
|
+
position: "long",
|
|
4861
|
+
context,
|
|
4862
|
+
when,
|
|
4863
|
+
backtest: true,
|
|
4864
|
+
});
|
|
4865
|
+
}
|
|
4866
|
+
if (commit === "breakeven") {
|
|
4867
|
+
await commitBreakeven({
|
|
4868
|
+
symbol,
|
|
4869
|
+
signalId,
|
|
4870
|
+
currentPrice,
|
|
4871
|
+
newStopLossPrice: currentPrice,
|
|
4872
|
+
newTakeProfitPrice: priceTakeProfit,
|
|
4873
|
+
position: "long",
|
|
4874
|
+
context,
|
|
4875
|
+
when,
|
|
4876
|
+
backtest: true,
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4879
|
+
};
|
|
4880
|
+
await runInMockContext(run, {
|
|
4881
|
+
when,
|
|
4882
|
+
strategyName: context.strategyName,
|
|
4883
|
+
exchangeName: context.exchangeName,
|
|
4884
|
+
frameName: "",
|
|
4885
|
+
symbol,
|
|
4886
|
+
backtest: true,
|
|
4887
|
+
});
|
|
4888
|
+
{
|
|
4889
|
+
console.log(`${commit} OK`);
|
|
4890
|
+
console.timeEnd(commit);
|
|
4891
|
+
}
|
|
4892
|
+
process.exit(0);
|
|
4893
|
+
};
|
|
4894
|
+
main$6();
|
|
4895
|
+
|
|
4896
|
+
const SIMULATOR_NAME$1 = "cli_simulator";
|
|
4897
|
+
const IDEA_DIRECTIONS$1 = ["LONG", "SHORT", "NEUTRAL"];
|
|
4898
|
+
const validateIdea$1 = (idea, line) => {
|
|
4447
4899
|
if (typeof idea !== "object" || idea === null) {
|
|
4448
4900
|
return `line ${line}: not an object`;
|
|
4449
4901
|
}
|
|
@@ -4456,15 +4908,16 @@ const validateIdea = (idea, line) => {
|
|
|
4456
4908
|
if (typeof idea.symbol !== "string" || !idea.symbol) {
|
|
4457
4909
|
return `line ${line}: "symbol" must be a non-empty string`;
|
|
4458
4910
|
}
|
|
4459
|
-
if (!IDEA_DIRECTIONS.includes(idea.direction)) {
|
|
4460
|
-
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
4911
|
+
if (!IDEA_DIRECTIONS$1.includes(idea.direction)) {
|
|
4912
|
+
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS$1.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
4461
4913
|
}
|
|
4462
4914
|
if (typeof idea.author !== "string" || !idea.author) {
|
|
4463
4915
|
return `line ${line}: "author" must be a non-empty string`;
|
|
4464
4916
|
}
|
|
4465
4917
|
return null;
|
|
4466
4918
|
};
|
|
4467
|
-
const toMarkdown = (result) => {
|
|
4919
|
+
const toMarkdown$1 = (result) => {
|
|
4920
|
+
const profitable = result.reports.filter(({ totalPnlPercent }) => totalPnlPercent > 0).length;
|
|
4468
4921
|
const lines = [];
|
|
4469
4922
|
lines.push(`# Simulator Report — ${result.symbol}`);
|
|
4470
4923
|
lines.push("");
|
|
@@ -4473,33 +4926,36 @@ const toMarkdown = (result) => {
|
|
|
4473
4926
|
lines.push(`| Ideas (total / directional) | ${result.ideasTotal} / ${result.ideasDirectional} |`);
|
|
4474
4927
|
lines.push(`| Profiles (truncated) | ${result.profileCount} (${result.truncatedCount}) |`);
|
|
4475
4928
|
lines.push(`| Authors allowed / banned | ${result.allowedAuthors.length} / ${result.bannedAuthors.length} |`);
|
|
4476
|
-
lines.push(`|
|
|
4929
|
+
lines.push(`| Profitable corridor | ${profitable} / ${result.reports.length} grid points |`);
|
|
4477
4930
|
lines.push(`| Hold minutes avg / p95 / p99 | ${result.avgHoldMinutes.toFixed(0)} / ${result.p95HoldMinutes} / ${result.p99HoldMinutes} |`);
|
|
4478
4931
|
lines.push("");
|
|
4479
4932
|
lines.push(`## Ranking winners`);
|
|
4480
4933
|
lines.push("");
|
|
4481
|
-
lines.push(`| Criterion | Stop% |
|
|
4482
|
-
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
|
|
4934
|
+
lines.push(`| Criterion | Stop% | Hold | Track | HitRate | Trades | PNL% | WinRate | Sharpe | Sortino |`);
|
|
4935
|
+
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |`);
|
|
4483
4936
|
for (const best of result.best) {
|
|
4484
4937
|
if (!best.report) {
|
|
4485
|
-
lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | —
|
|
4938
|
+
lines.push(`| ${best.criterion} | — | — | — | — | — | — | — | — | — |`);
|
|
4486
4939
|
continue;
|
|
4487
4940
|
}
|
|
4488
4941
|
const { point } = best.report;
|
|
4489
|
-
lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.
|
|
4942
|
+
lines.push(`| ${best.criterion} | ${point.hardStopPercent} | ${point.holdMinutes / 60}h | ${point.minAuthorTrack} | ${point.minAuthorHitRate} | ` +
|
|
4490
4943
|
`${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ${(best.report.winRate * 100).toFixed(0)}% | ${best.report.sharpe.toFixed(2)} | ${best.report.sortino.toFixed(2)} |`);
|
|
4491
4944
|
}
|
|
4492
4945
|
lines.push("");
|
|
4493
|
-
lines.push(`## Allowed authors (
|
|
4946
|
+
lines.push(`## Allowed authors (whitelist of the sharpe winner rule)`);
|
|
4494
4947
|
lines.push("");
|
|
4495
4948
|
lines.push(`| Author | Ideas | Hits | HitRate |`);
|
|
4496
4949
|
lines.push(`| --- | --- | --- | --- |`);
|
|
4497
|
-
|
|
4950
|
+
// артефакт авторов живёт по-победительно в best[] — ран-левел
|
|
4951
|
+
// authorStats в ISimulatorResult больше нет
|
|
4952
|
+
const sharpeBest = result.best.find(({ criterion }) => criterion === "sharpe");
|
|
4953
|
+
for (const stat of (sharpeBest?.authorStats ?? []).filter(({ banned }) => !banned)) {
|
|
4498
4954
|
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${(stat.hitRate * 100).toFixed(0)}% |`);
|
|
4499
4955
|
}
|
|
4500
4956
|
return lines.join("\n");
|
|
4501
4957
|
};
|
|
4502
|
-
const main$
|
|
4958
|
+
const main$5 = async () => {
|
|
4503
4959
|
if (!getEntry(import.meta.url)) {
|
|
4504
4960
|
return;
|
|
4505
4961
|
}
|
|
@@ -4536,7 +4992,7 @@ const main$4 = async () => {
|
|
|
4536
4992
|
console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
|
|
4537
4993
|
process.exit(1);
|
|
4538
4994
|
}
|
|
4539
|
-
const problem = validateIdea(idea, i + 1);
|
|
4995
|
+
const problem = validateIdea$1(idea, i + 1);
|
|
4540
4996
|
if (problem) {
|
|
4541
4997
|
console.error(`Error: ideas file does not match the idea structure — ${problem}`);
|
|
4542
4998
|
console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
|
|
@@ -4572,11 +5028,48 @@ const main$4 = async () => {
|
|
|
4572
5028
|
}
|
|
4573
5029
|
const [defaultExchangeName = null] = await listExchangeSchema();
|
|
4574
5030
|
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
5031
|
+
if (values.verbose) {
|
|
5032
|
+
overrideExchangeSchema({
|
|
5033
|
+
exchangeName,
|
|
5034
|
+
callbacks: {
|
|
5035
|
+
onCandleData(symbol, interval, since) {
|
|
5036
|
+
console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
|
|
5037
|
+
},
|
|
5038
|
+
},
|
|
5039
|
+
});
|
|
5040
|
+
}
|
|
4575
5041
|
const symbol = values.symbol || "BTCUSDT";
|
|
5042
|
+
// Проба осуществимости, НЕ поиск заработка (подбор параметров — у
|
|
5043
|
+
// --tune): собирающая прибыль механика выключена, остаётся вопрос
|
|
5044
|
+
// "есть ли прибыльный коридор" по стопу x холду x правилу бана —
|
|
5045
|
+
// 48 точек, быстрый вердикт
|
|
4576
5046
|
addSimulatorSchema({
|
|
4577
|
-
simulatorName: SIMULATOR_NAME,
|
|
5047
|
+
simulatorName: SIMULATOR_NAME$1,
|
|
4578
5048
|
exchangeName,
|
|
5049
|
+
gridAxes: {
|
|
5050
|
+
// грубая шкала катастрофы: коридор должен быть широким, не точкой
|
|
5051
|
+
hardStopPercent: [2, 3, 5, 7],
|
|
5052
|
+
// инертен: проба не собирает прибыль, выход — по времени или стопу
|
|
5053
|
+
trailingTakePercent: [100],
|
|
5054
|
+
holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
|
|
5055
|
+
// одного проверенного автора достаточно — консенсус не перебираем
|
|
5056
|
+
minIdeasAligned: [1],
|
|
5057
|
+
// правило бана — единственная перебираемая "умность" пробы
|
|
5058
|
+
minAuthorTrack: [3, 5],
|
|
5059
|
+
minAuthorHitRate: [0.5, 0.6],
|
|
5060
|
+
minAuthorWilson: [0],
|
|
5061
|
+
minWeightAligned: [0],
|
|
5062
|
+
profitLockPercent: [0],
|
|
5063
|
+
authorMetric: ["close"],
|
|
5064
|
+
banCriteria: ["sharpe", "pnl"],
|
|
5065
|
+
},
|
|
5066
|
+
reportOrder: "sharpe",
|
|
4579
5067
|
callbacks: {
|
|
5068
|
+
onProgress: (symbol, stage, processed, total) => {
|
|
5069
|
+
if (values.verbose) {
|
|
5070
|
+
console.log("onProgress", { symbol, stage, processed, total });
|
|
5071
|
+
}
|
|
5072
|
+
},
|
|
4580
5073
|
onIdeas: (symbol, ideasTotal, ideasDirectional) => {
|
|
4581
5074
|
if (values.verbose) {
|
|
4582
5075
|
console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
|
|
@@ -4632,7 +5125,7 @@ const main$4 = async () => {
|
|
|
4632
5125
|
});
|
|
4633
5126
|
const result = await Simulator.run({
|
|
4634
5127
|
symbol,
|
|
4635
|
-
simulatorName: SIMULATOR_NAME,
|
|
5128
|
+
simulatorName: SIMULATOR_NAME$1,
|
|
4636
5129
|
ideas,
|
|
4637
5130
|
});
|
|
4638
5131
|
const dumpName = values.output || `simulator_${symbol}_${Date.now()}`;
|
|
@@ -4647,11 +5140,299 @@ const main$4 = async () => {
|
|
|
4647
5140
|
if (values.markdown) {
|
|
4648
5141
|
const filePath = resolve(dumpDir, `${dumpName}.md`);
|
|
4649
5142
|
await mkdir(dumpDir, { recursive: true });
|
|
4650
|
-
await writeFile(filePath, toMarkdown(result), "utf-8");
|
|
5143
|
+
await writeFile(filePath, toMarkdown$1(result), "utf-8");
|
|
4651
5144
|
console.log(`Saved: ${filePath}`);
|
|
4652
5145
|
process.exit(0);
|
|
4653
5146
|
}
|
|
4654
|
-
console.log(toMarkdown(result));
|
|
5147
|
+
console.log(toMarkdown$1(result));
|
|
5148
|
+
process.exit(0);
|
|
5149
|
+
};
|
|
5150
|
+
main$5();
|
|
5151
|
+
|
|
5152
|
+
const SIMULATOR_NAME = "cli_tune";
|
|
5153
|
+
/** Доля времени фида, уходящая в обучение; хвост — один OOS-выстрел. */
|
|
5154
|
+
const DEFAULT_TRAIN_SPLIT = 0.7;
|
|
5155
|
+
const IDEA_DIRECTIONS = ["LONG", "SHORT", "NEUTRAL"];
|
|
5156
|
+
const validateIdea = (idea, line) => {
|
|
5157
|
+
if (typeof idea !== "object" || idea === null) {
|
|
5158
|
+
return `line ${line}: not an object`;
|
|
5159
|
+
}
|
|
5160
|
+
if (typeof idea.id !== "number") {
|
|
5161
|
+
return `line ${line}: "id" must be a number, got ${typeof idea.id}`;
|
|
5162
|
+
}
|
|
5163
|
+
if (typeof idea.ts !== "number") {
|
|
5164
|
+
return `line ${line}: "ts" must be a number (unix ms), got ${typeof idea.ts}`;
|
|
5165
|
+
}
|
|
5166
|
+
if (typeof idea.symbol !== "string" || !idea.symbol) {
|
|
5167
|
+
return `line ${line}: "symbol" must be a non-empty string`;
|
|
5168
|
+
}
|
|
5169
|
+
if (!IDEA_DIRECTIONS.includes(idea.direction)) {
|
|
5170
|
+
return `line ${line}: "direction" must be one of ${IDEA_DIRECTIONS.join("|")}, got ${JSON.stringify(idea.direction)}`;
|
|
5171
|
+
}
|
|
5172
|
+
if (typeof idea.author !== "string" || !idea.author) {
|
|
5173
|
+
return `line ${line}: "author" must be a non-empty string`;
|
|
5174
|
+
}
|
|
5175
|
+
return null;
|
|
5176
|
+
};
|
|
5177
|
+
const pointLabel = (point) => `H=${point.hardStopPercent} TT=${point.trailingTakePercent} hold=${point.holdMinutes / 60}h N=${point.minIdeasAligned} ` +
|
|
5178
|
+
`track=${point.minAuthorTrack} rate=${point.minAuthorHitRate} wilson=${point.minAuthorWilson} W=${point.minWeightAligned} ` +
|
|
5179
|
+
`lock=${point.profitLockPercent} metric=${point.authorMetric}`;
|
|
5180
|
+
const fmtRatio = (value) => Number.isFinite(value) ? value.toFixed(2) : "inf";
|
|
5181
|
+
const toMarkdown = (train, test, trainSplit) => {
|
|
5182
|
+
const lines = [];
|
|
5183
|
+
lines.push(`# Tune Report — ${train.symbol}`);
|
|
5184
|
+
lines.push("");
|
|
5185
|
+
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.`);
|
|
5186
|
+
lines.push("");
|
|
5187
|
+
lines.push(`## Train (${train.ideasDirectional} directional ideas, ${train.reports.length} grid points)`);
|
|
5188
|
+
lines.push("");
|
|
5189
|
+
lines.push(`| Criterion | Point | Trades | PNL% | WinRate | DD% | Sharpe | Sortino |`);
|
|
5190
|
+
lines.push(`| --- | --- | --- | --- | --- | --- | --- | --- |`);
|
|
5191
|
+
for (const best of train.best) {
|
|
5192
|
+
if (!best.report) {
|
|
5193
|
+
lines.push(`| ${best.criterion} | — | — | — | — | — | — | — |`);
|
|
5194
|
+
continue;
|
|
5195
|
+
}
|
|
5196
|
+
lines.push(`| ${best.criterion} | ${pointLabel(best.report.point)} | ${best.report.trades} | ${best.report.totalPnlPercent.toFixed(2)}% | ` +
|
|
5197
|
+
`${(best.report.winRate * 100).toFixed(0)}% | ${best.report.maxSeriesDrawdownPercent.toFixed(2)} | ${fmtRatio(best.report.sharpe)} | ${fmtRatio(best.report.sortino)} |`);
|
|
5198
|
+
}
|
|
5199
|
+
lines.push("");
|
|
5200
|
+
lines.push(`## Out-of-sample (frozen sharpe winner, ${test.ideasDirectional} tail ideas)`);
|
|
5201
|
+
lines.push("");
|
|
5202
|
+
lines.push(`Frozen point: ${pointLabel(test.point)}`);
|
|
5203
|
+
lines.push("");
|
|
5204
|
+
lines.push(`| Metric | Value |`);
|
|
5205
|
+
lines.push(`| --- | --- |`);
|
|
5206
|
+
lines.push(`| Trades (skipped busy) | ${test.report.trades} (${test.report.skippedBusy}) |`);
|
|
5207
|
+
lines.push(`| PNL | ${test.report.totalPnlPercent.toFixed(2)}% |`);
|
|
5208
|
+
lines.push(`| Win rate | ${(test.report.winRate * 100).toFixed(0)}% |`);
|
|
5209
|
+
lines.push(`| Profit factor | ${fmtRatio(test.report.profitFactor)} |`);
|
|
5210
|
+
lines.push(`| Max series drawdown | ${test.report.maxSeriesDrawdownPercent.toFixed(2)}% |`);
|
|
5211
|
+
lines.push(`| Sharpe / Sortino | ${fmtRatio(test.report.sharpe)} / ${fmtRatio(test.report.sortino)} |`);
|
|
5212
|
+
lines.push(`| Recovery factor | ${fmtRatio(test.report.recoveryFactor)} |`);
|
|
5213
|
+
lines.push(`| Exits | ${Object.entries(test.report.exitReasons).map(([reason, count]) => `${reason}=${count}`).join(", ")} |`);
|
|
5214
|
+
lines.push("");
|
|
5215
|
+
lines.push(`## Test trades`);
|
|
5216
|
+
lines.push("");
|
|
5217
|
+
lines.push(`| Direction | Exit | PNL% | Hold | Entry (UTC) |`);
|
|
5218
|
+
lines.push(`| --- | --- | --- | --- | --- |`);
|
|
5219
|
+
for (const trade of test.trades) {
|
|
5220
|
+
lines.push(`| ${trade.direction} | ${trade.exitReason} | ${trade.pnlPercent.toFixed(2)} | ${trade.holdMinutesActual}m | ${new Date(trade.entryTimestamp).toISOString()} |`);
|
|
5221
|
+
}
|
|
5222
|
+
lines.push("");
|
|
5223
|
+
lines.push(`## Frozen author track record (train range, sharpe winner rule)`);
|
|
5224
|
+
lines.push("");
|
|
5225
|
+
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.`);
|
|
5226
|
+
lines.push("");
|
|
5227
|
+
lines.push(`| Author | Ideas | Hits | Banned |`);
|
|
5228
|
+
lines.push(`| --- | --- | --- | --- |`);
|
|
5229
|
+
for (const stat of test.authorStats) {
|
|
5230
|
+
lines.push(`| ${stat.author} | ${stat.ideas} | ${stat.hits} | ${stat.banned ? "yes" : ""} |`);
|
|
5231
|
+
}
|
|
5232
|
+
return lines.join("\n");
|
|
5233
|
+
};
|
|
5234
|
+
const main$4 = async () => {
|
|
5235
|
+
if (!getEntry(import.meta.url)) {
|
|
5236
|
+
return;
|
|
5237
|
+
}
|
|
5238
|
+
const { values, positionals } = getArgs();
|
|
5239
|
+
if (!values.tune) {
|
|
5240
|
+
return;
|
|
5241
|
+
}
|
|
5242
|
+
const [ideasPath = null] = positionals.filter((value) => value.endsWith(".jsonl"));
|
|
5243
|
+
if (!ideasPath) {
|
|
5244
|
+
console.error("Error: positional path to an ideas .jsonl file is required");
|
|
5245
|
+
process.exit(1);
|
|
5246
|
+
}
|
|
5247
|
+
let ideas = [];
|
|
5248
|
+
{
|
|
5249
|
+
let content;
|
|
5250
|
+
try {
|
|
5251
|
+
content = await readFile(resolve(ideasPath), "utf-8");
|
|
5252
|
+
}
|
|
5253
|
+
catch (error) {
|
|
5254
|
+
console.error(`Error: cannot read ideas file: ${ideasPath}`);
|
|
5255
|
+
process.exit(1);
|
|
5256
|
+
}
|
|
5257
|
+
const rows = content.split("\n").filter(Boolean);
|
|
5258
|
+
if (!rows.length) {
|
|
5259
|
+
console.error(`Error: ideas file is empty: ${ideasPath}`);
|
|
5260
|
+
process.exit(1);
|
|
5261
|
+
}
|
|
5262
|
+
for (let i = 0; i < rows.length; i++) {
|
|
5263
|
+
let idea;
|
|
5264
|
+
try {
|
|
5265
|
+
idea = JSON.parse(rows[i]);
|
|
5266
|
+
}
|
|
5267
|
+
catch {
|
|
5268
|
+
console.error(`Error: invalid JSON in ideas file, line ${i + 1}`);
|
|
5269
|
+
process.exit(1);
|
|
5270
|
+
}
|
|
5271
|
+
const problem = validateIdea(idea, i + 1);
|
|
5272
|
+
if (problem) {
|
|
5273
|
+
console.error(`Error: ideas file does not match the idea structure — ${problem}`);
|
|
5274
|
+
console.error(`Expected shape: { "id": number, "ts": number, "symbol": string, "direction": "LONG"|"SHORT"|"NEUTRAL", "author": string }`);
|
|
5275
|
+
process.exit(1);
|
|
5276
|
+
}
|
|
5277
|
+
ideas.push(idea);
|
|
5278
|
+
}
|
|
5279
|
+
}
|
|
5280
|
+
const trainSplit = values.split
|
|
5281
|
+
? parseFloat(values.split)
|
|
5282
|
+
: DEFAULT_TRAIN_SPLIT;
|
|
5283
|
+
if (!Number.isFinite(trainSplit) || trainSplit <= 0 || trainSplit >= 1) {
|
|
5284
|
+
console.error(`Error: --split must be a fraction between 0 and 1, got ${values.split}`);
|
|
5285
|
+
process.exit(1);
|
|
5286
|
+
}
|
|
5287
|
+
{
|
|
5288
|
+
const cwd = process.cwd();
|
|
5289
|
+
dotenv.config({ path: path.join(cwd, '.env'), override: true, quiet: true });
|
|
5290
|
+
}
|
|
5291
|
+
await cli.configConnectionService.loadConfig("setup.config");
|
|
5292
|
+
{
|
|
5293
|
+
const loader = await cli.configConnectionService.loadConfig("loader.config");
|
|
5294
|
+
try {
|
|
5295
|
+
if (typeof loader === "function") {
|
|
5296
|
+
await loader();
|
|
5297
|
+
}
|
|
5298
|
+
if (typeof loader?.loader === "function") {
|
|
5299
|
+
await loader.loader();
|
|
5300
|
+
}
|
|
5301
|
+
}
|
|
5302
|
+
catch (error) {
|
|
5303
|
+
console.error("Module loader failed", error);
|
|
5304
|
+
process.exit(-1);
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
5307
|
+
await cli.moduleConnectionService.loadModule("tune.module");
|
|
5308
|
+
{
|
|
5309
|
+
await cli.exchangeSchemaService.addSchema();
|
|
5310
|
+
await cli.symbolSchemaService.addSchema();
|
|
5311
|
+
}
|
|
5312
|
+
const [defaultExchangeName = null] = await listExchangeSchema();
|
|
5313
|
+
const exchangeName = values.exchange || defaultExchangeName?.exchangeName;
|
|
5314
|
+
if (values.verbose) {
|
|
5315
|
+
overrideExchangeSchema({
|
|
5316
|
+
exchangeName,
|
|
5317
|
+
callbacks: {
|
|
5318
|
+
onCandleData(symbol, interval, since) {
|
|
5319
|
+
console.log(`Received candle data for symbol: ${symbol}, interval: ${interval}, since: ${since.toUTCString()}`);
|
|
5320
|
+
},
|
|
5321
|
+
},
|
|
5322
|
+
});
|
|
5323
|
+
}
|
|
5324
|
+
const symbol = values.symbol || "BTCUSDT";
|
|
5325
|
+
// Подбор параметров: полная сетка с собирающей прибыль механикой —
|
|
5326
|
+
// замок, трейлинг, взвешенный консенсус, Вильсон, обе метрики бана.
|
|
5327
|
+
// Честность обеспечивается сплитом: обучение видит только голову,
|
|
5328
|
+
// хвост тратится на один выстрел замороженного sharpe-победителя
|
|
5329
|
+
addSimulatorSchema({
|
|
5330
|
+
simulatorName: SIMULATOR_NAME,
|
|
5331
|
+
exchangeName,
|
|
5332
|
+
gridAxes: {
|
|
5333
|
+
// стопы < 2% сидят внутри медианного шейкаута и не выигрывают
|
|
5334
|
+
hardStopPercent: [2, 2.5, 3, 4, 5, 7],
|
|
5335
|
+
trailingTakePercent: [1, 1.5, 2, 3, 4],
|
|
5336
|
+
holdMinutes: [24 * 60, 2 * 24 * 60, 3 * 24 * 60],
|
|
5337
|
+
minIdeasAligned: [1, 2],
|
|
5338
|
+
minAuthorTrack: [2, 3, 5],
|
|
5339
|
+
minAuthorHitRate: [0.5, 0.6],
|
|
5340
|
+
minAuthorWilson: [0, 0.6],
|
|
5341
|
+
minWeightAligned: [0, 0.6, 1.2],
|
|
5342
|
+
profitLockPercent: [0, 1.5, 2.5],
|
|
5343
|
+
authorMetric: ["close", "reach"],
|
|
5344
|
+
banCriteria: ["sharpe"],
|
|
5345
|
+
},
|
|
5346
|
+
reportOrder: "sharpe",
|
|
5347
|
+
callbacks: {
|
|
5348
|
+
onProgress: (symbol, stage, processed, total) => {
|
|
5349
|
+
if (values.verbose) {
|
|
5350
|
+
console.log("onProgress", { symbol, stage, processed, total });
|
|
5351
|
+
}
|
|
5352
|
+
},
|
|
5353
|
+
onIdeas: (symbol, ideasTotal, ideasDirectional) => {
|
|
5354
|
+
if (values.verbose) {
|
|
5355
|
+
console.log("onIdeas", { symbol, ideasTotal, ideasDirectional });
|
|
5356
|
+
}
|
|
5357
|
+
},
|
|
5358
|
+
onProfiles: (symbol, profiles, truncatedCount) => {
|
|
5359
|
+
if (values.verbose) {
|
|
5360
|
+
console.log("onProfiles", { symbol, profiles: profiles.length, truncatedCount });
|
|
5361
|
+
}
|
|
5362
|
+
},
|
|
5363
|
+
onAuthorsTrained: (symbol, stats, bannedIdeas) => {
|
|
5364
|
+
if (values.verbose) {
|
|
5365
|
+
console.log("onAuthorsTrained", {
|
|
5366
|
+
symbol,
|
|
5367
|
+
authors: stats.length,
|
|
5368
|
+
banned: stats.filter(({ banned }) => banned).length,
|
|
5369
|
+
bannedIdeas,
|
|
5370
|
+
});
|
|
5371
|
+
}
|
|
5372
|
+
},
|
|
5373
|
+
onRanking: (symbol, criterion, _sorted, best) => {
|
|
5374
|
+
if (values.verbose) {
|
|
5375
|
+
console.log("onRanking", {
|
|
5376
|
+
symbol,
|
|
5377
|
+
criterion,
|
|
5378
|
+
point: best.report?.point ?? null,
|
|
5379
|
+
pnl: best.report ? +best.report.totalPnlPercent.toFixed(2) : null,
|
|
5380
|
+
});
|
|
5381
|
+
}
|
|
5382
|
+
},
|
|
5383
|
+
onTestDone: (symbol, result) => {
|
|
5384
|
+
if (values.verbose) {
|
|
5385
|
+
console.log("onTestDone", {
|
|
5386
|
+
symbol,
|
|
5387
|
+
trades: result.report.trades,
|
|
5388
|
+
pnl: +result.report.totalPnlPercent.toFixed(2),
|
|
5389
|
+
});
|
|
5390
|
+
}
|
|
5391
|
+
},
|
|
5392
|
+
},
|
|
5393
|
+
});
|
|
5394
|
+
// обучение видит ТОЛЬКО голову ленты; хвост — один OOS-выстрел
|
|
5395
|
+
const sorted = [...ideas].sort((a, b) => a.ts - b.ts);
|
|
5396
|
+
const cutoff = sorted[0].ts + (sorted[sorted.length - 1].ts - sorted[0].ts) * trainSplit;
|
|
5397
|
+
const trainIdeas = sorted.filter(({ ts }) => ts < cutoff);
|
|
5398
|
+
const testIdeas = sorted.filter(({ ts }) => ts >= cutoff);
|
|
5399
|
+
const train = await Simulator.run({
|
|
5400
|
+
symbol,
|
|
5401
|
+
simulatorName: SIMULATOR_NAME,
|
|
5402
|
+
ideas: trainIdeas,
|
|
5403
|
+
});
|
|
5404
|
+
const sharpeBest = train.best.find(({ criterion }) => criterion === "sharpe");
|
|
5405
|
+
if (!sharpeBest?.report) {
|
|
5406
|
+
console.error("Error: training produced no sharpe winner — nothing to freeze for the out-of-sample shot");
|
|
5407
|
+
process.exit(1);
|
|
5408
|
+
}
|
|
5409
|
+
// заморозка: точка и трек-рекорд sharpe-победителя как есть — из
|
|
5410
|
+
// замороженных статов test() читает только author/ideas/hits и сам
|
|
5411
|
+
// перевыводит баны и веса под правило точки
|
|
5412
|
+
const test = await Simulator.test({
|
|
5413
|
+
symbol,
|
|
5414
|
+
simulatorName: SIMULATOR_NAME,
|
|
5415
|
+
ideas: testIdeas,
|
|
5416
|
+
point: sharpeBest.report.point,
|
|
5417
|
+
authorStats: sharpeBest.authorStats,
|
|
5418
|
+
});
|
|
5419
|
+
const dumpName = values.output || `tune_${symbol}_${Date.now()}`;
|
|
5420
|
+
const dumpDir = join(process.cwd(), "dump");
|
|
5421
|
+
if (values.json) {
|
|
5422
|
+
const filePath = resolve(dumpDir, `${dumpName}.json`);
|
|
5423
|
+
await mkdir(dumpDir, { recursive: true });
|
|
5424
|
+
await writeFile(filePath, JSON.stringify({ trainSplit, train, test }, null, 2), "utf-8");
|
|
5425
|
+
console.log(`Saved: ${filePath}`);
|
|
5426
|
+
process.exit(0);
|
|
5427
|
+
}
|
|
5428
|
+
if (values.markdown) {
|
|
5429
|
+
const filePath = resolve(dumpDir, `${dumpName}.md`);
|
|
5430
|
+
await mkdir(dumpDir, { recursive: true });
|
|
5431
|
+
await writeFile(filePath, toMarkdown(train, test, trainSplit), "utf-8");
|
|
5432
|
+
console.log(`Saved: ${filePath}`);
|
|
5433
|
+
process.exit(0);
|
|
5434
|
+
}
|
|
5435
|
+
console.log(toMarkdown(train, test, trainSplit));
|
|
4655
5436
|
process.exit(0);
|
|
4656
5437
|
};
|
|
4657
5438
|
main$4();
|
|
@@ -4852,7 +5633,8 @@ Modes:
|
|
|
4852
5633
|
--dump Fetch and save raw OHLCV candles
|
|
4853
5634
|
--pnldebug Simulate PnL per minute for a given entry price and direction
|
|
4854
5635
|
--brokerdebug Fire a single broker commit against the live broker adapter
|
|
4855
|
-
--simulator <ideas.jsonl>
|
|
5636
|
+
--simulator <ideas.jsonl> Feasibility probe over crowd ideas: is there a profitable corridor at all
|
|
5637
|
+
--tune <ideas.jsonl> Walk-forward parameter search: train on the head, one frozen shot on the tail
|
|
4856
5638
|
--flush <entry...> Delete report/log/markdown/agent folders from strategy dump dir
|
|
4857
5639
|
--init Scaffold a new project in the current directory
|
|
4858
5640
|
--docker Scaffold a Docker workspace for running strategies in a container
|
|
@@ -4987,19 +5769,47 @@ Simulator flags (--simulator):
|
|
|
4987
5769
|
"author": string }. The file is validated BEFORE any work; a line that does not
|
|
4988
5770
|
match the structure aborts the run with an error.
|
|
4989
5771
|
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
5772
|
+
A FEASIBILITY PROBE, not a parameter search (that is --tune): the profit-harvesting
|
|
5773
|
+
machinery is off (no profit lock, inert trailing, no weighted consensus), and a
|
|
5774
|
+
fast 48-point grid of hard stop x hold x ban rule answers one question — does the
|
|
5775
|
+
feed contain a profitable corridor at all. One 5-day candle pass per idea, flood
|
|
5776
|
+
dedupe (one idea per author per direction per 8h), default-ban author filter,
|
|
5777
|
+
time-based Sharpe/Sortino, four ranking winners. The report carries the corridor
|
|
5778
|
+
share and the whitelist of the sharpe winner's ban rule as evidence.
|
|
4994
5779
|
Ideas of other symbols are filtered out — one shared feed serves any --symbol.
|
|
4995
5780
|
|
|
4996
5781
|
No output flag → print the Markdown summary to stdout. With --verbose every
|
|
4997
|
-
simulator lifecycle callback (onIdeas, onProfiles, onAuthorsTrained,
|
|
4998
|
-
onRanking, onDone) is logged to the console as it fires.
|
|
5782
|
+
simulator lifecycle callback (onProgress, onIdeas, onProfiles, onAuthorsTrained,
|
|
5783
|
+
onGridPoint, onRanking, onDone) is logged to the console as it fires.
|
|
4999
5784
|
|
|
5000
5785
|
Module file ./modules/simulator.module is loaded automatically if it exists
|
|
5001
5786
|
(register your exchange there); without it CCXT Binance is used by default.
|
|
5002
5787
|
|
|
5788
|
+
Tune flags (--tune):
|
|
5789
|
+
|
|
5790
|
+
--symbol <string> Trading pair to tune (default: BTCUSDT)
|
|
5791
|
+
--exchange <string> Exchange name (default: first registered)
|
|
5792
|
+
--split <string> Train share of the feed time range, 0..1 (default: 0.7)
|
|
5793
|
+
--output <string> Output file base name (default: tune_{SYMBOL}_{TIMESTAMP})
|
|
5794
|
+
--json Save { trainSplit, train, test } to ./dump/<output>.json
|
|
5795
|
+
--markdown Save walk-forward report to ./dump/<output>.md
|
|
5796
|
+
--verbose Log simulator lifecycle callbacks to the console
|
|
5797
|
+
|
|
5798
|
+
Positional: path to an ideas .jsonl file — same shape and validation as --simulator.
|
|
5799
|
+
|
|
5800
|
+
The PARAMETER SEARCH counterpart of the --simulator probe: the full grid with the
|
|
5801
|
+
profit-harvesting machinery on — profit lock, trailing take, weighted consensus,
|
|
5802
|
+
Wilson-bound ban, both author-hit metrics (close/reach). Honesty is structural:
|
|
5803
|
+
training sees ONLY the head of the feed (--split of its time range), then the
|
|
5804
|
+
sharpe winner is frozen — point and raw author track record — and fired exactly
|
|
5805
|
+
once on the tail via Simulator.test. Nothing is trained on the tail; authors
|
|
5806
|
+
unseen in training are banned by default. The report carries the train winners,
|
|
5807
|
+
the out-of-sample result with the trade list, and the frozen author track record
|
|
5808
|
+
to hardcode for production Simulator.test.
|
|
5809
|
+
|
|
5810
|
+
Module file ./modules/tune.module is loaded automatically if it exists
|
|
5811
|
+
(register your exchange there); without it CCXT Binance is used by default.
|
|
5812
|
+
|
|
5003
5813
|
Flush flags (--flush):
|
|
5004
5814
|
|
|
5005
5815
|
One or more positional entry points. For each entry point the following
|
|
@@ -5033,7 +5843,8 @@ Module hooks (loaded automatically by each mode):
|
|
|
5033
5843
|
modules/dump.module --dump Exchange schema for candle dumps
|
|
5034
5844
|
modules/pnldebug.module --pnldebug Exchange schema for PnL debug runs
|
|
5035
5845
|
modules/brokerdebug.module --brokerdebug Broker adapter used for broker commit testing
|
|
5036
|
-
modules/simulator.module --simulator Exchange schema for crowd-ideas
|
|
5846
|
+
modules/simulator.module --simulator Exchange schema for the crowd-ideas feasibility probe
|
|
5847
|
+
modules/tune.module --tune Exchange schema for the walk-forward parameter search
|
|
5037
5848
|
|
|
5038
5849
|
--flush has no associated module. It only removes dump subdirectories.
|
|
5039
5850
|
|
|
@@ -5062,8 +5873,10 @@ Examples:
|
|
|
5062
5873
|
node ${ENTRY_PATH} --pnldebug --priceopen 67956.73 --direction long --when 1772064000000 --minutes 60 --markdown
|
|
5063
5874
|
node ${ENTRY_PATH} --brokerdebug --commit signal-open --symbol BTCUSDT
|
|
5064
5875
|
node ${ENTRY_PATH} --brokerdebug --commit partial-profit --symbol ETHUSDT
|
|
5065
|
-
node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/
|
|
5066
|
-
node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output
|
|
5876
|
+
node ${ENTRY_PATH} --simulator --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
|
|
5877
|
+
node ${ENTRY_PATH} --simulator --symbol BTCUSDT --json --output jun_2026_probe ./assets/tv-ideas.normalized.jsonl
|
|
5878
|
+
node ${ENTRY_PATH} --tune --symbol BTCUSDT ./assets/tv-ideas.normalized.jsonl
|
|
5879
|
+
node ${ENTRY_PATH} --tune --symbol BTCUSDT --split 0.7 --markdown --output jun_2026_tune ./assets/tv-ideas.normalized.jsonl
|
|
5067
5880
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts
|
|
5068
5881
|
node ${ENTRY_PATH} --flush ./content/feb_2026.strategy/feb_2026.strategy.ts ./content/feb_2026.strategy/feb_2026.test.ts
|
|
5069
5882
|
node ${ENTRY_PATH} --init --output my-trading-bot
|
|
@@ -5077,7 +5890,7 @@ const main$1 = async () => {
|
|
|
5077
5890
|
if (!values.help) {
|
|
5078
5891
|
return;
|
|
5079
5892
|
}
|
|
5080
|
-
process.stdout.write(`@backtest-kit/cli ${"16.
|
|
5893
|
+
process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n\n`);
|
|
5081
5894
|
process.stdout.write(HELP_TEXT);
|
|
5082
5895
|
process.exit(0);
|
|
5083
5896
|
};
|
|
@@ -5091,7 +5904,7 @@ const main = async () => {
|
|
|
5091
5904
|
if (!values.version) {
|
|
5092
5905
|
return;
|
|
5093
5906
|
}
|
|
5094
|
-
process.stdout.write(`@backtest-kit/cli ${"16.
|
|
5907
|
+
process.stdout.write(`@backtest-kit/cli ${"16.6.0"}\n`);
|
|
5095
5908
|
process.exit(0);
|
|
5096
5909
|
};
|
|
5097
5910
|
main();
|