backtest-kit 1.10.3 → 1.10.4
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/LICENSE +21 -21
- package/README.md +259 -259
- package/build/index.cjs +508 -230
- package/build/index.mjs +508 -230
- package/package.json +81 -81
- package/types.d.ts +308 -91
package/build/index.cjs
CHANGED
|
@@ -2676,193 +2676,167 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
|
|
|
2676
2676
|
slClosed: slClosed + percentToClose,
|
|
2677
2677
|
});
|
|
2678
2678
|
};
|
|
2679
|
-
const
|
|
2680
|
-
//
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
// Calculate new stop-loss distance percentage by adding shift to CURRENT distance
|
|
2679
|
+
const TRAILING_STOP_LOSS_FN = (self, signal, percentShift) => {
|
|
2680
|
+
// CRITICAL: Always calculate from ORIGINAL SL, not from current trailing SL
|
|
2681
|
+
// This prevents error accumulation on repeated calls
|
|
2682
|
+
const originalSlDistancePercent = Math.abs((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen * 100);
|
|
2683
|
+
// Calculate new stop-loss distance percentage by adding shift to ORIGINAL distance
|
|
2685
2684
|
// Negative percentShift: reduces distance % (tightens stop, moves SL toward entry or beyond)
|
|
2686
2685
|
// Positive percentShift: increases distance % (loosens stop, moves SL away from entry)
|
|
2687
|
-
const newSlDistancePercent =
|
|
2686
|
+
const newSlDistancePercent = originalSlDistancePercent + percentShift;
|
|
2688
2687
|
// Calculate new stop-loss price based on new distance percentage
|
|
2689
2688
|
// Negative newSlDistancePercent means SL crosses entry into profit zone
|
|
2690
2689
|
let newStopLoss;
|
|
2691
2690
|
if (signal.position === "long") {
|
|
2692
2691
|
// LONG: SL is below entry (or above entry if in profit zone)
|
|
2693
2692
|
// Formula: entry * (1 - newDistance%)
|
|
2694
|
-
// Example: entry=100,
|
|
2693
|
+
// Example: entry=100, originalSL=90 (10%), shift=-5% → newDistance=5% → 100 * 0.95 = 95 (tighter)
|
|
2695
2694
|
newStopLoss = signal.priceOpen * (1 - newSlDistancePercent / 100);
|
|
2696
2695
|
}
|
|
2697
2696
|
else {
|
|
2698
2697
|
// SHORT: SL is above entry (or below entry if in profit zone)
|
|
2699
2698
|
// Formula: entry * (1 + newDistance%)
|
|
2700
|
-
// Example: entry=100,
|
|
2699
|
+
// Example: entry=100, originalSL=110 (10%), shift=-5% → newDistance=5% → 100 * 1.05 = 105 (tighter)
|
|
2701
2700
|
newStopLoss = signal.priceOpen * (1 + newSlDistancePercent / 100);
|
|
2702
2701
|
}
|
|
2703
|
-
|
|
2704
|
-
const isFirstCall =
|
|
2702
|
+
const currentTrailingSL = signal._trailingPriceStopLoss;
|
|
2703
|
+
const isFirstCall = currentTrailingSL === undefined;
|
|
2705
2704
|
if (isFirstCall) {
|
|
2706
|
-
// First call: set
|
|
2705
|
+
// First call: set trailing SL unconditionally
|
|
2707
2706
|
signal._trailingPriceStopLoss = newStopLoss;
|
|
2708
|
-
self.params.logger.info("TRAILING_STOP_FN executed (first call
|
|
2707
|
+
self.params.logger.info("TRAILING_STOP_FN executed (first call)", {
|
|
2709
2708
|
signalId: signal.id,
|
|
2710
2709
|
position: signal.position,
|
|
2711
2710
|
priceOpen: signal.priceOpen,
|
|
2712
2711
|
originalStopLoss: signal.priceStopLoss,
|
|
2713
|
-
|
|
2714
|
-
previousStopLoss: currentStopLoss,
|
|
2712
|
+
originalDistancePercent: originalSlDistancePercent,
|
|
2715
2713
|
newStopLoss,
|
|
2716
2714
|
newDistancePercent: newSlDistancePercent,
|
|
2717
2715
|
percentShift,
|
|
2718
2716
|
inProfitZone: signal.position === "long" ? newStopLoss > signal.priceOpen : newStopLoss < signal.priceOpen,
|
|
2719
|
-
direction: newStopLoss > currentStopLoss ? "up" : "down",
|
|
2720
2717
|
});
|
|
2718
|
+
return true;
|
|
2721
2719
|
}
|
|
2722
2720
|
else {
|
|
2723
|
-
//
|
|
2724
|
-
//
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
// LONG: closer = SL closer to entry = higher SL value (moving up)
|
|
2728
|
-
initialDirection = signal._trailingPriceStopLoss > signal.priceStopLoss ? "closer" : "farther";
|
|
2729
|
-
}
|
|
2730
|
-
else {
|
|
2731
|
-
// SHORT: closer = SL closer to entry = lower SL value (moving down)
|
|
2732
|
-
initialDirection = signal._trailingPriceStopLoss < signal.priceStopLoss ? "closer" : "farther";
|
|
2733
|
-
}
|
|
2734
|
-
// Determine new direction
|
|
2735
|
-
let newDirection;
|
|
2721
|
+
// CRITICAL: Larger percentShift absorbs smaller one
|
|
2722
|
+
// For LONG: higher SL (closer to entry) absorbs lower one
|
|
2723
|
+
// For SHORT: lower SL (closer to entry) absorbs higher one
|
|
2724
|
+
let shouldUpdate = false;
|
|
2736
2725
|
if (signal.position === "long") {
|
|
2737
|
-
// LONG:
|
|
2738
|
-
|
|
2726
|
+
// LONG: update only if new SL is higher (better protection)
|
|
2727
|
+
shouldUpdate = newStopLoss > currentTrailingSL;
|
|
2739
2728
|
}
|
|
2740
2729
|
else {
|
|
2741
|
-
// SHORT:
|
|
2742
|
-
|
|
2730
|
+
// SHORT: update only if new SL is lower (better protection)
|
|
2731
|
+
shouldUpdate = newStopLoss < currentTrailingSL;
|
|
2743
2732
|
}
|
|
2744
|
-
// Only allow continuation in same direction
|
|
2745
|
-
const shouldUpdate = initialDirection === newDirection;
|
|
2746
2733
|
if (!shouldUpdate) {
|
|
2747
|
-
self.params.logger.debug("TRAILING_STOP_FN: new SL not
|
|
2734
|
+
self.params.logger.debug("TRAILING_STOP_FN: new SL not better than current, skipping", {
|
|
2748
2735
|
signalId: signal.id,
|
|
2749
2736
|
position: signal.position,
|
|
2750
|
-
|
|
2737
|
+
currentTrailingSL,
|
|
2751
2738
|
newStopLoss,
|
|
2752
2739
|
percentShift,
|
|
2753
|
-
|
|
2754
|
-
attemptedDirection: newDirection,
|
|
2740
|
+
reason: "larger percentShift absorbs smaller one",
|
|
2755
2741
|
});
|
|
2756
|
-
return;
|
|
2742
|
+
return false;
|
|
2757
2743
|
}
|
|
2758
2744
|
// Update trailing stop-loss
|
|
2745
|
+
const previousTrailingSL = signal._trailingPriceStopLoss;
|
|
2759
2746
|
signal._trailingPriceStopLoss = newStopLoss;
|
|
2760
2747
|
self.params.logger.info("TRAILING_STOP_FN executed", {
|
|
2761
2748
|
signalId: signal.id,
|
|
2762
2749
|
position: signal.position,
|
|
2763
2750
|
priceOpen: signal.priceOpen,
|
|
2764
2751
|
originalStopLoss: signal.priceStopLoss,
|
|
2765
|
-
|
|
2766
|
-
|
|
2752
|
+
originalDistancePercent: originalSlDistancePercent,
|
|
2753
|
+
previousTrailingSL,
|
|
2767
2754
|
newStopLoss,
|
|
2768
2755
|
newDistancePercent: newSlDistancePercent,
|
|
2769
2756
|
percentShift,
|
|
2770
2757
|
inProfitZone: signal.position === "long" ? newStopLoss > signal.priceOpen : newStopLoss < signal.priceOpen,
|
|
2771
|
-
direction: initialDirection,
|
|
2772
2758
|
});
|
|
2759
|
+
return true;
|
|
2773
2760
|
}
|
|
2774
2761
|
};
|
|
2775
|
-
const
|
|
2776
|
-
//
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
// Calculate new take-profit distance percentage by adding shift to CURRENT distance
|
|
2762
|
+
const TRAILING_TAKE_PROFIT_FN = (self, signal, percentShift) => {
|
|
2763
|
+
// CRITICAL: Always calculate from ORIGINAL TP, not from current trailing TP
|
|
2764
|
+
// This prevents error accumulation on repeated calls
|
|
2765
|
+
const originalTpDistancePercent = Math.abs((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen * 100);
|
|
2766
|
+
// Calculate new take-profit distance percentage by adding shift to ORIGINAL distance
|
|
2781
2767
|
// Negative percentShift: reduces distance % (brings TP closer to entry)
|
|
2782
2768
|
// Positive percentShift: increases distance % (moves TP further from entry)
|
|
2783
|
-
const newTpDistancePercent =
|
|
2769
|
+
const newTpDistancePercent = originalTpDistancePercent + percentShift;
|
|
2784
2770
|
// Calculate new take-profit price based on new distance percentage
|
|
2785
2771
|
let newTakeProfit;
|
|
2786
2772
|
if (signal.position === "long") {
|
|
2787
2773
|
// LONG: TP is above entry
|
|
2788
2774
|
// Formula: entry * (1 + newDistance%)
|
|
2789
|
-
// Example: entry=100,
|
|
2775
|
+
// Example: entry=100, originalTP=110 (10%), shift=-3% → newDistance=7% → 100 * 1.07 = 107 (closer)
|
|
2790
2776
|
newTakeProfit = signal.priceOpen * (1 + newTpDistancePercent / 100);
|
|
2791
2777
|
}
|
|
2792
2778
|
else {
|
|
2793
2779
|
// SHORT: TP is below entry
|
|
2794
2780
|
// Formula: entry * (1 - newDistance%)
|
|
2795
|
-
// Example: entry=100,
|
|
2781
|
+
// Example: entry=100, originalTP=90 (10%), shift=-3% → newDistance=7% → 100 * 0.93 = 93 (closer)
|
|
2796
2782
|
newTakeProfit = signal.priceOpen * (1 - newTpDistancePercent / 100);
|
|
2797
2783
|
}
|
|
2798
|
-
|
|
2799
|
-
const isFirstCall =
|
|
2784
|
+
const currentTrailingTP = signal._trailingPriceTakeProfit;
|
|
2785
|
+
const isFirstCall = currentTrailingTP === undefined;
|
|
2800
2786
|
if (isFirstCall) {
|
|
2801
|
-
// First call: set
|
|
2787
|
+
// First call: set trailing TP unconditionally
|
|
2802
2788
|
signal._trailingPriceTakeProfit = newTakeProfit;
|
|
2803
|
-
self.params.logger.info("TRAILING_PROFIT_FN executed (first call
|
|
2789
|
+
self.params.logger.info("TRAILING_PROFIT_FN executed (first call)", {
|
|
2804
2790
|
signalId: signal.id,
|
|
2805
2791
|
position: signal.position,
|
|
2806
2792
|
priceOpen: signal.priceOpen,
|
|
2807
2793
|
originalTakeProfit: signal.priceTakeProfit,
|
|
2808
|
-
|
|
2809
|
-
previousTakeProfit: currentTakeProfit,
|
|
2794
|
+
originalDistancePercent: originalTpDistancePercent,
|
|
2810
2795
|
newTakeProfit,
|
|
2811
2796
|
newDistancePercent: newTpDistancePercent,
|
|
2812
2797
|
percentShift,
|
|
2813
|
-
direction: newTakeProfit > currentTakeProfit ? "up" : "down",
|
|
2814
2798
|
});
|
|
2799
|
+
return true;
|
|
2815
2800
|
}
|
|
2816
2801
|
else {
|
|
2817
|
-
//
|
|
2818
|
-
//
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
// LONG: closer = TP closer to entry = lower TP value
|
|
2822
|
-
initialDirection = signal._trailingPriceTakeProfit < signal.priceTakeProfit ? "closer" : "farther";
|
|
2823
|
-
}
|
|
2824
|
-
else {
|
|
2825
|
-
// SHORT: closer = TP closer to entry = higher TP value
|
|
2826
|
-
initialDirection = signal._trailingPriceTakeProfit > signal.priceTakeProfit ? "closer" : "farther";
|
|
2827
|
-
}
|
|
2828
|
-
// Determine new direction
|
|
2829
|
-
let newDirection;
|
|
2802
|
+
// CRITICAL: Larger percentShift absorbs smaller one
|
|
2803
|
+
// For LONG: lower TP (closer to entry) absorbs higher one
|
|
2804
|
+
// For SHORT: higher TP (closer to entry) absorbs lower one
|
|
2805
|
+
let shouldUpdate = false;
|
|
2830
2806
|
if (signal.position === "long") {
|
|
2831
|
-
// LONG:
|
|
2832
|
-
|
|
2807
|
+
// LONG: update only if new TP is lower (closer to entry, more conservative)
|
|
2808
|
+
shouldUpdate = newTakeProfit < currentTrailingTP;
|
|
2833
2809
|
}
|
|
2834
2810
|
else {
|
|
2835
|
-
// SHORT:
|
|
2836
|
-
|
|
2811
|
+
// SHORT: update only if new TP is higher (closer to entry, more conservative)
|
|
2812
|
+
shouldUpdate = newTakeProfit > currentTrailingTP;
|
|
2837
2813
|
}
|
|
2838
|
-
// Only allow continuation in same direction
|
|
2839
|
-
const shouldUpdate = initialDirection === newDirection;
|
|
2840
2814
|
if (!shouldUpdate) {
|
|
2841
|
-
self.params.logger.debug("TRAILING_PROFIT_FN: new TP not
|
|
2815
|
+
self.params.logger.debug("TRAILING_PROFIT_FN: new TP not better than current, skipping", {
|
|
2842
2816
|
signalId: signal.id,
|
|
2843
2817
|
position: signal.position,
|
|
2844
|
-
|
|
2818
|
+
currentTrailingTP,
|
|
2845
2819
|
newTakeProfit,
|
|
2846
2820
|
percentShift,
|
|
2847
|
-
|
|
2848
|
-
attemptedDirection: newDirection,
|
|
2821
|
+
reason: "larger percentShift absorbs smaller one",
|
|
2849
2822
|
});
|
|
2850
|
-
return;
|
|
2823
|
+
return false;
|
|
2851
2824
|
}
|
|
2852
2825
|
// Update trailing take-profit
|
|
2826
|
+
const previousTrailingTP = signal._trailingPriceTakeProfit;
|
|
2853
2827
|
signal._trailingPriceTakeProfit = newTakeProfit;
|
|
2854
2828
|
self.params.logger.info("TRAILING_PROFIT_FN executed", {
|
|
2855
2829
|
signalId: signal.id,
|
|
2856
2830
|
position: signal.position,
|
|
2857
2831
|
priceOpen: signal.priceOpen,
|
|
2858
2832
|
originalTakeProfit: signal.priceTakeProfit,
|
|
2859
|
-
|
|
2860
|
-
|
|
2833
|
+
originalDistancePercent: originalTpDistancePercent,
|
|
2834
|
+
previousTrailingTP,
|
|
2861
2835
|
newTakeProfit,
|
|
2862
2836
|
newDistancePercent: newTpDistancePercent,
|
|
2863
2837
|
percentShift,
|
|
2864
|
-
direction: initialDirection,
|
|
2865
2838
|
});
|
|
2839
|
+
return true;
|
|
2866
2840
|
}
|
|
2867
2841
|
};
|
|
2868
2842
|
const BREAKEVEN_FN = (self, signal, currentPrice) => {
|
|
@@ -3212,7 +3186,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_FN = async (self, scheduled, activationTimestamp
|
|
|
3212
3186
|
_isScheduled: false,
|
|
3213
3187
|
};
|
|
3214
3188
|
await self.setPendingSignal(activatedSignal);
|
|
3215
|
-
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activationTime, self.params.execution.context.backtest);
|
|
3189
|
+
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, activationTime, self.params.execution.context.backtest);
|
|
3216
3190
|
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, self._pendingSignal, self._pendingSignal.priceOpen, activationTime, self.params.execution.context.backtest);
|
|
3217
3191
|
const result = {
|
|
3218
3192
|
action: "opened",
|
|
@@ -3412,13 +3386,20 @@ const CALL_IDLE_CALLBACKS_FN = functoolsKit.trycatch(async (self, symbol, curren
|
|
|
3412
3386
|
errorEmitter.next(error);
|
|
3413
3387
|
},
|
|
3414
3388
|
});
|
|
3415
|
-
const CALL_RISK_ADD_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, timestamp, backtest) => {
|
|
3389
|
+
const CALL_RISK_ADD_SIGNAL_FN = functoolsKit.trycatch(async (self, symbol, signal, timestamp, backtest) => {
|
|
3416
3390
|
await ExecutionContextService.runInContext(async () => {
|
|
3417
3391
|
await self.params.risk.addSignal(symbol, {
|
|
3418
3392
|
strategyName: self.params.method.context.strategyName,
|
|
3419
3393
|
riskName: self.params.riskName,
|
|
3420
3394
|
exchangeName: self.params.method.context.exchangeName,
|
|
3421
3395
|
frameName: self.params.method.context.frameName,
|
|
3396
|
+
}, {
|
|
3397
|
+
position: signal.position,
|
|
3398
|
+
priceOpen: signal.priceOpen,
|
|
3399
|
+
priceStopLoss: signal.priceStopLoss,
|
|
3400
|
+
priceTakeProfit: signal.priceTakeProfit,
|
|
3401
|
+
minuteEstimatedTime: signal.minuteEstimatedTime,
|
|
3402
|
+
openTimestamp: timestamp,
|
|
3422
3403
|
});
|
|
3423
3404
|
}, {
|
|
3424
3405
|
when: new Date(timestamp),
|
|
@@ -3652,7 +3633,7 @@ const OPEN_NEW_PENDING_SIGNAL_FN = async (self, signal) => {
|
|
|
3652
3633
|
if (await functoolsKit.not(CALL_RISK_CHECK_SIGNAL_FN(self, self.params.execution.context.symbol, signal, signal.priceOpen, currentTime, self.params.execution.context.backtest))) {
|
|
3653
3634
|
return null;
|
|
3654
3635
|
}
|
|
3655
|
-
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, currentTime, self.params.execution.context.backtest);
|
|
3636
|
+
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, signal, currentTime, self.params.execution.context.backtest);
|
|
3656
3637
|
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, signal.priceOpen, currentTime, self.params.execution.context.backtest);
|
|
3657
3638
|
const result = {
|
|
3658
3639
|
action: "opened",
|
|
@@ -3880,7 +3861,7 @@ const ACTIVATE_SCHEDULED_SIGNAL_IN_BACKTEST_FN = async (self, scheduled, activat
|
|
|
3880
3861
|
_isScheduled: false,
|
|
3881
3862
|
};
|
|
3882
3863
|
await self.setPendingSignal(activatedSignal);
|
|
3883
|
-
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activationTime, self.params.execution.context.backtest);
|
|
3864
|
+
await CALL_RISK_ADD_SIGNAL_FN(self, self.params.execution.context.symbol, activatedSignal, activationTime, self.params.execution.context.backtest);
|
|
3884
3865
|
await CALL_OPEN_CALLBACKS_FN(self, self.params.execution.context.symbol, activatedSignal, activatedSignal.priceOpen, activationTime, self.params.execution.context.backtest);
|
|
3885
3866
|
return true;
|
|
3886
3867
|
};
|
|
@@ -4783,6 +4764,28 @@ class ClientStrategy {
|
|
|
4783
4764
|
throw new Error(`ClientStrategy partialProfit: For SHORT position, currentPrice (${currentPrice}) must be < priceOpen (${this._pendingSignal.priceOpen})`);
|
|
4784
4765
|
}
|
|
4785
4766
|
}
|
|
4767
|
+
// Check if currentPrice already crossed take profit level
|
|
4768
|
+
const effectiveTakeProfit = this._pendingSignal._trailingPriceTakeProfit ?? this._pendingSignal.priceTakeProfit;
|
|
4769
|
+
if (this._pendingSignal.position === "long" && currentPrice >= effectiveTakeProfit) {
|
|
4770
|
+
this.params.logger.debug("ClientStrategy partialProfit: price already at/above TP, skipping partial close", {
|
|
4771
|
+
signalId: this._pendingSignal.id,
|
|
4772
|
+
position: this._pendingSignal.position,
|
|
4773
|
+
currentPrice,
|
|
4774
|
+
effectiveTakeProfit,
|
|
4775
|
+
reason: "currentPrice >= effectiveTakeProfit (LONG position)"
|
|
4776
|
+
});
|
|
4777
|
+
return;
|
|
4778
|
+
}
|
|
4779
|
+
if (this._pendingSignal.position === "short" && currentPrice <= effectiveTakeProfit) {
|
|
4780
|
+
this.params.logger.debug("ClientStrategy partialProfit: price already at/below TP, skipping partial close", {
|
|
4781
|
+
signalId: this._pendingSignal.id,
|
|
4782
|
+
position: this._pendingSignal.position,
|
|
4783
|
+
currentPrice,
|
|
4784
|
+
effectiveTakeProfit,
|
|
4785
|
+
reason: "currentPrice <= effectiveTakeProfit (SHORT position)"
|
|
4786
|
+
});
|
|
4787
|
+
return;
|
|
4788
|
+
}
|
|
4786
4789
|
// Execute partial close logic
|
|
4787
4790
|
PARTIAL_PROFIT_FN(this, this._pendingSignal, percentToClose, currentPrice);
|
|
4788
4791
|
// Persist updated signal state (inline setPendingSignal content)
|
|
@@ -4875,6 +4878,28 @@ class ClientStrategy {
|
|
|
4875
4878
|
throw new Error(`ClientStrategy partialLoss: For SHORT position, currentPrice (${currentPrice}) must be > priceOpen (${this._pendingSignal.priceOpen})`);
|
|
4876
4879
|
}
|
|
4877
4880
|
}
|
|
4881
|
+
// Check if currentPrice already crossed stop loss level
|
|
4882
|
+
const effectiveStopLoss = this._pendingSignal._trailingPriceStopLoss ?? this._pendingSignal.priceStopLoss;
|
|
4883
|
+
if (this._pendingSignal.position === "long" && currentPrice <= effectiveStopLoss) {
|
|
4884
|
+
this.params.logger.debug("ClientStrategy partialLoss: price already at/below SL, skipping partial close", {
|
|
4885
|
+
signalId: this._pendingSignal.id,
|
|
4886
|
+
position: this._pendingSignal.position,
|
|
4887
|
+
currentPrice,
|
|
4888
|
+
effectiveStopLoss,
|
|
4889
|
+
reason: "currentPrice <= effectiveStopLoss (LONG position)"
|
|
4890
|
+
});
|
|
4891
|
+
return;
|
|
4892
|
+
}
|
|
4893
|
+
if (this._pendingSignal.position === "short" && currentPrice >= effectiveStopLoss) {
|
|
4894
|
+
this.params.logger.debug("ClientStrategy partialLoss: price already at/above SL, skipping partial close", {
|
|
4895
|
+
signalId: this._pendingSignal.id,
|
|
4896
|
+
position: this._pendingSignal.position,
|
|
4897
|
+
currentPrice,
|
|
4898
|
+
effectiveStopLoss,
|
|
4899
|
+
reason: "currentPrice >= effectiveStopLoss (SHORT position)"
|
|
4900
|
+
});
|
|
4901
|
+
return;
|
|
4902
|
+
}
|
|
4878
4903
|
// Execute partial close logic
|
|
4879
4904
|
PARTIAL_LOSS_FN(this, this._pendingSignal, percentToClose, currentPrice);
|
|
4880
4905
|
// Persist updated signal state (inline setPendingSignal content)
|
|
@@ -4956,6 +4981,34 @@ class ClientStrategy {
|
|
|
4956
4981
|
if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
|
|
4957
4982
|
throw new Error(`ClientStrategy breakeven: currentPrice must be a positive finite number, got ${currentPrice}`);
|
|
4958
4983
|
}
|
|
4984
|
+
// Check for conflict with existing trailing take profit
|
|
4985
|
+
const signal = this._pendingSignal;
|
|
4986
|
+
const breakevenPrice = signal.priceOpen;
|
|
4987
|
+
const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
|
|
4988
|
+
if (signal.position === "long" && breakevenPrice >= effectiveTakeProfit) {
|
|
4989
|
+
// LONG: Breakeven SL would be at or above current TP - invalid configuration
|
|
4990
|
+
this.params.logger.debug("ClientStrategy breakeven: SL/TP conflict detected, skipping breakeven", {
|
|
4991
|
+
signalId: signal.id,
|
|
4992
|
+
position: signal.position,
|
|
4993
|
+
priceOpen: signal.priceOpen,
|
|
4994
|
+
breakevenPrice,
|
|
4995
|
+
effectiveTakeProfit,
|
|
4996
|
+
reason: "breakevenPrice >= effectiveTakeProfit (LONG position)"
|
|
4997
|
+
});
|
|
4998
|
+
return false;
|
|
4999
|
+
}
|
|
5000
|
+
if (signal.position === "short" && breakevenPrice <= effectiveTakeProfit) {
|
|
5001
|
+
// SHORT: Breakeven SL would be at or below current TP - invalid configuration
|
|
5002
|
+
this.params.logger.debug("ClientStrategy breakeven: SL/TP conflict detected, skipping breakeven", {
|
|
5003
|
+
signalId: signal.id,
|
|
5004
|
+
position: signal.position,
|
|
5005
|
+
priceOpen: signal.priceOpen,
|
|
5006
|
+
breakevenPrice,
|
|
5007
|
+
effectiveTakeProfit,
|
|
5008
|
+
reason: "breakevenPrice <= effectiveTakeProfit (SHORT position)"
|
|
5009
|
+
});
|
|
5010
|
+
return false;
|
|
5011
|
+
}
|
|
4959
5012
|
// Execute breakeven logic
|
|
4960
5013
|
const result = BREAKEVEN_FN(this, this._pendingSignal, currentPrice);
|
|
4961
5014
|
// Only persist if breakeven was actually set
|
|
@@ -4980,24 +5033,35 @@ class ClientStrategy {
|
|
|
4980
5033
|
/**
|
|
4981
5034
|
* Adjusts trailing stop-loss by shifting distance between entry and original SL.
|
|
4982
5035
|
*
|
|
4983
|
-
*
|
|
5036
|
+
* CRITICAL: Always calculates from ORIGINAL SL, not from current trailing SL.
|
|
5037
|
+
* This prevents error accumulation on repeated calls.
|
|
5038
|
+
* Larger percentShift ABSORBS smaller one (updates only towards better protection).
|
|
5039
|
+
*
|
|
5040
|
+
* Calculates new SL based on percentage shift of the ORIGINAL distance (entry - originalSL):
|
|
4984
5041
|
* - Negative %: tightens stop (moves SL closer to entry, reduces risk)
|
|
4985
5042
|
* - Positive %: loosens stop (moves SL away from entry, allows more drawdown)
|
|
4986
5043
|
*
|
|
4987
|
-
* For LONG position (entry=100, originalSL=90, distance=10):
|
|
4988
|
-
* - percentShift = -50: newSL = 100 - 10
|
|
4989
|
-
* - percentShift = +20: newSL = 100 - 10
|
|
5044
|
+
* For LONG position (entry=100, originalSL=90, distance=10%):
|
|
5045
|
+
* - percentShift = -50: newSL = 100 - 10%*(1-0.5) = 95 (5% distance, tighter)
|
|
5046
|
+
* - percentShift = +20: newSL = 100 - 10%*(1+0.2) = 88 (12% distance, looser)
|
|
5047
|
+
*
|
|
5048
|
+
* For SHORT position (entry=100, originalSL=110, distance=10%):
|
|
5049
|
+
* - percentShift = -50: newSL = 100 + 10%*(1-0.5) = 105 (5% distance, tighter)
|
|
5050
|
+
* - percentShift = +20: newSL = 100 + 10%*(1+0.2) = 112 (12% distance, looser)
|
|
4990
5051
|
*
|
|
4991
|
-
*
|
|
4992
|
-
* -
|
|
4993
|
-
* -
|
|
5052
|
+
* Trailing behavior (absorption):
|
|
5053
|
+
* - First call: sets trailing SL unconditionally
|
|
5054
|
+
* - Subsequent calls: updates only if new SL is BETTER (protects more profit)
|
|
5055
|
+
* - For LONG: only accepts HIGHER SL (never moves down, closer to entry wins)
|
|
5056
|
+
* - For SHORT: only accepts LOWER SL (never moves up, closer to entry wins)
|
|
5057
|
+
* - Stores in _trailingPriceStopLoss, original priceStopLoss always preserved
|
|
4994
5058
|
*
|
|
4995
|
-
*
|
|
4996
|
-
*
|
|
4997
|
-
* -
|
|
4998
|
-
* -
|
|
4999
|
-
* -
|
|
5000
|
-
*
|
|
5059
|
+
* Example of absorption (LONG, entry=100, originalSL=90):
|
|
5060
|
+
* ```typescript
|
|
5061
|
+
* await trailingStop(-50, price); // Sets SL=95 (first call)
|
|
5062
|
+
* await trailingStop(-30, price); // SKIPPED: SL=97 < 95 (worse, not absorbed)
|
|
5063
|
+
* await trailingStop(-70, price); // Sets SL=97 (better, absorbs previous)
|
|
5064
|
+
* ```
|
|
5001
5065
|
*
|
|
5002
5066
|
* Validation:
|
|
5003
5067
|
* - Throws if no pending signal exists
|
|
@@ -5007,28 +5071,34 @@ class ClientStrategy {
|
|
|
5007
5071
|
* - Throws if currentPrice is not a positive finite number
|
|
5008
5072
|
* - Skips if new SL would cross entry price
|
|
5009
5073
|
* - Skips if currentPrice already crossed new SL level (price intrusion protection)
|
|
5074
|
+
* - Skips if new SL conflicts with existing trailing take-profit
|
|
5010
5075
|
*
|
|
5011
5076
|
* @param symbol - Trading pair symbol (e.g., "BTCUSDT")
|
|
5012
|
-
* @param percentShift - Percentage shift of SL distance [-100, 100], excluding 0
|
|
5077
|
+
* @param percentShift - Percentage shift of ORIGINAL SL distance [-100, 100], excluding 0
|
|
5013
5078
|
* @param currentPrice - Current market price to check for intrusion
|
|
5014
5079
|
* @param backtest - Whether running in backtest mode (controls persistence)
|
|
5015
|
-
* @returns Promise
|
|
5080
|
+
* @returns Promise<boolean> - true if trailing SL was set/updated, false if rejected (absorption/intrusion/conflict)
|
|
5016
5081
|
*
|
|
5017
5082
|
* @example
|
|
5018
5083
|
* ```typescript
|
|
5019
5084
|
* // LONG position: entry=100, originalSL=90, distance=10%, currentPrice=102
|
|
5020
5085
|
*
|
|
5021
5086
|
* // Move SL 50% closer to entry (tighten): reduces distance by 50%
|
|
5022
|
-
* await strategy.trailingStop("BTCUSDT", -50, 102, false);
|
|
5023
|
-
* // newDistance = 10% - 50% = 5%, newSL = 100 * (1 - 0.05) = 95
|
|
5087
|
+
* const success1 = await strategy.trailingStop("BTCUSDT", -50, 102, false);
|
|
5088
|
+
* // success1 = true, newDistance = 10% - 50% = 5%, newSL = 100 * (1 - 0.05) = 95
|
|
5024
5089
|
*
|
|
5025
|
-
* //
|
|
5026
|
-
* await strategy.trailingStop("BTCUSDT", 30, 102, false);
|
|
5027
|
-
* //
|
|
5090
|
+
* // Try to move SL only 30% closer (less aggressive)
|
|
5091
|
+
* const success2 = await strategy.trailingStop("BTCUSDT", -30, 102, false);
|
|
5092
|
+
* // success2 = false (SKIPPED: newSL=97 < 95, worse protection, larger % absorbs smaller)
|
|
5093
|
+
*
|
|
5094
|
+
* // Move SL 70% closer to entry (more aggressive)
|
|
5095
|
+
* const success3 = await strategy.trailingStop("BTCUSDT", -70, 102, false);
|
|
5096
|
+
* // success3 = true, newDistance = 10% - 70% = 3%, newSL = 100 * (1 - 0.03) = 97
|
|
5097
|
+
* // Updated! SL=97 > 95 (better protection)
|
|
5028
5098
|
*
|
|
5029
5099
|
* // Price intrusion example: currentPrice=92, trying to set SL=95
|
|
5030
|
-
* await strategy.trailingStop("BTCUSDT", -50, 92, false);
|
|
5031
|
-
* // SKIPPED: currentPrice (92) < newSL (95) - would trigger immediate stop
|
|
5100
|
+
* const success4 = await strategy.trailingStop("BTCUSDT", -50, 92, false);
|
|
5101
|
+
* // success4 = false (SKIPPED: currentPrice (92) < newSL (95) - would trigger immediate stop)
|
|
5032
5102
|
* ```
|
|
5033
5103
|
*/
|
|
5034
5104
|
async trailingStop(symbol, percentShift, currentPrice, backtest) {
|
|
@@ -5078,7 +5148,7 @@ class ClientStrategy {
|
|
|
5078
5148
|
currentPrice,
|
|
5079
5149
|
reason: "currentPrice below newStopLoss (LONG position)"
|
|
5080
5150
|
});
|
|
5081
|
-
return;
|
|
5151
|
+
return false;
|
|
5082
5152
|
}
|
|
5083
5153
|
if (signal.position === "short" && currentPrice > newStopLoss) {
|
|
5084
5154
|
// SHORT: Price already crossed the new stop loss level - skip setting SL
|
|
@@ -5090,7 +5160,7 @@ class ClientStrategy {
|
|
|
5090
5160
|
currentPrice,
|
|
5091
5161
|
reason: "currentPrice above newStopLoss (SHORT position)"
|
|
5092
5162
|
});
|
|
5093
|
-
return;
|
|
5163
|
+
return false;
|
|
5094
5164
|
}
|
|
5095
5165
|
// Check for conflict with existing trailing take profit
|
|
5096
5166
|
const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
|
|
@@ -5104,7 +5174,7 @@ class ClientStrategy {
|
|
|
5104
5174
|
effectiveTakeProfit,
|
|
5105
5175
|
reason: "newStopLoss >= effectiveTakeProfit (LONG position)"
|
|
5106
5176
|
});
|
|
5107
|
-
return;
|
|
5177
|
+
return false;
|
|
5108
5178
|
}
|
|
5109
5179
|
if (signal.position === "short" && newStopLoss <= effectiveTakeProfit) {
|
|
5110
5180
|
// SHORT: New SL would be at or below current TP - invalid configuration
|
|
@@ -5116,10 +5186,14 @@ class ClientStrategy {
|
|
|
5116
5186
|
effectiveTakeProfit,
|
|
5117
5187
|
reason: "newStopLoss <= effectiveTakeProfit (SHORT position)"
|
|
5118
5188
|
});
|
|
5119
|
-
return;
|
|
5189
|
+
return false;
|
|
5190
|
+
}
|
|
5191
|
+
// Execute trailing logic and get result
|
|
5192
|
+
const wasUpdated = TRAILING_STOP_LOSS_FN(this, this._pendingSignal, percentShift);
|
|
5193
|
+
// If trailing was not updated (absorption rejected), return false without persistence
|
|
5194
|
+
if (!wasUpdated) {
|
|
5195
|
+
return false;
|
|
5120
5196
|
}
|
|
5121
|
-
// Execute trailing logic
|
|
5122
|
-
TRAILING_STOP_FN(this, this._pendingSignal, percentShift);
|
|
5123
5197
|
// Persist updated signal state (inline setPendingSignal content)
|
|
5124
5198
|
// Note: this._pendingSignal already mutated by TRAILING_STOP_FN, no reassignment needed
|
|
5125
5199
|
this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
|
|
@@ -5133,36 +5207,67 @@ class ClientStrategy {
|
|
|
5133
5207
|
if (!backtest) {
|
|
5134
5208
|
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
|
|
5135
5209
|
}
|
|
5210
|
+
return true;
|
|
5136
5211
|
}
|
|
5137
5212
|
/**
|
|
5138
5213
|
* Adjusts the trailing take-profit distance for an active pending signal.
|
|
5139
5214
|
*
|
|
5140
|
-
*
|
|
5141
|
-
*
|
|
5142
|
-
*
|
|
5215
|
+
* CRITICAL: Always calculates from ORIGINAL TP, not from current trailing TP.
|
|
5216
|
+
* This prevents error accumulation on repeated calls.
|
|
5217
|
+
* Larger percentShift ABSORBS smaller one (updates only towards more conservative TP).
|
|
5218
|
+
*
|
|
5219
|
+
* Updates the take-profit distance by a percentage adjustment relative to the ORIGINAL TP distance.
|
|
5220
|
+
* Negative percentShift brings TP closer to entry (more conservative).
|
|
5221
|
+
* Positive percentShift moves TP further from entry (more aggressive).
|
|
5222
|
+
*
|
|
5223
|
+
* Trailing behavior (absorption):
|
|
5224
|
+
* - First call: sets trailing TP unconditionally
|
|
5225
|
+
* - Subsequent calls: updates only if new TP is MORE CONSERVATIVE (closer to entry)
|
|
5226
|
+
* - For LONG: only accepts LOWER TP (never moves up, closer to entry wins)
|
|
5227
|
+
* - For SHORT: only accepts HIGHER TP (never moves down, closer to entry wins)
|
|
5228
|
+
* - Stores in _trailingPriceTakeProfit, original priceTakeProfit always preserved
|
|
5229
|
+
*
|
|
5230
|
+
* Example of absorption (LONG, entry=100, originalTP=110):
|
|
5231
|
+
* ```typescript
|
|
5232
|
+
* await trailingTake(-30, price); // Sets TP=107 (first call, 7% distance)
|
|
5233
|
+
* await trailingTake(+20, price); // SKIPPED: TP=112 > 107 (less conservative)
|
|
5234
|
+
* await trailingTake(-50, price); // Sets TP=105 (more conservative, absorbs previous)
|
|
5235
|
+
* ```
|
|
5143
5236
|
*
|
|
5144
5237
|
* Price intrusion protection: If current price has already crossed the new TP level,
|
|
5145
5238
|
* the update is skipped to prevent immediate TP triggering.
|
|
5146
5239
|
*
|
|
5147
5240
|
* @param symbol - Trading pair symbol
|
|
5148
|
-
* @param percentShift - Percentage adjustment to TP distance (-100 to 100)
|
|
5241
|
+
* @param percentShift - Percentage adjustment to ORIGINAL TP distance (-100 to 100)
|
|
5149
5242
|
* @param currentPrice - Current market price to check for intrusion
|
|
5150
5243
|
* @param backtest - Whether running in backtest mode
|
|
5151
|
-
* @returns Promise
|
|
5244
|
+
* @returns Promise<boolean> - true if trailing TP was set/updated, false if rejected (absorption/intrusion/conflict)
|
|
5152
5245
|
*
|
|
5153
5246
|
* @example
|
|
5154
5247
|
* ```typescript
|
|
5155
5248
|
* // LONG: entry=100, originalTP=110, distance=10%, currentPrice=102
|
|
5156
|
-
*
|
|
5157
|
-
*
|
|
5249
|
+
*
|
|
5250
|
+
* // Move TP closer by 30% (more conservative)
|
|
5251
|
+
* const success1 = await strategy.trailingTake("BTCUSDT", -30, 102, false);
|
|
5252
|
+
* // success1 = true, newDistance = 10% - 30% = 7%, newTP = 100 * (1 + 0.07) = 107
|
|
5253
|
+
*
|
|
5254
|
+
* // Try to move TP further by 20% (less conservative)
|
|
5255
|
+
* const success2 = await strategy.trailingTake("BTCUSDT", 20, 102, false);
|
|
5256
|
+
* // success2 = false (SKIPPED: newTP=112 > 107, less conservative, larger % absorbs smaller)
|
|
5257
|
+
*
|
|
5258
|
+
* // Move TP even closer by 50% (most conservative)
|
|
5259
|
+
* const success3 = await strategy.trailingTake("BTCUSDT", -50, 102, false);
|
|
5260
|
+
* // success3 = true, newDistance = 10% - 50% = 5%, newTP = 100 * (1 + 0.05) = 105
|
|
5261
|
+
* // Updated! TP=105 < 107 (more conservative)
|
|
5158
5262
|
*
|
|
5159
5263
|
* // SHORT: entry=100, originalTP=90, distance=10%, currentPrice=98
|
|
5160
5264
|
* // Move TP closer by 30%: newTP = 100 - 7% = 93
|
|
5161
|
-
* await strategy.
|
|
5265
|
+
* const success4 = await strategy.trailingTake("BTCUSDT", -30, 98, false);
|
|
5266
|
+
* // success4 = true
|
|
5162
5267
|
* ```
|
|
5163
5268
|
*/
|
|
5164
|
-
async
|
|
5165
|
-
this.params.logger.debug("ClientStrategy
|
|
5269
|
+
async trailingTake(symbol, percentShift, currentPrice, backtest) {
|
|
5270
|
+
this.params.logger.debug("ClientStrategy trailingTake", {
|
|
5166
5271
|
symbol,
|
|
5167
5272
|
percentShift,
|
|
5168
5273
|
currentPrice,
|
|
@@ -5170,21 +5275,21 @@ class ClientStrategy {
|
|
|
5170
5275
|
});
|
|
5171
5276
|
// Validation: must have pending signal
|
|
5172
5277
|
if (!this._pendingSignal) {
|
|
5173
|
-
throw new Error(`ClientStrategy
|
|
5278
|
+
throw new Error(`ClientStrategy trailingTake: No pending signal exists for symbol=${symbol}`);
|
|
5174
5279
|
}
|
|
5175
5280
|
// Validation: percentShift must be valid
|
|
5176
5281
|
if (typeof percentShift !== "number" || !isFinite(percentShift)) {
|
|
5177
|
-
throw new Error(`ClientStrategy
|
|
5282
|
+
throw new Error(`ClientStrategy trailingTake: percentShift must be a finite number, got ${percentShift} (${typeof percentShift})`);
|
|
5178
5283
|
}
|
|
5179
5284
|
if (percentShift < -100 || percentShift > 100) {
|
|
5180
|
-
throw new Error(`ClientStrategy
|
|
5285
|
+
throw new Error(`ClientStrategy trailingTake: percentShift must be in range [-100, 100], got ${percentShift}`);
|
|
5181
5286
|
}
|
|
5182
5287
|
if (percentShift === 0) {
|
|
5183
|
-
throw new Error(`ClientStrategy
|
|
5288
|
+
throw new Error(`ClientStrategy trailingTake: percentShift cannot be 0`);
|
|
5184
5289
|
}
|
|
5185
5290
|
// Validation: currentPrice must be valid
|
|
5186
5291
|
if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
|
|
5187
|
-
throw new Error(`ClientStrategy
|
|
5292
|
+
throw new Error(`ClientStrategy trailingTake: currentPrice must be a positive finite number, got ${currentPrice}`);
|
|
5188
5293
|
}
|
|
5189
5294
|
// Calculate what the new take profit would be
|
|
5190
5295
|
const signal = this._pendingSignal;
|
|
@@ -5200,7 +5305,7 @@ class ClientStrategy {
|
|
|
5200
5305
|
// Check for price intrusion before executing trailing logic
|
|
5201
5306
|
if (signal.position === "long" && currentPrice > newTakeProfit) {
|
|
5202
5307
|
// LONG: Price already crossed the new take profit level - skip setting TP
|
|
5203
|
-
this.params.logger.debug("ClientStrategy
|
|
5308
|
+
this.params.logger.debug("ClientStrategy trailingTake: price intrusion detected, skipping TP update", {
|
|
5204
5309
|
signalId: signal.id,
|
|
5205
5310
|
position: signal.position,
|
|
5206
5311
|
priceOpen: signal.priceOpen,
|
|
@@ -5208,11 +5313,11 @@ class ClientStrategy {
|
|
|
5208
5313
|
currentPrice,
|
|
5209
5314
|
reason: "currentPrice above newTakeProfit (LONG position)"
|
|
5210
5315
|
});
|
|
5211
|
-
return;
|
|
5316
|
+
return false;
|
|
5212
5317
|
}
|
|
5213
5318
|
if (signal.position === "short" && currentPrice < newTakeProfit) {
|
|
5214
5319
|
// SHORT: Price already crossed the new take profit level - skip setting TP
|
|
5215
|
-
this.params.logger.debug("ClientStrategy
|
|
5320
|
+
this.params.logger.debug("ClientStrategy trailingTake: price intrusion detected, skipping TP update", {
|
|
5216
5321
|
signalId: signal.id,
|
|
5217
5322
|
position: signal.position,
|
|
5218
5323
|
priceOpen: signal.priceOpen,
|
|
@@ -5220,13 +5325,13 @@ class ClientStrategy {
|
|
|
5220
5325
|
currentPrice,
|
|
5221
5326
|
reason: "currentPrice below newTakeProfit (SHORT position)"
|
|
5222
5327
|
});
|
|
5223
|
-
return;
|
|
5328
|
+
return false;
|
|
5224
5329
|
}
|
|
5225
5330
|
// Check for conflict with existing trailing stop loss
|
|
5226
5331
|
const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
|
|
5227
5332
|
if (signal.position === "long" && newTakeProfit <= effectiveStopLoss) {
|
|
5228
5333
|
// LONG: New TP would be at or below current SL - invalid configuration
|
|
5229
|
-
this.params.logger.debug("ClientStrategy
|
|
5334
|
+
this.params.logger.debug("ClientStrategy trailingTake: TP/SL conflict detected, skipping TP update", {
|
|
5230
5335
|
signalId: signal.id,
|
|
5231
5336
|
position: signal.position,
|
|
5232
5337
|
priceOpen: signal.priceOpen,
|
|
@@ -5234,11 +5339,11 @@ class ClientStrategy {
|
|
|
5234
5339
|
effectiveStopLoss,
|
|
5235
5340
|
reason: "newTakeProfit <= effectiveStopLoss (LONG position)"
|
|
5236
5341
|
});
|
|
5237
|
-
return;
|
|
5342
|
+
return false;
|
|
5238
5343
|
}
|
|
5239
5344
|
if (signal.position === "short" && newTakeProfit >= effectiveStopLoss) {
|
|
5240
5345
|
// SHORT: New TP would be at or above current SL - invalid configuration
|
|
5241
|
-
this.params.logger.debug("ClientStrategy
|
|
5346
|
+
this.params.logger.debug("ClientStrategy trailingTake: TP/SL conflict detected, skipping TP update", {
|
|
5242
5347
|
signalId: signal.id,
|
|
5243
5348
|
position: signal.position,
|
|
5244
5349
|
priceOpen: signal.priceOpen,
|
|
@@ -5246,10 +5351,14 @@ class ClientStrategy {
|
|
|
5246
5351
|
effectiveStopLoss,
|
|
5247
5352
|
reason: "newTakeProfit >= effectiveStopLoss (SHORT position)"
|
|
5248
5353
|
});
|
|
5249
|
-
return;
|
|
5354
|
+
return false;
|
|
5355
|
+
}
|
|
5356
|
+
// Execute trailing logic and get result
|
|
5357
|
+
const wasUpdated = TRAILING_TAKE_PROFIT_FN(this, this._pendingSignal, percentShift);
|
|
5358
|
+
// If trailing was not updated (absorption rejected), return false without persistence
|
|
5359
|
+
if (!wasUpdated) {
|
|
5360
|
+
return false;
|
|
5250
5361
|
}
|
|
5251
|
-
// Execute trailing logic
|
|
5252
|
-
TRAILING_PROFIT_FN(this, this._pendingSignal, percentShift);
|
|
5253
5362
|
// Persist updated signal state (inline setPendingSignal content)
|
|
5254
5363
|
// Note: this._pendingSignal already mutated by TRAILING_PROFIT_FN, no reassignment needed
|
|
5255
5364
|
this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
|
|
@@ -5263,6 +5372,7 @@ class ClientStrategy {
|
|
|
5263
5372
|
if (!backtest) {
|
|
5264
5373
|
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
|
|
5265
5374
|
}
|
|
5375
|
+
return true;
|
|
5266
5376
|
}
|
|
5267
5377
|
}
|
|
5268
5378
|
|
|
@@ -5341,12 +5451,12 @@ class MergeRisk {
|
|
|
5341
5451
|
* @param context - Context with strategyName, riskName, exchangeName and frameName
|
|
5342
5452
|
* @returns Promise that resolves when all risks have registered the signal
|
|
5343
5453
|
*/
|
|
5344
|
-
async addSignal(symbol, context) {
|
|
5454
|
+
async addSignal(symbol, context, positionData) {
|
|
5345
5455
|
bt.loggerService.info("MergeRisk addSignal", {
|
|
5346
5456
|
symbol,
|
|
5347
5457
|
context,
|
|
5348
5458
|
});
|
|
5349
|
-
await Promise.all(this._riskList.map(async (risk) => await risk.addSignal(symbol, context)));
|
|
5459
|
+
await Promise.all(this._riskList.map(async (risk) => await risk.addSignal(symbol, context, positionData)));
|
|
5350
5460
|
}
|
|
5351
5461
|
/**
|
|
5352
5462
|
* Removes a signal from all child risk profiles.
|
|
@@ -5434,6 +5544,7 @@ class RiskUtils {
|
|
|
5434
5544
|
});
|
|
5435
5545
|
bt.strategyValidationService.validate(context.strategyName, RISK_METHOD_NAME_GET_DATA);
|
|
5436
5546
|
bt.exchangeValidationService.validate(context.exchangeName, RISK_METHOD_NAME_GET_DATA);
|
|
5547
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, RISK_METHOD_NAME_GET_DATA);
|
|
5437
5548
|
{
|
|
5438
5549
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
5439
5550
|
riskName &&
|
|
@@ -5491,6 +5602,7 @@ class RiskUtils {
|
|
|
5491
5602
|
});
|
|
5492
5603
|
bt.strategyValidationService.validate(context.strategyName, RISK_METHOD_NAME_GET_REPORT);
|
|
5493
5604
|
bt.exchangeValidationService.validate(context.exchangeName, RISK_METHOD_NAME_GET_REPORT);
|
|
5605
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, RISK_METHOD_NAME_GET_REPORT);
|
|
5494
5606
|
{
|
|
5495
5607
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
5496
5608
|
riskName &&
|
|
@@ -5540,6 +5652,7 @@ class RiskUtils {
|
|
|
5540
5652
|
});
|
|
5541
5653
|
bt.strategyValidationService.validate(context.strategyName, RISK_METHOD_NAME_DUMP);
|
|
5542
5654
|
bt.exchangeValidationService.validate(context.exchangeName, RISK_METHOD_NAME_DUMP);
|
|
5655
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, RISK_METHOD_NAME_DUMP);
|
|
5543
5656
|
{
|
|
5544
5657
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
5545
5658
|
riskName &&
|
|
@@ -6050,7 +6163,7 @@ class StrategyConnectionService {
|
|
|
6050
6163
|
backtest,
|
|
6051
6164
|
});
|
|
6052
6165
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
6053
|
-
await strategy.trailingStop(symbol, percentShift, currentPrice, backtest);
|
|
6166
|
+
return await strategy.trailingStop(symbol, percentShift, currentPrice, backtest);
|
|
6054
6167
|
};
|
|
6055
6168
|
/**
|
|
6056
6169
|
* Adjusts the trailing take-profit distance for an active pending signal.
|
|
@@ -6058,7 +6171,7 @@ class StrategyConnectionService {
|
|
|
6058
6171
|
* Updates the take-profit distance by a percentage adjustment relative to the original TP distance.
|
|
6059
6172
|
* Negative percentShift brings TP closer to entry, positive percentShift moves it further.
|
|
6060
6173
|
*
|
|
6061
|
-
* Delegates to ClientStrategy.
|
|
6174
|
+
* Delegates to ClientStrategy.trailingTake() with current execution context.
|
|
6062
6175
|
*
|
|
6063
6176
|
* @param backtest - Whether running in backtest mode
|
|
6064
6177
|
* @param symbol - Trading pair symbol
|
|
@@ -6071,7 +6184,7 @@ class StrategyConnectionService {
|
|
|
6071
6184
|
* ```typescript
|
|
6072
6185
|
* // LONG: entry=100, originalTP=110, distance=10%, currentPrice=102
|
|
6073
6186
|
* // Move TP further by 50%: newTP = 100 + 15% = 115
|
|
6074
|
-
* await strategyConnectionService.
|
|
6187
|
+
* await strategyConnectionService.trailingTake(
|
|
6075
6188
|
* false,
|
|
6076
6189
|
* "BTCUSDT",
|
|
6077
6190
|
* 50,
|
|
@@ -6080,8 +6193,8 @@ class StrategyConnectionService {
|
|
|
6080
6193
|
* );
|
|
6081
6194
|
* ```
|
|
6082
6195
|
*/
|
|
6083
|
-
this.
|
|
6084
|
-
this.loggerService.log("strategyConnectionService
|
|
6196
|
+
this.trailingTake = async (backtest, symbol, percentShift, currentPrice, context) => {
|
|
6197
|
+
this.loggerService.log("strategyConnectionService trailingTake", {
|
|
6085
6198
|
symbol,
|
|
6086
6199
|
context,
|
|
6087
6200
|
percentShift,
|
|
@@ -6089,7 +6202,7 @@ class StrategyConnectionService {
|
|
|
6089
6202
|
backtest,
|
|
6090
6203
|
});
|
|
6091
6204
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
6092
|
-
await strategy.
|
|
6205
|
+
return await strategy.trailingTake(symbol, percentShift, currentPrice, backtest);
|
|
6093
6206
|
};
|
|
6094
6207
|
/**
|
|
6095
6208
|
* Delegates to ClientStrategy.breakeven() with current execution context.
|
|
@@ -6783,10 +6896,11 @@ class ClientRisk {
|
|
|
6783
6896
|
* Registers a new opened signal.
|
|
6784
6897
|
* Called by StrategyConnectionService after signal is opened.
|
|
6785
6898
|
*/
|
|
6786
|
-
async addSignal(symbol, context) {
|
|
6899
|
+
async addSignal(symbol, context, positionData) {
|
|
6787
6900
|
this.params.logger.debug("ClientRisk addSignal", {
|
|
6788
6901
|
symbol,
|
|
6789
6902
|
context,
|
|
6903
|
+
positionData,
|
|
6790
6904
|
backtest: this.params.backtest,
|
|
6791
6905
|
});
|
|
6792
6906
|
if (this._activePositions === POSITION_NEED_FETCH) {
|
|
@@ -6797,7 +6911,14 @@ class ClientRisk {
|
|
|
6797
6911
|
riskMap.set(key, {
|
|
6798
6912
|
strategyName: context.strategyName,
|
|
6799
6913
|
exchangeName: context.exchangeName,
|
|
6800
|
-
|
|
6914
|
+
frameName: context.frameName,
|
|
6915
|
+
symbol,
|
|
6916
|
+
position: positionData.position,
|
|
6917
|
+
priceOpen: positionData.priceOpen,
|
|
6918
|
+
priceStopLoss: positionData.priceStopLoss,
|
|
6919
|
+
priceTakeProfit: positionData.priceTakeProfit,
|
|
6920
|
+
minuteEstimatedTime: positionData.minuteEstimatedTime,
|
|
6921
|
+
openTimestamp: positionData.openTimestamp,
|
|
6801
6922
|
});
|
|
6802
6923
|
await this._updatePositions();
|
|
6803
6924
|
}
|
|
@@ -6947,13 +7068,15 @@ class RiskConnectionService {
|
|
|
6947
7068
|
*
|
|
6948
7069
|
* @param symbol - Trading pair symbol
|
|
6949
7070
|
* @param payload - Payload information (strategyName, riskName, exchangeName, frameName, backtest)
|
|
7071
|
+
* @param positionData - Position data (position, prices, timing)
|
|
6950
7072
|
*/
|
|
6951
|
-
this.addSignal = async (symbol, payload) => {
|
|
7073
|
+
this.addSignal = async (symbol, payload, positionData) => {
|
|
6952
7074
|
this.loggerService.log("riskConnectionService addSignal", {
|
|
6953
7075
|
symbol,
|
|
6954
7076
|
payload,
|
|
7077
|
+
positionData,
|
|
6955
7078
|
});
|
|
6956
|
-
await this.getRisk(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest).addSignal(symbol, payload);
|
|
7079
|
+
await this.getRisk(payload.riskName, payload.exchangeName, payload.frameName, payload.backtest).addSignal(symbol, payload, positionData);
|
|
6957
7080
|
};
|
|
6958
7081
|
/**
|
|
6959
7082
|
* Removes a closed signal from the risk management system.
|
|
@@ -7178,6 +7301,8 @@ class StrategyCoreService {
|
|
|
7178
7301
|
this.strategySchemaService = inject(TYPES.strategySchemaService);
|
|
7179
7302
|
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
7180
7303
|
this.strategyValidationService = inject(TYPES.strategyValidationService);
|
|
7304
|
+
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
7305
|
+
this.frameValidationService = inject(TYPES.frameValidationService);
|
|
7181
7306
|
/**
|
|
7182
7307
|
* Validates strategy and associated risk configuration.
|
|
7183
7308
|
*
|
|
@@ -7187,13 +7312,14 @@ class StrategyCoreService {
|
|
|
7187
7312
|
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
7188
7313
|
* @returns Promise that resolves when validation is complete
|
|
7189
7314
|
*/
|
|
7190
|
-
this.validate = functoolsKit.memoize(([
|
|
7315
|
+
this.validate = functoolsKit.memoize(([context]) => `${context.strategyName}:${context.exchangeName}:${context.frameName}`, async (context) => {
|
|
7191
7316
|
this.loggerService.log(METHOD_NAME_VALIDATE, {
|
|
7192
|
-
symbol,
|
|
7193
7317
|
context,
|
|
7194
7318
|
});
|
|
7195
7319
|
const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
|
|
7196
7320
|
this.strategyValidationService.validate(context.strategyName, METHOD_NAME_VALIDATE);
|
|
7321
|
+
this.exchangeValidationService.validate(context.exchangeName, METHOD_NAME_VALIDATE);
|
|
7322
|
+
context.frameName && this.frameValidationService.validate(context.frameName, METHOD_NAME_VALIDATE);
|
|
7197
7323
|
riskName && this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE);
|
|
7198
7324
|
riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, METHOD_NAME_VALIDATE));
|
|
7199
7325
|
});
|
|
@@ -7212,7 +7338,7 @@ class StrategyCoreService {
|
|
|
7212
7338
|
symbol,
|
|
7213
7339
|
context,
|
|
7214
7340
|
});
|
|
7215
|
-
await this.validate(
|
|
7341
|
+
await this.validate(context);
|
|
7216
7342
|
return await this.strategyConnectionService.getPendingSignal(backtest, symbol, context);
|
|
7217
7343
|
};
|
|
7218
7344
|
/**
|
|
@@ -7230,7 +7356,7 @@ class StrategyCoreService {
|
|
|
7230
7356
|
symbol,
|
|
7231
7357
|
context,
|
|
7232
7358
|
});
|
|
7233
|
-
await this.validate(
|
|
7359
|
+
await this.validate(context);
|
|
7234
7360
|
return await this.strategyConnectionService.getScheduledSignal(backtest, symbol, context);
|
|
7235
7361
|
};
|
|
7236
7362
|
/**
|
|
@@ -7270,7 +7396,7 @@ class StrategyCoreService {
|
|
|
7270
7396
|
context,
|
|
7271
7397
|
backtest,
|
|
7272
7398
|
});
|
|
7273
|
-
await this.validate(
|
|
7399
|
+
await this.validate(context);
|
|
7274
7400
|
return await this.strategyConnectionService.getBreakeven(backtest, symbol, currentPrice, context);
|
|
7275
7401
|
};
|
|
7276
7402
|
/**
|
|
@@ -7290,7 +7416,7 @@ class StrategyCoreService {
|
|
|
7290
7416
|
context,
|
|
7291
7417
|
backtest,
|
|
7292
7418
|
});
|
|
7293
|
-
await this.validate(
|
|
7419
|
+
await this.validate(context);
|
|
7294
7420
|
return await this.strategyConnectionService.getStopped(backtest, symbol, context);
|
|
7295
7421
|
};
|
|
7296
7422
|
/**
|
|
@@ -7312,7 +7438,7 @@ class StrategyCoreService {
|
|
|
7312
7438
|
backtest,
|
|
7313
7439
|
context,
|
|
7314
7440
|
});
|
|
7315
|
-
await this.validate(
|
|
7441
|
+
await this.validate(context);
|
|
7316
7442
|
return await ExecutionContextService.runInContext(async () => {
|
|
7317
7443
|
return await this.strategyConnectionService.tick(symbol, context);
|
|
7318
7444
|
}, {
|
|
@@ -7342,7 +7468,7 @@ class StrategyCoreService {
|
|
|
7342
7468
|
backtest,
|
|
7343
7469
|
context,
|
|
7344
7470
|
});
|
|
7345
|
-
await this.validate(
|
|
7471
|
+
await this.validate(context);
|
|
7346
7472
|
return await ExecutionContextService.runInContext(async () => {
|
|
7347
7473
|
return await this.strategyConnectionService.backtest(symbol, context, candles);
|
|
7348
7474
|
}, {
|
|
@@ -7368,7 +7494,7 @@ class StrategyCoreService {
|
|
|
7368
7494
|
context,
|
|
7369
7495
|
backtest,
|
|
7370
7496
|
});
|
|
7371
|
-
await this.validate(
|
|
7497
|
+
await this.validate(context);
|
|
7372
7498
|
return await this.strategyConnectionService.stop(backtest, symbol, context);
|
|
7373
7499
|
};
|
|
7374
7500
|
/**
|
|
@@ -7391,7 +7517,7 @@ class StrategyCoreService {
|
|
|
7391
7517
|
backtest,
|
|
7392
7518
|
cancelId,
|
|
7393
7519
|
});
|
|
7394
|
-
await this.validate(
|
|
7520
|
+
await this.validate(context);
|
|
7395
7521
|
return await this.strategyConnectionService.cancel(backtest, symbol, context, cancelId);
|
|
7396
7522
|
};
|
|
7397
7523
|
/**
|
|
@@ -7407,7 +7533,7 @@ class StrategyCoreService {
|
|
|
7407
7533
|
payload,
|
|
7408
7534
|
});
|
|
7409
7535
|
if (payload) {
|
|
7410
|
-
await this.validate(
|
|
7536
|
+
await this.validate({
|
|
7411
7537
|
strategyName: payload.strategyName,
|
|
7412
7538
|
exchangeName: payload.exchangeName,
|
|
7413
7539
|
frameName: payload.frameName
|
|
@@ -7450,7 +7576,7 @@ class StrategyCoreService {
|
|
|
7450
7576
|
context,
|
|
7451
7577
|
backtest,
|
|
7452
7578
|
});
|
|
7453
|
-
await this.validate(
|
|
7579
|
+
await this.validate(context);
|
|
7454
7580
|
return await this.strategyConnectionService.partialProfit(backtest, symbol, percentToClose, currentPrice, context);
|
|
7455
7581
|
};
|
|
7456
7582
|
/**
|
|
@@ -7488,7 +7614,7 @@ class StrategyCoreService {
|
|
|
7488
7614
|
context,
|
|
7489
7615
|
backtest,
|
|
7490
7616
|
});
|
|
7491
|
-
await this.validate(
|
|
7617
|
+
await this.validate(context);
|
|
7492
7618
|
return await this.strategyConnectionService.partialLoss(backtest, symbol, percentToClose, currentPrice, context);
|
|
7493
7619
|
};
|
|
7494
7620
|
/**
|
|
@@ -7527,7 +7653,7 @@ class StrategyCoreService {
|
|
|
7527
7653
|
context,
|
|
7528
7654
|
backtest,
|
|
7529
7655
|
});
|
|
7530
|
-
await this.validate(
|
|
7656
|
+
await this.validate(context);
|
|
7531
7657
|
return await this.strategyConnectionService.trailingStop(backtest, symbol, percentShift, currentPrice, context);
|
|
7532
7658
|
};
|
|
7533
7659
|
/**
|
|
@@ -7545,7 +7671,7 @@ class StrategyCoreService {
|
|
|
7545
7671
|
* ```typescript
|
|
7546
7672
|
* // LONG: entry=100, originalTP=110, distance=10%, currentPrice=102
|
|
7547
7673
|
* // Move TP further by 50%: newTP = 100 + 15% = 115
|
|
7548
|
-
* await strategyCoreService.
|
|
7674
|
+
* await strategyCoreService.trailingTake(
|
|
7549
7675
|
* false,
|
|
7550
7676
|
* "BTCUSDT",
|
|
7551
7677
|
* 50,
|
|
@@ -7554,16 +7680,16 @@ class StrategyCoreService {
|
|
|
7554
7680
|
* );
|
|
7555
7681
|
* ```
|
|
7556
7682
|
*/
|
|
7557
|
-
this.
|
|
7558
|
-
this.loggerService.log("strategyCoreService
|
|
7683
|
+
this.trailingTake = async (backtest, symbol, percentShift, currentPrice, context) => {
|
|
7684
|
+
this.loggerService.log("strategyCoreService trailingTake", {
|
|
7559
7685
|
symbol,
|
|
7560
7686
|
percentShift,
|
|
7561
7687
|
currentPrice,
|
|
7562
7688
|
context,
|
|
7563
7689
|
backtest,
|
|
7564
7690
|
});
|
|
7565
|
-
await this.validate(
|
|
7566
|
-
return await this.strategyConnectionService.
|
|
7691
|
+
await this.validate(context);
|
|
7692
|
+
return await this.strategyConnectionService.trailingTake(backtest, symbol, percentShift, currentPrice, context);
|
|
7567
7693
|
};
|
|
7568
7694
|
/**
|
|
7569
7695
|
* Moves stop-loss to breakeven when price reaches threshold.
|
|
@@ -7592,7 +7718,7 @@ class StrategyCoreService {
|
|
|
7592
7718
|
context,
|
|
7593
7719
|
backtest,
|
|
7594
7720
|
});
|
|
7595
|
-
await this.validate(
|
|
7721
|
+
await this.validate(context);
|
|
7596
7722
|
return await this.strategyConnectionService.breakeven(backtest, symbol, currentPrice, context);
|
|
7597
7723
|
};
|
|
7598
7724
|
}
|
|
@@ -7673,6 +7799,7 @@ class RiskGlobalService {
|
|
|
7673
7799
|
this.riskConnectionService = inject(TYPES.riskConnectionService);
|
|
7674
7800
|
this.riskValidationService = inject(TYPES.riskValidationService);
|
|
7675
7801
|
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
7802
|
+
this.frameValidationService = inject(TYPES.frameValidationService);
|
|
7676
7803
|
/**
|
|
7677
7804
|
* Validates risk configuration.
|
|
7678
7805
|
* Memoized to avoid redundant validations for the same risk-exchange-frame combination.
|
|
@@ -7686,6 +7813,7 @@ class RiskGlobalService {
|
|
|
7686
7813
|
});
|
|
7687
7814
|
this.riskValidationService.validate(payload.riskName, "riskGlobalService validate");
|
|
7688
7815
|
this.exchangeValidationService.validate(payload.exchangeName, "riskGlobalService validate");
|
|
7816
|
+
payload.frameName && this.frameValidationService.validate(payload.frameName, "riskGlobalService validate");
|
|
7689
7817
|
});
|
|
7690
7818
|
/**
|
|
7691
7819
|
* Checks if a signal should be allowed based on risk limits.
|
|
@@ -7707,14 +7835,16 @@ class RiskGlobalService {
|
|
|
7707
7835
|
*
|
|
7708
7836
|
* @param symbol - Trading pair symbol
|
|
7709
7837
|
* @param payload - Payload information (strategyName, riskName, exchangeName, frameName, backtest)
|
|
7838
|
+
* @param positionData - Position data (position, prices, timing)
|
|
7710
7839
|
*/
|
|
7711
|
-
this.addSignal = async (symbol, payload) => {
|
|
7840
|
+
this.addSignal = async (symbol, payload, positionData) => {
|
|
7712
7841
|
this.loggerService.log("riskGlobalService addSignal", {
|
|
7713
7842
|
symbol,
|
|
7714
7843
|
payload,
|
|
7844
|
+
positionData,
|
|
7715
7845
|
});
|
|
7716
7846
|
await this.validate(payload);
|
|
7717
|
-
await this.riskConnectionService.addSignal(symbol, payload);
|
|
7847
|
+
await this.riskConnectionService.addSignal(symbol, payload, positionData);
|
|
7718
7848
|
};
|
|
7719
7849
|
/**
|
|
7720
7850
|
* Removes a closed signal from the risk management system.
|
|
@@ -15962,6 +16092,10 @@ class PartialGlobalService {
|
|
|
15962
16092
|
* Exchange validation service for validating exchange existence.
|
|
15963
16093
|
*/
|
|
15964
16094
|
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
16095
|
+
/**
|
|
16096
|
+
* Frame validation service for validating frame existence.
|
|
16097
|
+
*/
|
|
16098
|
+
this.frameValidationService = inject(TYPES.frameValidationService);
|
|
15965
16099
|
/**
|
|
15966
16100
|
* Validates strategy and associated risk configuration.
|
|
15967
16101
|
* Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
|
|
@@ -15976,6 +16110,7 @@ class PartialGlobalService {
|
|
|
15976
16110
|
});
|
|
15977
16111
|
this.strategyValidationService.validate(context.strategyName, methodName);
|
|
15978
16112
|
this.exchangeValidationService.validate(context.exchangeName, methodName);
|
|
16113
|
+
context.frameName && this.frameValidationService.validate(context.frameName, methodName);
|
|
15979
16114
|
const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
|
|
15980
16115
|
riskName && this.riskValidationService.validate(riskName, methodName);
|
|
15981
16116
|
riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
|
|
@@ -16941,6 +17076,10 @@ class BreakevenGlobalService {
|
|
|
16941
17076
|
* Exchange validation service for validating exchange existence.
|
|
16942
17077
|
*/
|
|
16943
17078
|
this.exchangeValidationService = inject(TYPES.exchangeValidationService);
|
|
17079
|
+
/**
|
|
17080
|
+
* Frame validation service for validating frame existence.
|
|
17081
|
+
*/
|
|
17082
|
+
this.frameValidationService = inject(TYPES.frameValidationService);
|
|
16944
17083
|
/**
|
|
16945
17084
|
* Validates strategy and associated risk configuration.
|
|
16946
17085
|
* Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
|
|
@@ -16955,6 +17094,7 @@ class BreakevenGlobalService {
|
|
|
16955
17094
|
});
|
|
16956
17095
|
this.strategyValidationService.validate(context.strategyName, methodName);
|
|
16957
17096
|
this.exchangeValidationService.validate(context.exchangeName, methodName);
|
|
17097
|
+
context.frameName && this.frameValidationService.validate(context.frameName, methodName);
|
|
16958
17098
|
const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
|
|
16959
17099
|
riskName && this.riskValidationService.validate(riskName, methodName);
|
|
16960
17100
|
riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
|
|
@@ -18268,7 +18408,7 @@ const CANCEL_METHOD_NAME = "strategy.cancel";
|
|
|
18268
18408
|
const PARTIAL_PROFIT_METHOD_NAME = "strategy.partialProfit";
|
|
18269
18409
|
const PARTIAL_LOSS_METHOD_NAME = "strategy.partialLoss";
|
|
18270
18410
|
const TRAILING_STOP_METHOD_NAME = "strategy.trailingStop";
|
|
18271
|
-
const TRAILING_PROFIT_METHOD_NAME = "strategy.
|
|
18411
|
+
const TRAILING_PROFIT_METHOD_NAME = "strategy.trailingTake";
|
|
18272
18412
|
const BREAKEVEN_METHOD_NAME = "strategy.breakeven";
|
|
18273
18413
|
/**
|
|
18274
18414
|
* Stops the strategy from generating new signals.
|
|
@@ -18429,23 +18569,44 @@ async function partialLoss(symbol, percentToClose) {
|
|
|
18429
18569
|
/**
|
|
18430
18570
|
* Adjusts the trailing stop-loss distance for an active pending signal.
|
|
18431
18571
|
*
|
|
18432
|
-
*
|
|
18433
|
-
*
|
|
18572
|
+
* CRITICAL: Always calculates from ORIGINAL SL, not from current trailing SL.
|
|
18573
|
+
* This prevents error accumulation on repeated calls.
|
|
18574
|
+
* Larger percentShift ABSORBS smaller one (updates only towards better protection).
|
|
18575
|
+
*
|
|
18576
|
+
* Updates the stop-loss distance by a percentage adjustment relative to the ORIGINAL SL distance.
|
|
18577
|
+
* Negative percentShift tightens the SL (reduces distance, moves closer to entry).
|
|
18578
|
+
* Positive percentShift loosens the SL (increases distance, moves away from entry).
|
|
18579
|
+
*
|
|
18580
|
+
* Absorption behavior:
|
|
18581
|
+
* - First call: sets trailing SL unconditionally
|
|
18582
|
+
* - Subsequent calls: updates only if new SL is BETTER (protects more profit)
|
|
18583
|
+
* - For LONG: only accepts HIGHER SL (never moves down, closer to entry wins)
|
|
18584
|
+
* - For SHORT: only accepts LOWER SL (never moves up, closer to entry wins)
|
|
18434
18585
|
*
|
|
18435
18586
|
* Automatically detects backtest/live mode from execution context.
|
|
18436
18587
|
*
|
|
18437
18588
|
* @param symbol - Trading pair symbol
|
|
18438
|
-
* @param percentShift - Percentage adjustment to SL distance (-100 to 100)
|
|
18589
|
+
* @param percentShift - Percentage adjustment to ORIGINAL SL distance (-100 to 100)
|
|
18439
18590
|
* @param currentPrice - Current market price to check for intrusion
|
|
18440
|
-
* @returns Promise
|
|
18591
|
+
* @returns Promise<boolean> - true if trailing SL was set/updated, false if rejected (absorption/intrusion/conflict)
|
|
18441
18592
|
*
|
|
18442
18593
|
* @example
|
|
18443
18594
|
* ```typescript
|
|
18444
18595
|
* import { trailingStop } from "backtest-kit";
|
|
18445
18596
|
*
|
|
18446
18597
|
* // LONG: entry=100, originalSL=90, distance=10%, currentPrice=102
|
|
18447
|
-
*
|
|
18448
|
-
*
|
|
18598
|
+
*
|
|
18599
|
+
* // First call: tighten by 5%
|
|
18600
|
+
* const success1 = await trailingStop("BTCUSDT", -5, 102);
|
|
18601
|
+
* // success1 = true, newDistance = 10% - 5% = 5%, newSL = 95
|
|
18602
|
+
*
|
|
18603
|
+
* // Second call: try weaker protection (smaller percentShift)
|
|
18604
|
+
* const success2 = await trailingStop("BTCUSDT", -3, 102);
|
|
18605
|
+
* // success2 = false (SKIPPED: newSL=97 < 95, worse protection, larger % absorbs smaller)
|
|
18606
|
+
*
|
|
18607
|
+
* // Third call: stronger protection (larger percentShift)
|
|
18608
|
+
* const success3 = await trailingStop("BTCUSDT", -7, 102);
|
|
18609
|
+
* // success3 = true (ACCEPTED: newDistance = 10% - 7% = 3%, newSL = 97 > 95, better protection)
|
|
18449
18610
|
* ```
|
|
18450
18611
|
*/
|
|
18451
18612
|
async function trailingStop(symbol, percentShift, currentPrice) {
|
|
@@ -18462,46 +18623,66 @@ async function trailingStop(symbol, percentShift, currentPrice) {
|
|
|
18462
18623
|
}
|
|
18463
18624
|
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
18464
18625
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
18465
|
-
await bt.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
|
|
18626
|
+
return await bt.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
|
|
18466
18627
|
}
|
|
18467
18628
|
/**
|
|
18468
18629
|
* Adjusts the trailing take-profit distance for an active pending signal.
|
|
18469
18630
|
*
|
|
18470
|
-
*
|
|
18471
|
-
*
|
|
18472
|
-
*
|
|
18631
|
+
* CRITICAL: Always calculates from ORIGINAL TP, not from current trailing TP.
|
|
18632
|
+
* This prevents error accumulation on repeated calls.
|
|
18633
|
+
* Larger percentShift ABSORBS smaller one (updates only towards more conservative TP).
|
|
18634
|
+
*
|
|
18635
|
+
* Updates the take-profit distance by a percentage adjustment relative to the ORIGINAL TP distance.
|
|
18636
|
+
* Negative percentShift brings TP closer to entry (more conservative).
|
|
18637
|
+
* Positive percentShift moves TP further from entry (more aggressive).
|
|
18638
|
+
*
|
|
18639
|
+
* Absorption behavior:
|
|
18640
|
+
* - First call: sets trailing TP unconditionally
|
|
18641
|
+
* - Subsequent calls: updates only if new TP is MORE CONSERVATIVE (closer to entry)
|
|
18642
|
+
* - For LONG: only accepts LOWER TP (never moves up, closer to entry wins)
|
|
18643
|
+
* - For SHORT: only accepts HIGHER TP (never moves down, closer to entry wins)
|
|
18473
18644
|
*
|
|
18474
18645
|
* Automatically detects backtest/live mode from execution context.
|
|
18475
18646
|
*
|
|
18476
18647
|
* @param symbol - Trading pair symbol
|
|
18477
|
-
* @param percentShift - Percentage adjustment to TP distance (-100 to 100)
|
|
18648
|
+
* @param percentShift - Percentage adjustment to ORIGINAL TP distance (-100 to 100)
|
|
18478
18649
|
* @param currentPrice - Current market price to check for intrusion
|
|
18479
|
-
* @returns Promise
|
|
18650
|
+
* @returns Promise<boolean> - true if trailing TP was set/updated, false if rejected (absorption/intrusion/conflict)
|
|
18480
18651
|
*
|
|
18481
18652
|
* @example
|
|
18482
18653
|
* ```typescript
|
|
18483
|
-
* import {
|
|
18654
|
+
* import { trailingTake } from "backtest-kit";
|
|
18484
18655
|
*
|
|
18485
18656
|
* // LONG: entry=100, originalTP=110, distance=10%, currentPrice=102
|
|
18486
|
-
*
|
|
18487
|
-
*
|
|
18657
|
+
*
|
|
18658
|
+
* // First call: bring TP closer by 3%
|
|
18659
|
+
* const success1 = await trailingTake("BTCUSDT", -3, 102);
|
|
18660
|
+
* // success1 = true, newDistance = 10% - 3% = 7%, newTP = 107
|
|
18661
|
+
*
|
|
18662
|
+
* // Second call: try to move TP further (less conservative)
|
|
18663
|
+
* const success2 = await trailingTake("BTCUSDT", 2, 102);
|
|
18664
|
+
* // success2 = false (SKIPPED: newTP=112 > 107, less conservative, larger % absorbs smaller)
|
|
18665
|
+
*
|
|
18666
|
+
* // Third call: even more conservative
|
|
18667
|
+
* const success3 = await trailingTake("BTCUSDT", -5, 102);
|
|
18668
|
+
* // success3 = true (ACCEPTED: newDistance = 10% - 5% = 5%, newTP = 105 < 107, more conservative)
|
|
18488
18669
|
* ```
|
|
18489
18670
|
*/
|
|
18490
|
-
async function
|
|
18671
|
+
async function trailingTake(symbol, percentShift, currentPrice) {
|
|
18491
18672
|
bt.loggerService.info(TRAILING_PROFIT_METHOD_NAME, {
|
|
18492
18673
|
symbol,
|
|
18493
18674
|
percentShift,
|
|
18494
18675
|
currentPrice,
|
|
18495
18676
|
});
|
|
18496
18677
|
if (!ExecutionContextService.hasContext()) {
|
|
18497
|
-
throw new Error("
|
|
18678
|
+
throw new Error("trailingTake requires an execution context");
|
|
18498
18679
|
}
|
|
18499
18680
|
if (!MethodContextService.hasContext()) {
|
|
18500
|
-
throw new Error("
|
|
18681
|
+
throw new Error("trailingTake requires a method context");
|
|
18501
18682
|
}
|
|
18502
18683
|
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
18503
18684
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
18504
|
-
await bt.strategyCoreService.
|
|
18685
|
+
return await bt.strategyCoreService.trailingTake(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
|
|
18505
18686
|
}
|
|
18506
18687
|
/**
|
|
18507
18688
|
* Moves stop-loss to breakeven when price reaches threshold.
|
|
@@ -20448,7 +20629,7 @@ const BACKTEST_METHOD_NAME_CANCEL = "BacktestUtils.cancel";
|
|
|
20448
20629
|
const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.partialProfit";
|
|
20449
20630
|
const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.partialLoss";
|
|
20450
20631
|
const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.trailingStop";
|
|
20451
|
-
const BACKTEST_METHOD_NAME_TRAILING_PROFIT = "BacktestUtils.
|
|
20632
|
+
const BACKTEST_METHOD_NAME_TRAILING_PROFIT = "BacktestUtils.trailingTake";
|
|
20452
20633
|
const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
|
|
20453
20634
|
/**
|
|
20454
20635
|
* Internal task function that runs backtest and handles completion.
|
|
@@ -21067,11 +21248,22 @@ class BacktestUtils {
|
|
|
21067
21248
|
/**
|
|
21068
21249
|
* Adjusts the trailing stop-loss distance for an active pending signal.
|
|
21069
21250
|
*
|
|
21070
|
-
*
|
|
21071
|
-
*
|
|
21251
|
+
* CRITICAL: Always calculates from ORIGINAL SL, not from current trailing SL.
|
|
21252
|
+
* This prevents error accumulation on repeated calls.
|
|
21253
|
+
* Larger percentShift ABSORBS smaller one (updates only towards better protection).
|
|
21254
|
+
*
|
|
21255
|
+
* Updates the stop-loss distance by a percentage adjustment relative to the ORIGINAL SL distance.
|
|
21256
|
+
* Negative percentShift tightens the SL (reduces distance, moves closer to entry).
|
|
21257
|
+
* Positive percentShift loosens the SL (increases distance, moves away from entry).
|
|
21258
|
+
*
|
|
21259
|
+
* Absorption behavior:
|
|
21260
|
+
* - First call: sets trailing SL unconditionally
|
|
21261
|
+
* - Subsequent calls: updates only if new SL is BETTER (protects more profit)
|
|
21262
|
+
* - For LONG: only accepts HIGHER SL (never moves down, closer to entry wins)
|
|
21263
|
+
* - For SHORT: only accepts LOWER SL (never moves up, closer to entry wins)
|
|
21072
21264
|
*
|
|
21073
21265
|
* @param symbol - Trading pair symbol
|
|
21074
|
-
* @param percentShift - Percentage adjustment to SL distance (-100 to 100)
|
|
21266
|
+
* @param percentShift - Percentage adjustment to ORIGINAL SL distance (-100 to 100)
|
|
21075
21267
|
* @param currentPrice - Current market price to check for intrusion
|
|
21076
21268
|
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
21077
21269
|
* @returns Promise that resolves when trailing SL is updated
|
|
@@ -21079,12 +21271,22 @@ class BacktestUtils {
|
|
|
21079
21271
|
* @example
|
|
21080
21272
|
* ```typescript
|
|
21081
21273
|
* // LONG: entry=100, originalSL=90, distance=10%, currentPrice=102
|
|
21082
|
-
*
|
|
21083
|
-
*
|
|
21274
|
+
*
|
|
21275
|
+
* // First call: tighten by 5%
|
|
21276
|
+
* await Backtest.trailingStop("BTCUSDT", -5, 102, {
|
|
21084
21277
|
* exchangeName: "binance",
|
|
21085
21278
|
* frameName: "frame1",
|
|
21086
21279
|
* strategyName: "my-strategy"
|
|
21087
21280
|
* });
|
|
21281
|
+
* // newDistance = 10% - 5% = 5%, newSL = 95
|
|
21282
|
+
*
|
|
21283
|
+
* // Second call: try weaker protection (smaller percentShift)
|
|
21284
|
+
* await Backtest.trailingStop("BTCUSDT", -3, 102, context);
|
|
21285
|
+
* // SKIPPED: newSL=97 < 95 (worse protection, larger % absorbs smaller)
|
|
21286
|
+
*
|
|
21287
|
+
* // Third call: stronger protection (larger percentShift)
|
|
21288
|
+
* await Backtest.trailingStop("BTCUSDT", -7, 102, context);
|
|
21289
|
+
* // ACCEPTED: newDistance = 10% - 7% = 3%, newSL = 97 > 95 (better protection)
|
|
21088
21290
|
* ```
|
|
21089
21291
|
*/
|
|
21090
21292
|
this.trailingStop = async (symbol, percentShift, currentPrice, context) => {
|
|
@@ -21103,17 +21305,27 @@ class BacktestUtils {
|
|
|
21103
21305
|
riskList &&
|
|
21104
21306
|
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_STOP));
|
|
21105
21307
|
}
|
|
21106
|
-
await bt.strategyCoreService.trailingStop(true, symbol, percentShift, currentPrice, context);
|
|
21308
|
+
return await bt.strategyCoreService.trailingStop(true, symbol, percentShift, currentPrice, context);
|
|
21107
21309
|
};
|
|
21108
21310
|
/**
|
|
21109
21311
|
* Adjusts the trailing take-profit distance for an active pending signal.
|
|
21110
21312
|
*
|
|
21111
|
-
*
|
|
21112
|
-
*
|
|
21113
|
-
*
|
|
21313
|
+
* CRITICAL: Always calculates from ORIGINAL TP, not from current trailing TP.
|
|
21314
|
+
* This prevents error accumulation on repeated calls.
|
|
21315
|
+
* Larger percentShift ABSORBS smaller one (updates only towards more conservative TP).
|
|
21316
|
+
*
|
|
21317
|
+
* Updates the take-profit distance by a percentage adjustment relative to the ORIGINAL TP distance.
|
|
21318
|
+
* Negative percentShift brings TP closer to entry (more conservative).
|
|
21319
|
+
* Positive percentShift moves TP further from entry (more aggressive).
|
|
21320
|
+
*
|
|
21321
|
+
* Absorption behavior:
|
|
21322
|
+
* - First call: sets trailing TP unconditionally
|
|
21323
|
+
* - Subsequent calls: updates only if new TP is MORE CONSERVATIVE (closer to entry)
|
|
21324
|
+
* - For LONG: only accepts LOWER TP (never moves up, closer to entry wins)
|
|
21325
|
+
* - For SHORT: only accepts HIGHER TP (never moves down, closer to entry wins)
|
|
21114
21326
|
*
|
|
21115
21327
|
* @param symbol - Trading pair symbol
|
|
21116
|
-
* @param percentShift - Percentage adjustment to TP distance (-100 to 100)
|
|
21328
|
+
* @param percentShift - Percentage adjustment to ORIGINAL TP distance (-100 to 100)
|
|
21117
21329
|
* @param currentPrice - Current market price to check for intrusion
|
|
21118
21330
|
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
21119
21331
|
* @returns Promise that resolves when trailing TP is updated
|
|
@@ -21121,15 +21333,25 @@ class BacktestUtils {
|
|
|
21121
21333
|
* @example
|
|
21122
21334
|
* ```typescript
|
|
21123
21335
|
* // LONG: entry=100, originalTP=110, distance=10%, currentPrice=102
|
|
21124
|
-
*
|
|
21125
|
-
*
|
|
21336
|
+
*
|
|
21337
|
+
* // First call: bring TP closer by 3%
|
|
21338
|
+
* await Backtest.trailingTake("BTCUSDT", -3, 102, {
|
|
21126
21339
|
* exchangeName: "binance",
|
|
21127
21340
|
* frameName: "frame1",
|
|
21128
21341
|
* strategyName: "my-strategy"
|
|
21129
21342
|
* });
|
|
21343
|
+
* // newDistance = 10% - 3% = 7%, newTP = 107
|
|
21344
|
+
*
|
|
21345
|
+
* // Second call: try to move TP further (less conservative)
|
|
21346
|
+
* await Backtest.trailingTake("BTCUSDT", 2, 102, context);
|
|
21347
|
+
* // SKIPPED: newTP=112 > 107 (less conservative, larger % absorbs smaller)
|
|
21348
|
+
*
|
|
21349
|
+
* // Third call: even more conservative
|
|
21350
|
+
* await Backtest.trailingTake("BTCUSDT", -5, 102, context);
|
|
21351
|
+
* // ACCEPTED: newDistance = 10% - 5% = 5%, newTP = 105 < 107 (more conservative)
|
|
21130
21352
|
* ```
|
|
21131
21353
|
*/
|
|
21132
|
-
this.
|
|
21354
|
+
this.trailingTake = async (symbol, percentShift, currentPrice, context) => {
|
|
21133
21355
|
bt.loggerService.info(BACKTEST_METHOD_NAME_TRAILING_PROFIT, {
|
|
21134
21356
|
symbol,
|
|
21135
21357
|
percentShift,
|
|
@@ -21145,7 +21367,7 @@ class BacktestUtils {
|
|
|
21145
21367
|
riskList &&
|
|
21146
21368
|
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_PROFIT));
|
|
21147
21369
|
}
|
|
21148
|
-
await bt.strategyCoreService.
|
|
21370
|
+
return await bt.strategyCoreService.trailingTake(true, symbol, percentShift, currentPrice, context);
|
|
21149
21371
|
};
|
|
21150
21372
|
/**
|
|
21151
21373
|
* Moves stop-loss to breakeven when price reaches threshold.
|
|
@@ -21351,7 +21573,7 @@ const LIVE_METHOD_NAME_CANCEL = "LiveUtils.cancel";
|
|
|
21351
21573
|
const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.partialProfit";
|
|
21352
21574
|
const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.partialLoss";
|
|
21353
21575
|
const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.trailingStop";
|
|
21354
|
-
const LIVE_METHOD_NAME_TRAILING_PROFIT = "LiveUtils.
|
|
21576
|
+
const LIVE_METHOD_NAME_TRAILING_PROFIT = "LiveUtils.trailingTake";
|
|
21355
21577
|
/**
|
|
21356
21578
|
* Internal task function that runs live trading and handles completion.
|
|
21357
21579
|
* Consumes live trading results and updates instance state flags.
|
|
@@ -21947,23 +22169,44 @@ class LiveUtils {
|
|
|
21947
22169
|
/**
|
|
21948
22170
|
* Adjusts the trailing stop-loss distance for an active pending signal.
|
|
21949
22171
|
*
|
|
21950
|
-
*
|
|
21951
|
-
*
|
|
22172
|
+
* CRITICAL: Always calculates from ORIGINAL SL, not from current trailing SL.
|
|
22173
|
+
* This prevents error accumulation on repeated calls.
|
|
22174
|
+
* Larger percentShift ABSORBS smaller one (updates only towards better protection).
|
|
22175
|
+
*
|
|
22176
|
+
* Updates the stop-loss distance by a percentage adjustment relative to the ORIGINAL SL distance.
|
|
22177
|
+
* Negative percentShift tightens the SL (reduces distance, moves closer to entry).
|
|
22178
|
+
* Positive percentShift loosens the SL (increases distance, moves away from entry).
|
|
22179
|
+
*
|
|
22180
|
+
* Absorption behavior:
|
|
22181
|
+
* - First call: sets trailing SL unconditionally
|
|
22182
|
+
* - Subsequent calls: updates only if new SL is BETTER (protects more profit)
|
|
22183
|
+
* - For LONG: only accepts HIGHER SL (never moves down, closer to entry wins)
|
|
22184
|
+
* - For SHORT: only accepts LOWER SL (never moves up, closer to entry wins)
|
|
21952
22185
|
*
|
|
21953
22186
|
* @param symbol - Trading pair symbol
|
|
21954
|
-
* @param percentShift - Percentage adjustment to SL distance (-100 to 100)
|
|
22187
|
+
* @param percentShift - Percentage adjustment to ORIGINAL SL distance (-100 to 100)
|
|
21955
22188
|
* @param currentPrice - Current market price to check for intrusion
|
|
21956
22189
|
* @param context - Execution context with strategyName and exchangeName
|
|
21957
|
-
* @returns Promise
|
|
22190
|
+
* @returns Promise<boolean> - true if trailing SL was set/updated, false if rejected (absorption/intrusion/conflict)
|
|
21958
22191
|
*
|
|
21959
22192
|
* @example
|
|
21960
22193
|
* ```typescript
|
|
21961
22194
|
* // LONG: entry=100, originalSL=90, distance=10%, currentPrice=102
|
|
21962
|
-
*
|
|
21963
|
-
*
|
|
22195
|
+
*
|
|
22196
|
+
* // First call: tighten by 5%
|
|
22197
|
+
* const success1 = await Live.trailingStop("BTCUSDT", -5, 102, {
|
|
21964
22198
|
* exchangeName: "binance",
|
|
21965
22199
|
* strategyName: "my-strategy"
|
|
21966
22200
|
* });
|
|
22201
|
+
* // success1 = true, newDistance = 10% - 5% = 5%, newSL = 95
|
|
22202
|
+
*
|
|
22203
|
+
* // Second call: try weaker protection (smaller percentShift)
|
|
22204
|
+
* const success2 = await Live.trailingStop("BTCUSDT", -3, 102, context);
|
|
22205
|
+
* // success2 = false (SKIPPED: newSL=97 < 95, worse protection, larger % absorbs smaller)
|
|
22206
|
+
*
|
|
22207
|
+
* // Third call: stronger protection (larger percentShift)
|
|
22208
|
+
* const success3 = await Live.trailingStop("BTCUSDT", -7, 102, context);
|
|
22209
|
+
* // success3 = true (ACCEPTED: newDistance = 10% - 7% = 3%, newSL = 97 > 95, better protection)
|
|
21967
22210
|
* ```
|
|
21968
22211
|
*/
|
|
21969
22212
|
this.trailingStop = async (symbol, percentShift, currentPrice, context) => {
|
|
@@ -21980,7 +22223,7 @@ class LiveUtils {
|
|
|
21980
22223
|
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP);
|
|
21981
22224
|
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP));
|
|
21982
22225
|
}
|
|
21983
|
-
await bt.strategyCoreService.trailingStop(false, symbol, percentShift, currentPrice, {
|
|
22226
|
+
return await bt.strategyCoreService.trailingStop(false, symbol, percentShift, currentPrice, {
|
|
21984
22227
|
strategyName: context.strategyName,
|
|
21985
22228
|
exchangeName: context.exchangeName,
|
|
21986
22229
|
frameName: "",
|
|
@@ -21989,27 +22232,47 @@ class LiveUtils {
|
|
|
21989
22232
|
/**
|
|
21990
22233
|
* Adjusts the trailing take-profit distance for an active pending signal.
|
|
21991
22234
|
*
|
|
21992
|
-
*
|
|
21993
|
-
*
|
|
21994
|
-
*
|
|
22235
|
+
* CRITICAL: Always calculates from ORIGINAL TP, not from current trailing TP.
|
|
22236
|
+
* This prevents error accumulation on repeated calls.
|
|
22237
|
+
* Larger percentShift ABSORBS smaller one (updates only towards more conservative TP).
|
|
22238
|
+
*
|
|
22239
|
+
* Updates the take-profit distance by a percentage adjustment relative to the ORIGINAL TP distance.
|
|
22240
|
+
* Negative percentShift brings TP closer to entry (more conservative).
|
|
22241
|
+
* Positive percentShift moves TP further from entry (more aggressive).
|
|
22242
|
+
*
|
|
22243
|
+
* Absorption behavior:
|
|
22244
|
+
* - First call: sets trailing TP unconditionally
|
|
22245
|
+
* - Subsequent calls: updates only if new TP is MORE CONSERVATIVE (closer to entry)
|
|
22246
|
+
* - For LONG: only accepts LOWER TP (never moves up, closer to entry wins)
|
|
22247
|
+
* - For SHORT: only accepts HIGHER TP (never moves down, closer to entry wins)
|
|
21995
22248
|
*
|
|
21996
22249
|
* @param symbol - Trading pair symbol
|
|
21997
|
-
* @param percentShift - Percentage adjustment to TP distance (-100 to 100)
|
|
22250
|
+
* @param percentShift - Percentage adjustment to ORIGINAL TP distance (-100 to 100)
|
|
21998
22251
|
* @param currentPrice - Current market price to check for intrusion
|
|
21999
22252
|
* @param context - Execution context with strategyName and exchangeName
|
|
22000
|
-
* @returns Promise
|
|
22253
|
+
* @returns Promise<boolean> - true if trailing TP was set/updated, false if rejected (absorption/intrusion/conflict)
|
|
22001
22254
|
*
|
|
22002
22255
|
* @example
|
|
22003
22256
|
* ```typescript
|
|
22004
22257
|
* // LONG: entry=100, originalTP=110, distance=10%, currentPrice=102
|
|
22005
|
-
*
|
|
22006
|
-
*
|
|
22258
|
+
*
|
|
22259
|
+
* // First call: bring TP closer by 3%
|
|
22260
|
+
* const success1 = await Live.trailingTake("BTCUSDT", -3, 102, {
|
|
22007
22261
|
* exchangeName: "binance",
|
|
22008
22262
|
* strategyName: "my-strategy"
|
|
22009
22263
|
* });
|
|
22264
|
+
* // success1 = true, newDistance = 10% - 3% = 7%, newTP = 107
|
|
22265
|
+
*
|
|
22266
|
+
* // Second call: try to move TP further (less conservative)
|
|
22267
|
+
* const success2 = await Live.trailingTake("BTCUSDT", 2, 102, context);
|
|
22268
|
+
* // success2 = false (SKIPPED: newTP=112 > 107, less conservative, larger % absorbs smaller)
|
|
22269
|
+
*
|
|
22270
|
+
* // Third call: even more conservative
|
|
22271
|
+
* const success3 = await Live.trailingTake("BTCUSDT", -5, 102, context);
|
|
22272
|
+
* // success3 = true (ACCEPTED: newDistance = 10% - 5% = 5%, newTP = 105 < 107, more conservative)
|
|
22010
22273
|
* ```
|
|
22011
22274
|
*/
|
|
22012
|
-
this.
|
|
22275
|
+
this.trailingTake = async (symbol, percentShift, currentPrice, context) => {
|
|
22013
22276
|
bt.loggerService.info(LIVE_METHOD_NAME_TRAILING_PROFIT, {
|
|
22014
22277
|
symbol,
|
|
22015
22278
|
percentShift,
|
|
@@ -22023,7 +22286,7 @@ class LiveUtils {
|
|
|
22023
22286
|
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT);
|
|
22024
22287
|
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT));
|
|
22025
22288
|
}
|
|
22026
|
-
await bt.strategyCoreService.
|
|
22289
|
+
return await bt.strategyCoreService.trailingTake(false, symbol, percentShift, currentPrice, {
|
|
22027
22290
|
strategyName: context.strategyName,
|
|
22028
22291
|
exchangeName: context.exchangeName,
|
|
22029
22292
|
frameName: "",
|
|
@@ -22262,6 +22525,7 @@ class ScheduleUtils {
|
|
|
22262
22525
|
});
|
|
22263
22526
|
bt.strategyValidationService.validate(context.strategyName, SCHEDULE_METHOD_NAME_GET_DATA);
|
|
22264
22527
|
bt.exchangeValidationService.validate(context.exchangeName, SCHEDULE_METHOD_NAME_GET_DATA);
|
|
22528
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, SCHEDULE_METHOD_NAME_GET_DATA);
|
|
22265
22529
|
{
|
|
22266
22530
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
22267
22531
|
riskName && bt.riskValidationService.validate(riskName, SCHEDULE_METHOD_NAME_GET_DATA);
|
|
@@ -22291,6 +22555,7 @@ class ScheduleUtils {
|
|
|
22291
22555
|
});
|
|
22292
22556
|
bt.strategyValidationService.validate(context.strategyName, SCHEDULE_METHOD_NAME_GET_REPORT);
|
|
22293
22557
|
bt.exchangeValidationService.validate(context.exchangeName, SCHEDULE_METHOD_NAME_GET_REPORT);
|
|
22558
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, SCHEDULE_METHOD_NAME_GET_REPORT);
|
|
22294
22559
|
{
|
|
22295
22560
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
22296
22561
|
riskName && bt.riskValidationService.validate(riskName, SCHEDULE_METHOD_NAME_GET_REPORT);
|
|
@@ -22324,6 +22589,7 @@ class ScheduleUtils {
|
|
|
22324
22589
|
});
|
|
22325
22590
|
bt.strategyValidationService.validate(context.strategyName, SCHEDULE_METHOD_NAME_DUMP);
|
|
22326
22591
|
bt.exchangeValidationService.validate(context.exchangeName, SCHEDULE_METHOD_NAME_DUMP);
|
|
22592
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, SCHEDULE_METHOD_NAME_DUMP);
|
|
22327
22593
|
{
|
|
22328
22594
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
22329
22595
|
riskName && bt.riskValidationService.validate(riskName, SCHEDULE_METHOD_NAME_DUMP);
|
|
@@ -22413,6 +22679,7 @@ class Performance {
|
|
|
22413
22679
|
static async getData(symbol, context, backtest = false) {
|
|
22414
22680
|
bt.strategyValidationService.validate(context.strategyName, PERFORMANCE_METHOD_NAME_GET_DATA);
|
|
22415
22681
|
bt.exchangeValidationService.validate(context.exchangeName, PERFORMANCE_METHOD_NAME_GET_DATA);
|
|
22682
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, PERFORMANCE_METHOD_NAME_GET_DATA);
|
|
22416
22683
|
{
|
|
22417
22684
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
22418
22685
|
riskName && bt.riskValidationService.validate(riskName, PERFORMANCE_METHOD_NAME_GET_DATA);
|
|
@@ -22446,6 +22713,7 @@ class Performance {
|
|
|
22446
22713
|
static async getReport(symbol, context, backtest = false, columns) {
|
|
22447
22714
|
bt.strategyValidationService.validate(context.strategyName, PERFORMANCE_METHOD_NAME_GET_REPORT);
|
|
22448
22715
|
bt.exchangeValidationService.validate(context.exchangeName, PERFORMANCE_METHOD_NAME_GET_REPORT);
|
|
22716
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, PERFORMANCE_METHOD_NAME_GET_REPORT);
|
|
22449
22717
|
{
|
|
22450
22718
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
22451
22719
|
riskName && bt.riskValidationService.validate(riskName, PERFORMANCE_METHOD_NAME_GET_REPORT);
|
|
@@ -22476,6 +22744,7 @@ class Performance {
|
|
|
22476
22744
|
static async dump(symbol, context, backtest = false, path = "./dump/performance", columns) {
|
|
22477
22745
|
bt.strategyValidationService.validate(context.strategyName, PERFORMANCE_METHOD_NAME_DUMP);
|
|
22478
22746
|
bt.exchangeValidationService.validate(context.exchangeName, PERFORMANCE_METHOD_NAME_DUMP);
|
|
22747
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, PERFORMANCE_METHOD_NAME_DUMP);
|
|
22479
22748
|
{
|
|
22480
22749
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
22481
22750
|
riskName && bt.riskValidationService.validate(riskName, PERFORMANCE_METHOD_NAME_DUMP);
|
|
@@ -23067,6 +23336,7 @@ class HeatUtils {
|
|
|
23067
23336
|
bt.loggerService.info(HEAT_METHOD_NAME_GET_DATA, { strategyName: context.strategyName });
|
|
23068
23337
|
bt.strategyValidationService.validate(context.strategyName, HEAT_METHOD_NAME_GET_DATA);
|
|
23069
23338
|
bt.exchangeValidationService.validate(context.exchangeName, HEAT_METHOD_NAME_GET_DATA);
|
|
23339
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, HEAT_METHOD_NAME_GET_DATA);
|
|
23070
23340
|
{
|
|
23071
23341
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
23072
23342
|
riskName && bt.riskValidationService.validate(riskName, HEAT_METHOD_NAME_GET_DATA);
|
|
@@ -23109,6 +23379,7 @@ class HeatUtils {
|
|
|
23109
23379
|
bt.loggerService.info(HEAT_METHOD_NAME_GET_REPORT, { strategyName: context.strategyName });
|
|
23110
23380
|
bt.strategyValidationService.validate(context.strategyName, HEAT_METHOD_NAME_GET_REPORT);
|
|
23111
23381
|
bt.exchangeValidationService.validate(context.exchangeName, HEAT_METHOD_NAME_GET_REPORT);
|
|
23382
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, HEAT_METHOD_NAME_GET_REPORT);
|
|
23112
23383
|
{
|
|
23113
23384
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
23114
23385
|
riskName && bt.riskValidationService.validate(riskName, HEAT_METHOD_NAME_GET_REPORT);
|
|
@@ -23148,6 +23419,7 @@ class HeatUtils {
|
|
|
23148
23419
|
bt.loggerService.info(HEAT_METHOD_NAME_DUMP, { strategyName: context.strategyName, path });
|
|
23149
23420
|
bt.strategyValidationService.validate(context.strategyName, HEAT_METHOD_NAME_DUMP);
|
|
23150
23421
|
bt.exchangeValidationService.validate(context.exchangeName, HEAT_METHOD_NAME_DUMP);
|
|
23422
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, HEAT_METHOD_NAME_DUMP);
|
|
23151
23423
|
{
|
|
23152
23424
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
23153
23425
|
riskName && bt.riskValidationService.validate(riskName, HEAT_METHOD_NAME_DUMP);
|
|
@@ -23481,6 +23753,7 @@ class PartialUtils {
|
|
|
23481
23753
|
bt.loggerService.info(PARTIAL_METHOD_NAME_GET_DATA, { symbol, strategyName: context.strategyName });
|
|
23482
23754
|
bt.strategyValidationService.validate(context.strategyName, PARTIAL_METHOD_NAME_GET_DATA);
|
|
23483
23755
|
bt.exchangeValidationService.validate(context.exchangeName, PARTIAL_METHOD_NAME_GET_DATA);
|
|
23756
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, PARTIAL_METHOD_NAME_GET_DATA);
|
|
23484
23757
|
{
|
|
23485
23758
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
23486
23759
|
riskName && bt.riskValidationService.validate(riskName, PARTIAL_METHOD_NAME_GET_DATA);
|
|
@@ -23531,6 +23804,7 @@ class PartialUtils {
|
|
|
23531
23804
|
bt.loggerService.info(PARTIAL_METHOD_NAME_GET_REPORT, { symbol, strategyName: context.strategyName });
|
|
23532
23805
|
bt.strategyValidationService.validate(context.strategyName, PARTIAL_METHOD_NAME_GET_REPORT);
|
|
23533
23806
|
bt.exchangeValidationService.validate(context.exchangeName, PARTIAL_METHOD_NAME_GET_REPORT);
|
|
23807
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, PARTIAL_METHOD_NAME_GET_REPORT);
|
|
23534
23808
|
{
|
|
23535
23809
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
23536
23810
|
riskName && bt.riskValidationService.validate(riskName, PARTIAL_METHOD_NAME_GET_REPORT);
|
|
@@ -23574,6 +23848,7 @@ class PartialUtils {
|
|
|
23574
23848
|
bt.loggerService.info(PARTIAL_METHOD_NAME_DUMP, { symbol, strategyName: context.strategyName, path });
|
|
23575
23849
|
bt.strategyValidationService.validate(context.strategyName, PARTIAL_METHOD_NAME_DUMP);
|
|
23576
23850
|
bt.exchangeValidationService.validate(context.exchangeName, PARTIAL_METHOD_NAME_DUMP);
|
|
23851
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, PARTIAL_METHOD_NAME_DUMP);
|
|
23577
23852
|
{
|
|
23578
23853
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
23579
23854
|
riskName && bt.riskValidationService.validate(riskName, PARTIAL_METHOD_NAME_DUMP);
|
|
@@ -24764,6 +25039,7 @@ class BreakevenUtils {
|
|
|
24764
25039
|
bt.loggerService.info(BREAKEVEN_METHOD_NAME_GET_DATA, { symbol, strategyName: context.strategyName });
|
|
24765
25040
|
bt.strategyValidationService.validate(context.strategyName, BREAKEVEN_METHOD_NAME_GET_DATA);
|
|
24766
25041
|
bt.exchangeValidationService.validate(context.exchangeName, BREAKEVEN_METHOD_NAME_GET_DATA);
|
|
25042
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, BREAKEVEN_METHOD_NAME_GET_DATA);
|
|
24767
25043
|
{
|
|
24768
25044
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
24769
25045
|
riskName && bt.riskValidationService.validate(riskName, BREAKEVEN_METHOD_NAME_GET_DATA);
|
|
@@ -24810,6 +25086,7 @@ class BreakevenUtils {
|
|
|
24810
25086
|
bt.loggerService.info(BREAKEVEN_METHOD_NAME_GET_REPORT, { symbol, strategyName: context.strategyName });
|
|
24811
25087
|
bt.strategyValidationService.validate(context.strategyName, BREAKEVEN_METHOD_NAME_GET_REPORT);
|
|
24812
25088
|
bt.exchangeValidationService.validate(context.exchangeName, BREAKEVEN_METHOD_NAME_GET_REPORT);
|
|
25089
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, BREAKEVEN_METHOD_NAME_GET_REPORT);
|
|
24813
25090
|
{
|
|
24814
25091
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
24815
25092
|
riskName && bt.riskValidationService.validate(riskName, BREAKEVEN_METHOD_NAME_GET_REPORT);
|
|
@@ -24853,6 +25130,7 @@ class BreakevenUtils {
|
|
|
24853
25130
|
bt.loggerService.info(BREAKEVEN_METHOD_NAME_DUMP, { symbol, strategyName: context.strategyName, path });
|
|
24854
25131
|
bt.strategyValidationService.validate(context.strategyName, BREAKEVEN_METHOD_NAME_DUMP);
|
|
24855
25132
|
bt.exchangeValidationService.validate(context.exchangeName, BREAKEVEN_METHOD_NAME_DUMP);
|
|
25133
|
+
context.frameName && bt.frameValidationService.validate(context.frameName, BREAKEVEN_METHOD_NAME_DUMP);
|
|
24856
25134
|
{
|
|
24857
25135
|
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
24858
25136
|
riskName && bt.riskValidationService.validate(riskName, BREAKEVEN_METHOD_NAME_DUMP);
|
|
@@ -24969,6 +25247,6 @@ exports.setColumns = setColumns;
|
|
|
24969
25247
|
exports.setConfig = setConfig;
|
|
24970
25248
|
exports.setLogger = setLogger;
|
|
24971
25249
|
exports.stop = stop;
|
|
24972
|
-
exports.trailingProfit = trailingProfit;
|
|
24973
25250
|
exports.trailingStop = trailingStop;
|
|
25251
|
+
exports.trailingTake = trailingTake;
|
|
24974
25252
|
exports.validate = validate;
|