backtest-kit 1.10.2 → 1.10.3
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/build/index.cjs +537 -67
- package/build/index.mjs +537 -68
- package/package.json +1 -1
- package/types.d.ts +200 -9
package/build/index.cjs
CHANGED
|
@@ -2124,8 +2124,10 @@ const TIMEOUT_SYMBOL = Symbol('timeout');
|
|
|
2124
2124
|
* It hides internal implementation details while exposing effective values:
|
|
2125
2125
|
*
|
|
2126
2126
|
* - Replaces internal _trailingPriceStopLoss with effective priceStopLoss
|
|
2127
|
+
* - Replaces internal _trailingPriceTakeProfit with effective priceTakeProfit
|
|
2127
2128
|
* - Preserves original stop-loss in originalPriceStopLoss for reference
|
|
2128
|
-
* -
|
|
2129
|
+
* - Preserves original take-profit in originalPriceTakeProfit for reference
|
|
2130
|
+
* - Ensures external code never sees private _trailing* fields
|
|
2129
2131
|
* - Maintains backward compatibility with non-trailing positions
|
|
2130
2132
|
*
|
|
2131
2133
|
* Key differences from TO_RISK_SIGNAL (in ClientRisk.ts):
|
|
@@ -2140,34 +2142,37 @@ const TIMEOUT_SYMBOL = Symbol('timeout');
|
|
|
2140
2142
|
* - Event emissions and logging
|
|
2141
2143
|
* - Integration with ClientPartial and ClientRisk
|
|
2142
2144
|
*
|
|
2143
|
-
* @param signal - Internal signal row with optional trailing stop-loss
|
|
2144
|
-
* @returns Signal in IPublicSignalRow format with effective
|
|
2145
|
+
* @param signal - Internal signal row with optional trailing stop-loss/take-profit
|
|
2146
|
+
* @returns Signal in IPublicSignalRow format with effective SL/TP and hidden internals
|
|
2145
2147
|
*
|
|
2146
2148
|
* @example
|
|
2147
2149
|
* ```typescript
|
|
2148
|
-
* // Signal without trailing SL
|
|
2150
|
+
* // Signal without trailing SL/TP
|
|
2149
2151
|
* const publicSignal = TO_PUBLIC_SIGNAL(signal);
|
|
2150
2152
|
* // publicSignal.priceStopLoss = signal.priceStopLoss
|
|
2153
|
+
* // publicSignal.priceTakeProfit = signal.priceTakeProfit
|
|
2151
2154
|
* // publicSignal.originalPriceStopLoss = signal.priceStopLoss
|
|
2155
|
+
* // publicSignal.originalPriceTakeProfit = signal.priceTakeProfit
|
|
2152
2156
|
*
|
|
2153
|
-
* // Signal with trailing SL
|
|
2157
|
+
* // Signal with trailing SL/TP
|
|
2154
2158
|
* const publicSignal = TO_PUBLIC_SIGNAL(signalWithTrailing);
|
|
2155
2159
|
* // publicSignal.priceStopLoss = signal._trailingPriceStopLoss (effective)
|
|
2160
|
+
* // publicSignal.priceTakeProfit = signal._trailingPriceTakeProfit (effective)
|
|
2156
2161
|
* // publicSignal.originalPriceStopLoss = signal.priceStopLoss (original)
|
|
2162
|
+
* // publicSignal.originalPriceTakeProfit = signal.priceTakeProfit (original)
|
|
2157
2163
|
* // publicSignal._trailingPriceStopLoss = undefined (hidden from external API)
|
|
2164
|
+
* // publicSignal._trailingPriceTakeProfit = undefined (hidden from external API)
|
|
2158
2165
|
* ```
|
|
2159
2166
|
*/
|
|
2160
2167
|
const TO_PUBLIC_SIGNAL = (signal) => {
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
...structuredClone(signal),
|
|
2164
|
-
priceStopLoss: signal._trailingPriceStopLoss,
|
|
2165
|
-
originalPriceStopLoss: signal.priceStopLoss,
|
|
2166
|
-
};
|
|
2167
|
-
}
|
|
2168
|
+
const hasTrailingSL = signal._trailingPriceStopLoss !== undefined;
|
|
2169
|
+
const hasTrailingTP = signal._trailingPriceTakeProfit !== undefined;
|
|
2168
2170
|
return {
|
|
2169
2171
|
...structuredClone(signal),
|
|
2172
|
+
priceStopLoss: hasTrailingSL ? signal._trailingPriceStopLoss : signal.priceStopLoss,
|
|
2173
|
+
priceTakeProfit: hasTrailingTP ? signal._trailingPriceTakeProfit : signal.priceTakeProfit,
|
|
2170
2174
|
originalPriceStopLoss: signal.priceStopLoss,
|
|
2175
|
+
originalPriceTakeProfit: signal.priceTakeProfit,
|
|
2171
2176
|
};
|
|
2172
2177
|
};
|
|
2173
2178
|
const VALIDATE_SIGNAL_FN = (signal, currentPrice, isScheduled) => {
|
|
@@ -2672,33 +2677,29 @@ const PARTIAL_LOSS_FN = (self, signal, percentToClose, currentPrice) => {
|
|
|
2672
2677
|
});
|
|
2673
2678
|
};
|
|
2674
2679
|
const TRAILING_STOP_FN = (self, signal, percentShift) => {
|
|
2675
|
-
//
|
|
2676
|
-
const
|
|
2677
|
-
// Calculate
|
|
2680
|
+
// Get current effective stop-loss (trailing or original)
|
|
2681
|
+
const currentStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
|
|
2682
|
+
// Calculate distance between entry and CURRENT stop-loss AS PERCENTAGE of entry price
|
|
2683
|
+
const currentSlDistancePercent = Math.abs((signal.priceOpen - currentStopLoss) / signal.priceOpen * 100);
|
|
2684
|
+
// Calculate new stop-loss distance percentage by adding shift to CURRENT distance
|
|
2678
2685
|
// Negative percentShift: reduces distance % (tightens stop, moves SL toward entry or beyond)
|
|
2679
2686
|
// Positive percentShift: increases distance % (loosens stop, moves SL away from entry)
|
|
2680
|
-
const newSlDistancePercent =
|
|
2687
|
+
const newSlDistancePercent = currentSlDistancePercent + percentShift;
|
|
2681
2688
|
// Calculate new stop-loss price based on new distance percentage
|
|
2682
2689
|
// Negative newSlDistancePercent means SL crosses entry into profit zone
|
|
2683
2690
|
let newStopLoss;
|
|
2684
2691
|
if (signal.position === "long") {
|
|
2685
2692
|
// LONG: SL is below entry (or above entry if in profit zone)
|
|
2686
2693
|
// Formula: entry * (1 - newDistance%)
|
|
2687
|
-
// Example: entry=100,
|
|
2688
|
-
// Example: entry=100, originalSL=90 (10%), shift=-5% → newDistance=5% → 100 * 0.95 = 95 (tighter)
|
|
2689
|
-
// Example: entry=100, originalSL=90 (10%), shift=+5% → newDistance=15% → 100 * 0.85 = 85 (looser)
|
|
2694
|
+
// Example: entry=100, currentSL=95 (5%), shift=-3% → newDistance=2% → 100 * 0.98 = 98 (tighter)
|
|
2690
2695
|
newStopLoss = signal.priceOpen * (1 - newSlDistancePercent / 100);
|
|
2691
2696
|
}
|
|
2692
2697
|
else {
|
|
2693
2698
|
// SHORT: SL is above entry (or below entry if in profit zone)
|
|
2694
2699
|
// Formula: entry * (1 + newDistance%)
|
|
2695
|
-
// Example: entry=100,
|
|
2696
|
-
// Example: entry=100, originalSL=110 (10%), shift=-5% → newDistance=5% → 100 * 1.05 = 105 (tighter)
|
|
2697
|
-
// Example: entry=100, originalSL=110 (10%), shift=+5% → newDistance=15% → 100 * 1.15 = 115 (looser)
|
|
2700
|
+
// Example: entry=100, currentSL=105 (5%), shift=-3% → newDistance=2% → 100 * 1.02 = 102 (tighter)
|
|
2698
2701
|
newStopLoss = signal.priceOpen * (1 + newSlDistancePercent / 100);
|
|
2699
2702
|
}
|
|
2700
|
-
// Get current effective stop-loss (trailing or original)
|
|
2701
|
-
const currentStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
|
|
2702
2703
|
// Determine if this is the first trailing stop call (direction not set yet)
|
|
2703
2704
|
const isFirstCall = signal._trailingPriceStopLoss === undefined;
|
|
2704
2705
|
if (isFirstCall) {
|
|
@@ -2709,7 +2710,7 @@ const TRAILING_STOP_FN = (self, signal, percentShift) => {
|
|
|
2709
2710
|
position: signal.position,
|
|
2710
2711
|
priceOpen: signal.priceOpen,
|
|
2711
2712
|
originalStopLoss: signal.priceStopLoss,
|
|
2712
|
-
|
|
2713
|
+
currentDistancePercent: currentSlDistancePercent,
|
|
2713
2714
|
previousStopLoss: currentStopLoss,
|
|
2714
2715
|
newStopLoss,
|
|
2715
2716
|
newDistancePercent: newSlDistancePercent,
|
|
@@ -2720,19 +2721,28 @@ const TRAILING_STOP_FN = (self, signal, percentShift) => {
|
|
|
2720
2721
|
}
|
|
2721
2722
|
else {
|
|
2722
2723
|
// Subsequent calls: only update if new SL continues in the same direction
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
if (initialDirection === "up" && movingUp) {
|
|
2729
|
-
// Direction is UP, and new SL continues moving up
|
|
2730
|
-
shouldUpdate = true;
|
|
2731
|
-
}
|
|
2732
|
-
else if (initialDirection === "down" && movingDown) {
|
|
2733
|
-
// Direction is DOWN, and new SL continues moving down
|
|
2734
|
-
shouldUpdate = true;
|
|
2724
|
+
// Determine initial direction: "closer" or "farther" relative to entry
|
|
2725
|
+
let initialDirection;
|
|
2726
|
+
if (signal.position === "long") {
|
|
2727
|
+
// LONG: closer = SL closer to entry = higher SL value (moving up)
|
|
2728
|
+
initialDirection = signal._trailingPriceStopLoss > signal.priceStopLoss ? "closer" : "farther";
|
|
2735
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;
|
|
2736
|
+
if (signal.position === "long") {
|
|
2737
|
+
// LONG: closer = higher SL value
|
|
2738
|
+
newDirection = newStopLoss > currentStopLoss ? "closer" : "farther";
|
|
2739
|
+
}
|
|
2740
|
+
else {
|
|
2741
|
+
// SHORT: closer = lower SL value
|
|
2742
|
+
newDirection = newStopLoss < currentStopLoss ? "closer" : "farther";
|
|
2743
|
+
}
|
|
2744
|
+
// Only allow continuation in same direction
|
|
2745
|
+
const shouldUpdate = initialDirection === newDirection;
|
|
2736
2746
|
if (!shouldUpdate) {
|
|
2737
2747
|
self.params.logger.debug("TRAILING_STOP_FN: new SL not in same direction, skipping", {
|
|
2738
2748
|
signalId: signal.id,
|
|
@@ -2741,7 +2751,7 @@ const TRAILING_STOP_FN = (self, signal, percentShift) => {
|
|
|
2741
2751
|
newStopLoss,
|
|
2742
2752
|
percentShift,
|
|
2743
2753
|
initialDirection,
|
|
2744
|
-
attemptedDirection:
|
|
2754
|
+
attemptedDirection: newDirection,
|
|
2745
2755
|
});
|
|
2746
2756
|
return;
|
|
2747
2757
|
}
|
|
@@ -2752,7 +2762,7 @@ const TRAILING_STOP_FN = (self, signal, percentShift) => {
|
|
|
2752
2762
|
position: signal.position,
|
|
2753
2763
|
priceOpen: signal.priceOpen,
|
|
2754
2764
|
originalStopLoss: signal.priceStopLoss,
|
|
2755
|
-
|
|
2765
|
+
currentDistancePercent: currentSlDistancePercent,
|
|
2756
2766
|
previousStopLoss: currentStopLoss,
|
|
2757
2767
|
newStopLoss,
|
|
2758
2768
|
newDistancePercent: newSlDistancePercent,
|
|
@@ -2762,6 +2772,99 @@ const TRAILING_STOP_FN = (self, signal, percentShift) => {
|
|
|
2762
2772
|
});
|
|
2763
2773
|
}
|
|
2764
2774
|
};
|
|
2775
|
+
const TRAILING_PROFIT_FN = (self, signal, percentShift) => {
|
|
2776
|
+
// Get current effective take-profit (trailing or original)
|
|
2777
|
+
const currentTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
|
|
2778
|
+
// Calculate distance between entry and CURRENT take-profit AS PERCENTAGE of entry price
|
|
2779
|
+
const currentTpDistancePercent = Math.abs((currentTakeProfit - signal.priceOpen) / signal.priceOpen * 100);
|
|
2780
|
+
// Calculate new take-profit distance percentage by adding shift to CURRENT distance
|
|
2781
|
+
// Negative percentShift: reduces distance % (brings TP closer to entry)
|
|
2782
|
+
// Positive percentShift: increases distance % (moves TP further from entry)
|
|
2783
|
+
const newTpDistancePercent = currentTpDistancePercent + percentShift;
|
|
2784
|
+
// Calculate new take-profit price based on new distance percentage
|
|
2785
|
+
let newTakeProfit;
|
|
2786
|
+
if (signal.position === "long") {
|
|
2787
|
+
// LONG: TP is above entry
|
|
2788
|
+
// Formula: entry * (1 + newDistance%)
|
|
2789
|
+
// Example: entry=100, currentTP=115 (15%), shift=-3% → newDistance=12% → 100 * 1.12 = 112 (closer)
|
|
2790
|
+
newTakeProfit = signal.priceOpen * (1 + newTpDistancePercent / 100);
|
|
2791
|
+
}
|
|
2792
|
+
else {
|
|
2793
|
+
// SHORT: TP is below entry
|
|
2794
|
+
// Formula: entry * (1 - newDistance%)
|
|
2795
|
+
// Example: entry=100, currentTP=85 (15%), shift=-3% → newDistance=12% → 100 * 0.88 = 88 (closer)
|
|
2796
|
+
newTakeProfit = signal.priceOpen * (1 - newTpDistancePercent / 100);
|
|
2797
|
+
}
|
|
2798
|
+
// Determine if this is the first trailing profit call (direction not set yet)
|
|
2799
|
+
const isFirstCall = signal._trailingPriceTakeProfit === undefined;
|
|
2800
|
+
if (isFirstCall) {
|
|
2801
|
+
// First call: set the direction and update TP unconditionally
|
|
2802
|
+
signal._trailingPriceTakeProfit = newTakeProfit;
|
|
2803
|
+
self.params.logger.info("TRAILING_PROFIT_FN executed (first call - direction set)", {
|
|
2804
|
+
signalId: signal.id,
|
|
2805
|
+
position: signal.position,
|
|
2806
|
+
priceOpen: signal.priceOpen,
|
|
2807
|
+
originalTakeProfit: signal.priceTakeProfit,
|
|
2808
|
+
currentDistancePercent: currentTpDistancePercent,
|
|
2809
|
+
previousTakeProfit: currentTakeProfit,
|
|
2810
|
+
newTakeProfit,
|
|
2811
|
+
newDistancePercent: newTpDistancePercent,
|
|
2812
|
+
percentShift,
|
|
2813
|
+
direction: newTakeProfit > currentTakeProfit ? "up" : "down",
|
|
2814
|
+
});
|
|
2815
|
+
}
|
|
2816
|
+
else {
|
|
2817
|
+
// Subsequent calls: only update if new TP continues in the same direction
|
|
2818
|
+
// Determine initial direction: "closer" or "farther" relative to entry
|
|
2819
|
+
let initialDirection;
|
|
2820
|
+
if (signal.position === "long") {
|
|
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;
|
|
2830
|
+
if (signal.position === "long") {
|
|
2831
|
+
// LONG: closer = lower TP value
|
|
2832
|
+
newDirection = newTakeProfit < currentTakeProfit ? "closer" : "farther";
|
|
2833
|
+
}
|
|
2834
|
+
else {
|
|
2835
|
+
// SHORT: closer = higher TP value
|
|
2836
|
+
newDirection = newTakeProfit > currentTakeProfit ? "closer" : "farther";
|
|
2837
|
+
}
|
|
2838
|
+
// Only allow continuation in same direction
|
|
2839
|
+
const shouldUpdate = initialDirection === newDirection;
|
|
2840
|
+
if (!shouldUpdate) {
|
|
2841
|
+
self.params.logger.debug("TRAILING_PROFIT_FN: new TP not in same direction, skipping", {
|
|
2842
|
+
signalId: signal.id,
|
|
2843
|
+
position: signal.position,
|
|
2844
|
+
currentTakeProfit,
|
|
2845
|
+
newTakeProfit,
|
|
2846
|
+
percentShift,
|
|
2847
|
+
initialDirection,
|
|
2848
|
+
attemptedDirection: newDirection,
|
|
2849
|
+
});
|
|
2850
|
+
return;
|
|
2851
|
+
}
|
|
2852
|
+
// Update trailing take-profit
|
|
2853
|
+
signal._trailingPriceTakeProfit = newTakeProfit;
|
|
2854
|
+
self.params.logger.info("TRAILING_PROFIT_FN executed", {
|
|
2855
|
+
signalId: signal.id,
|
|
2856
|
+
position: signal.position,
|
|
2857
|
+
priceOpen: signal.priceOpen,
|
|
2858
|
+
originalTakeProfit: signal.priceTakeProfit,
|
|
2859
|
+
currentDistancePercent: currentTpDistancePercent,
|
|
2860
|
+
previousTakeProfit: currentTakeProfit,
|
|
2861
|
+
newTakeProfit,
|
|
2862
|
+
newDistancePercent: newTpDistancePercent,
|
|
2863
|
+
percentShift,
|
|
2864
|
+
direction: initialDirection,
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
};
|
|
2765
2868
|
const BREAKEVEN_FN = (self, signal, currentPrice) => {
|
|
2766
2869
|
// Calculate breakeven threshold based on slippage and fees
|
|
2767
2870
|
// Need to cover: entry slippage + entry fee + exit slippage + exit fee
|
|
@@ -3573,13 +3676,14 @@ const CHECK_PENDING_SIGNAL_COMPLETION_FN = async (self, signal, averagePrice) =>
|
|
|
3573
3676
|
if (elapsedTime >= maxTimeToWait) {
|
|
3574
3677
|
return await CLOSE_PENDING_SIGNAL_FN(self, signal, averagePrice, "time_expired");
|
|
3575
3678
|
}
|
|
3576
|
-
// Check take profit
|
|
3577
|
-
|
|
3578
|
-
|
|
3679
|
+
// Check take profit (use trailing TP if set, otherwise original TP)
|
|
3680
|
+
const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
|
|
3681
|
+
if (signal.position === "long" && averagePrice >= effectiveTakeProfit) {
|
|
3682
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, effectiveTakeProfit, // КРИТИЧНО: используем точную цену TP
|
|
3579
3683
|
"take_profit");
|
|
3580
3684
|
}
|
|
3581
|
-
if (signal.position === "short" && averagePrice <=
|
|
3582
|
-
return await CLOSE_PENDING_SIGNAL_FN(self, signal,
|
|
3685
|
+
if (signal.position === "short" && averagePrice <= effectiveTakeProfit) {
|
|
3686
|
+
return await CLOSE_PENDING_SIGNAL_FN(self, signal, effectiveTakeProfit, // КРИТИЧНО: используем точную цену TP
|
|
3583
3687
|
"take_profit");
|
|
3584
3688
|
}
|
|
3585
3689
|
// Check stop loss (use trailing SL if set, otherwise original SL)
|
|
@@ -3641,8 +3745,9 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
3641
3745
|
await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
3642
3746
|
}
|
|
3643
3747
|
if (currentDistance > 0) {
|
|
3644
|
-
// Moving towards TP
|
|
3645
|
-
const
|
|
3748
|
+
// Moving towards TP (use trailing TP if set)
|
|
3749
|
+
const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
|
|
3750
|
+
const tpDistance = effectiveTakeProfit - signal.priceOpen;
|
|
3646
3751
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
3647
3752
|
percentTp = Math.min(progressPercent, 100);
|
|
3648
3753
|
await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
|
|
@@ -3664,8 +3769,9 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
|
|
|
3664
3769
|
await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
|
|
3665
3770
|
}
|
|
3666
3771
|
if (currentDistance > 0) {
|
|
3667
|
-
// Moving towards TP
|
|
3668
|
-
const
|
|
3772
|
+
// Moving towards TP (use trailing TP if set)
|
|
3773
|
+
const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
|
|
3774
|
+
const tpDistance = signal.priceOpen - effectiveTakeProfit;
|
|
3669
3775
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
3670
3776
|
percentTp = Math.min(progressPercent, 100);
|
|
3671
3777
|
await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
|
|
@@ -3927,11 +4033,12 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
3927
4033
|
}
|
|
3928
4034
|
// Check TP/SL only if not expired
|
|
3929
4035
|
// КРИТИЧНО: используем averagePrice (VWAP) для проверки достижения TP/SL (как в live mode)
|
|
3930
|
-
// КРИТИЧНО: используем trailing SL если
|
|
4036
|
+
// КРИТИЧНО: используем trailing SL и TP если установлены
|
|
3931
4037
|
const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
|
|
4038
|
+
const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
|
|
3932
4039
|
if (!shouldClose && signal.position === "long") {
|
|
3933
4040
|
// Для LONG: TP срабатывает если VWAP >= TP, SL если VWAP <= SL
|
|
3934
|
-
if (averagePrice >=
|
|
4041
|
+
if (averagePrice >= effectiveTakeProfit) {
|
|
3935
4042
|
shouldClose = true;
|
|
3936
4043
|
closeReason = "take_profit";
|
|
3937
4044
|
}
|
|
@@ -3942,7 +4049,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
3942
4049
|
}
|
|
3943
4050
|
if (!shouldClose && signal.position === "short") {
|
|
3944
4051
|
// Для SHORT: TP срабатывает если VWAP <= TP, SL если VWAP >= SL
|
|
3945
|
-
if (averagePrice <=
|
|
4052
|
+
if (averagePrice <= effectiveTakeProfit) {
|
|
3946
4053
|
shouldClose = true;
|
|
3947
4054
|
closeReason = "take_profit";
|
|
3948
4055
|
}
|
|
@@ -3955,7 +4062,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
3955
4062
|
// КРИТИЧНО: используем точную цену TP/SL для закрытия (как в live mode)
|
|
3956
4063
|
let closePrice;
|
|
3957
4064
|
if (closeReason === "take_profit") {
|
|
3958
|
-
closePrice =
|
|
4065
|
+
closePrice = effectiveTakeProfit; // используем trailing TP если установлен
|
|
3959
4066
|
}
|
|
3960
4067
|
else if (closeReason === "stop_loss") {
|
|
3961
4068
|
closePrice = effectiveStopLoss;
|
|
@@ -3976,8 +4083,9 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
3976
4083
|
await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, averagePrice, currentCandleTimestamp, self.params.execution.context.backtest);
|
|
3977
4084
|
}
|
|
3978
4085
|
if (currentDistance > 0) {
|
|
3979
|
-
// Moving towards TP
|
|
3980
|
-
const
|
|
4086
|
+
// Moving towards TP (use trailing TP if set)
|
|
4087
|
+
const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
|
|
4088
|
+
const tpDistance = effectiveTakeProfit - signal.priceOpen;
|
|
3981
4089
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
3982
4090
|
await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
|
|
3983
4091
|
}
|
|
@@ -3997,8 +4105,9 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
|
|
|
3997
4105
|
await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, averagePrice, currentCandleTimestamp, self.params.execution.context.backtest);
|
|
3998
4106
|
}
|
|
3999
4107
|
if (currentDistance > 0) {
|
|
4000
|
-
// Moving towards TP
|
|
4001
|
-
const
|
|
4108
|
+
// Moving towards TP (use trailing TP if set)
|
|
4109
|
+
const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
|
|
4110
|
+
const tpDistance = signal.priceOpen - effectiveTakeProfit;
|
|
4002
4111
|
const progressPercent = (currentDistance / tpDistance) * 100;
|
|
4003
4112
|
await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, averagePrice, Math.min(progressPercent, 100), currentCandleTimestamp, self.params.execution.context.backtest);
|
|
4004
4113
|
}
|
|
@@ -4983,6 +5092,32 @@ class ClientStrategy {
|
|
|
4983
5092
|
});
|
|
4984
5093
|
return;
|
|
4985
5094
|
}
|
|
5095
|
+
// Check for conflict with existing trailing take profit
|
|
5096
|
+
const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
|
|
5097
|
+
if (signal.position === "long" && newStopLoss >= effectiveTakeProfit) {
|
|
5098
|
+
// LONG: New SL would be at or above current TP - invalid configuration
|
|
5099
|
+
this.params.logger.debug("ClientStrategy trailingStop: SL/TP conflict detected, skipping SL update", {
|
|
5100
|
+
signalId: signal.id,
|
|
5101
|
+
position: signal.position,
|
|
5102
|
+
priceOpen: signal.priceOpen,
|
|
5103
|
+
newStopLoss,
|
|
5104
|
+
effectiveTakeProfit,
|
|
5105
|
+
reason: "newStopLoss >= effectiveTakeProfit (LONG position)"
|
|
5106
|
+
});
|
|
5107
|
+
return;
|
|
5108
|
+
}
|
|
5109
|
+
if (signal.position === "short" && newStopLoss <= effectiveTakeProfit) {
|
|
5110
|
+
// SHORT: New SL would be at or below current TP - invalid configuration
|
|
5111
|
+
this.params.logger.debug("ClientStrategy trailingStop: SL/TP conflict detected, skipping SL update", {
|
|
5112
|
+
signalId: signal.id,
|
|
5113
|
+
position: signal.position,
|
|
5114
|
+
priceOpen: signal.priceOpen,
|
|
5115
|
+
newStopLoss,
|
|
5116
|
+
effectiveTakeProfit,
|
|
5117
|
+
reason: "newStopLoss <= effectiveTakeProfit (SHORT position)"
|
|
5118
|
+
});
|
|
5119
|
+
return;
|
|
5120
|
+
}
|
|
4986
5121
|
// Execute trailing logic
|
|
4987
5122
|
TRAILING_STOP_FN(this, this._pendingSignal, percentShift);
|
|
4988
5123
|
// Persist updated signal state (inline setPendingSignal content)
|
|
@@ -4999,6 +5134,136 @@ class ClientStrategy {
|
|
|
4999
5134
|
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
|
|
5000
5135
|
}
|
|
5001
5136
|
}
|
|
5137
|
+
/**
|
|
5138
|
+
* Adjusts the trailing take-profit distance for an active pending signal.
|
|
5139
|
+
*
|
|
5140
|
+
* Updates the take-profit distance by a percentage adjustment relative to the original TP distance.
|
|
5141
|
+
* Negative percentShift brings TP closer to entry, positive percentShift moves it further.
|
|
5142
|
+
* Once direction is set on first call, subsequent calls must continue in same direction.
|
|
5143
|
+
*
|
|
5144
|
+
* Price intrusion protection: If current price has already crossed the new TP level,
|
|
5145
|
+
* the update is skipped to prevent immediate TP triggering.
|
|
5146
|
+
*
|
|
5147
|
+
* @param symbol - Trading pair symbol
|
|
5148
|
+
* @param percentShift - Percentage adjustment to TP distance (-100 to 100)
|
|
5149
|
+
* @param currentPrice - Current market price to check for intrusion
|
|
5150
|
+
* @param backtest - Whether running in backtest mode
|
|
5151
|
+
* @returns Promise that resolves when trailing TP is updated
|
|
5152
|
+
*
|
|
5153
|
+
* @example
|
|
5154
|
+
* ```typescript
|
|
5155
|
+
* // LONG: entry=100, originalTP=110, distance=10%, currentPrice=102
|
|
5156
|
+
* // Move TP further by 50%: newTP = 100 + 15% = 115
|
|
5157
|
+
* await strategy.trailingProfit("BTCUSDT", 50, 102, false);
|
|
5158
|
+
*
|
|
5159
|
+
* // SHORT: entry=100, originalTP=90, distance=10%, currentPrice=98
|
|
5160
|
+
* // Move TP closer by 30%: newTP = 100 - 7% = 93
|
|
5161
|
+
* await strategy.trailingProfit("BTCUSDT", -30, 98, false);
|
|
5162
|
+
* ```
|
|
5163
|
+
*/
|
|
5164
|
+
async trailingProfit(symbol, percentShift, currentPrice, backtest) {
|
|
5165
|
+
this.params.logger.debug("ClientStrategy trailingProfit", {
|
|
5166
|
+
symbol,
|
|
5167
|
+
percentShift,
|
|
5168
|
+
currentPrice,
|
|
5169
|
+
hasPendingSignal: this._pendingSignal !== null,
|
|
5170
|
+
});
|
|
5171
|
+
// Validation: must have pending signal
|
|
5172
|
+
if (!this._pendingSignal) {
|
|
5173
|
+
throw new Error(`ClientStrategy trailingProfit: No pending signal exists for symbol=${symbol}`);
|
|
5174
|
+
}
|
|
5175
|
+
// Validation: percentShift must be valid
|
|
5176
|
+
if (typeof percentShift !== "number" || !isFinite(percentShift)) {
|
|
5177
|
+
throw new Error(`ClientStrategy trailingProfit: percentShift must be a finite number, got ${percentShift} (${typeof percentShift})`);
|
|
5178
|
+
}
|
|
5179
|
+
if (percentShift < -100 || percentShift > 100) {
|
|
5180
|
+
throw new Error(`ClientStrategy trailingProfit: percentShift must be in range [-100, 100], got ${percentShift}`);
|
|
5181
|
+
}
|
|
5182
|
+
if (percentShift === 0) {
|
|
5183
|
+
throw new Error(`ClientStrategy trailingProfit: percentShift cannot be 0`);
|
|
5184
|
+
}
|
|
5185
|
+
// Validation: currentPrice must be valid
|
|
5186
|
+
if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
|
|
5187
|
+
throw new Error(`ClientStrategy trailingProfit: currentPrice must be a positive finite number, got ${currentPrice}`);
|
|
5188
|
+
}
|
|
5189
|
+
// Calculate what the new take profit would be
|
|
5190
|
+
const signal = this._pendingSignal;
|
|
5191
|
+
const tpDistancePercent = Math.abs((signal.priceTakeProfit - signal.priceOpen) / signal.priceOpen * 100);
|
|
5192
|
+
const newTpDistancePercent = tpDistancePercent + percentShift;
|
|
5193
|
+
let newTakeProfit;
|
|
5194
|
+
if (signal.position === "long") {
|
|
5195
|
+
newTakeProfit = signal.priceOpen * (1 + newTpDistancePercent / 100);
|
|
5196
|
+
}
|
|
5197
|
+
else {
|
|
5198
|
+
newTakeProfit = signal.priceOpen * (1 - newTpDistancePercent / 100);
|
|
5199
|
+
}
|
|
5200
|
+
// Check for price intrusion before executing trailing logic
|
|
5201
|
+
if (signal.position === "long" && currentPrice > newTakeProfit) {
|
|
5202
|
+
// LONG: Price already crossed the new take profit level - skip setting TP
|
|
5203
|
+
this.params.logger.debug("ClientStrategy trailingProfit: price intrusion detected, skipping TP update", {
|
|
5204
|
+
signalId: signal.id,
|
|
5205
|
+
position: signal.position,
|
|
5206
|
+
priceOpen: signal.priceOpen,
|
|
5207
|
+
newTakeProfit,
|
|
5208
|
+
currentPrice,
|
|
5209
|
+
reason: "currentPrice above newTakeProfit (LONG position)"
|
|
5210
|
+
});
|
|
5211
|
+
return;
|
|
5212
|
+
}
|
|
5213
|
+
if (signal.position === "short" && currentPrice < newTakeProfit) {
|
|
5214
|
+
// SHORT: Price already crossed the new take profit level - skip setting TP
|
|
5215
|
+
this.params.logger.debug("ClientStrategy trailingProfit: price intrusion detected, skipping TP update", {
|
|
5216
|
+
signalId: signal.id,
|
|
5217
|
+
position: signal.position,
|
|
5218
|
+
priceOpen: signal.priceOpen,
|
|
5219
|
+
newTakeProfit,
|
|
5220
|
+
currentPrice,
|
|
5221
|
+
reason: "currentPrice below newTakeProfit (SHORT position)"
|
|
5222
|
+
});
|
|
5223
|
+
return;
|
|
5224
|
+
}
|
|
5225
|
+
// Check for conflict with existing trailing stop loss
|
|
5226
|
+
const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
|
|
5227
|
+
if (signal.position === "long" && newTakeProfit <= effectiveStopLoss) {
|
|
5228
|
+
// LONG: New TP would be at or below current SL - invalid configuration
|
|
5229
|
+
this.params.logger.debug("ClientStrategy trailingProfit: TP/SL conflict detected, skipping TP update", {
|
|
5230
|
+
signalId: signal.id,
|
|
5231
|
+
position: signal.position,
|
|
5232
|
+
priceOpen: signal.priceOpen,
|
|
5233
|
+
newTakeProfit,
|
|
5234
|
+
effectiveStopLoss,
|
|
5235
|
+
reason: "newTakeProfit <= effectiveStopLoss (LONG position)"
|
|
5236
|
+
});
|
|
5237
|
+
return;
|
|
5238
|
+
}
|
|
5239
|
+
if (signal.position === "short" && newTakeProfit >= effectiveStopLoss) {
|
|
5240
|
+
// SHORT: New TP would be at or above current SL - invalid configuration
|
|
5241
|
+
this.params.logger.debug("ClientStrategy trailingProfit: TP/SL conflict detected, skipping TP update", {
|
|
5242
|
+
signalId: signal.id,
|
|
5243
|
+
position: signal.position,
|
|
5244
|
+
priceOpen: signal.priceOpen,
|
|
5245
|
+
newTakeProfit,
|
|
5246
|
+
effectiveStopLoss,
|
|
5247
|
+
reason: "newTakeProfit >= effectiveStopLoss (SHORT position)"
|
|
5248
|
+
});
|
|
5249
|
+
return;
|
|
5250
|
+
}
|
|
5251
|
+
// Execute trailing logic
|
|
5252
|
+
TRAILING_PROFIT_FN(this, this._pendingSignal, percentShift);
|
|
5253
|
+
// Persist updated signal state (inline setPendingSignal content)
|
|
5254
|
+
// Note: this._pendingSignal already mutated by TRAILING_PROFIT_FN, no reassignment needed
|
|
5255
|
+
this.params.logger.debug("ClientStrategy setPendingSignal (inline)", {
|
|
5256
|
+
pendingSignal: this._pendingSignal,
|
|
5257
|
+
});
|
|
5258
|
+
// Call onWrite callback for testing persist storage
|
|
5259
|
+
if (this.params.callbacks?.onWrite) {
|
|
5260
|
+
const publicSignal = TO_PUBLIC_SIGNAL(this._pendingSignal);
|
|
5261
|
+
this.params.callbacks.onWrite(this.params.execution.context.symbol, publicSignal, backtest);
|
|
5262
|
+
}
|
|
5263
|
+
if (!backtest) {
|
|
5264
|
+
await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5002
5267
|
}
|
|
5003
5268
|
|
|
5004
5269
|
const RISK_METHOD_NAME_GET_DATA = "RiskUtils.getData";
|
|
@@ -5787,6 +6052,45 @@ class StrategyConnectionService {
|
|
|
5787
6052
|
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
5788
6053
|
await strategy.trailingStop(symbol, percentShift, currentPrice, backtest);
|
|
5789
6054
|
};
|
|
6055
|
+
/**
|
|
6056
|
+
* Adjusts the trailing take-profit distance for an active pending signal.
|
|
6057
|
+
*
|
|
6058
|
+
* Updates the take-profit distance by a percentage adjustment relative to the original TP distance.
|
|
6059
|
+
* Negative percentShift brings TP closer to entry, positive percentShift moves it further.
|
|
6060
|
+
*
|
|
6061
|
+
* Delegates to ClientStrategy.trailingProfit() with current execution context.
|
|
6062
|
+
*
|
|
6063
|
+
* @param backtest - Whether running in backtest mode
|
|
6064
|
+
* @param symbol - Trading pair symbol
|
|
6065
|
+
* @param percentShift - Percentage adjustment to TP distance (-100 to 100)
|
|
6066
|
+
* @param currentPrice - Current market price to check for intrusion
|
|
6067
|
+
* @param context - Execution context with strategyName, exchangeName, frameName
|
|
6068
|
+
* @returns Promise that resolves when trailing TP is updated
|
|
6069
|
+
*
|
|
6070
|
+
* @example
|
|
6071
|
+
* ```typescript
|
|
6072
|
+
* // LONG: entry=100, originalTP=110, distance=10%, currentPrice=102
|
|
6073
|
+
* // Move TP further by 50%: newTP = 100 + 15% = 115
|
|
6074
|
+
* await strategyConnectionService.trailingProfit(
|
|
6075
|
+
* false,
|
|
6076
|
+
* "BTCUSDT",
|
|
6077
|
+
* 50,
|
|
6078
|
+
* 102,
|
|
6079
|
+
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
|
|
6080
|
+
* );
|
|
6081
|
+
* ```
|
|
6082
|
+
*/
|
|
6083
|
+
this.trailingProfit = async (backtest, symbol, percentShift, currentPrice, context) => {
|
|
6084
|
+
this.loggerService.log("strategyConnectionService trailingProfit", {
|
|
6085
|
+
symbol,
|
|
6086
|
+
context,
|
|
6087
|
+
percentShift,
|
|
6088
|
+
currentPrice,
|
|
6089
|
+
backtest,
|
|
6090
|
+
});
|
|
6091
|
+
const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
|
|
6092
|
+
await strategy.trailingProfit(symbol, percentShift, currentPrice, backtest);
|
|
6093
|
+
};
|
|
5790
6094
|
/**
|
|
5791
6095
|
* Delegates to ClientStrategy.breakeven() with current execution context.
|
|
5792
6096
|
*
|
|
@@ -6256,16 +6560,19 @@ const POSITION_NEED_FETCH = Symbol("risk-need-fetch");
|
|
|
6256
6560
|
*
|
|
6257
6561
|
* - Falls back to currentPrice if priceOpen is not set (for ISignalDto/scheduled signals)
|
|
6258
6562
|
* - Replaces priceStopLoss with trailing SL if active (for positions with trailing stops)
|
|
6563
|
+
* - Replaces priceTakeProfit with trailing TP if active (for positions with trailing take-profit)
|
|
6259
6564
|
* - Preserves original stop-loss in originalPriceStopLoss for reference
|
|
6565
|
+
* - Preserves original take-profit in originalPriceTakeProfit for reference
|
|
6260
6566
|
*
|
|
6261
6567
|
* Use cases:
|
|
6262
6568
|
* - Risk validation before opening a position (checkSignal)
|
|
6263
6569
|
* - Pre-flight validation of scheduled signals
|
|
6264
6570
|
* - Calculating position size based on stop-loss distance
|
|
6571
|
+
* - Calculating risk-reward ratio using effective SL/TP
|
|
6265
6572
|
*
|
|
6266
6573
|
* @param signal - Signal DTO or row (may not have priceOpen for scheduled signals)
|
|
6267
6574
|
* @param currentPrice - Current market price, used as fallback for priceOpen if not set
|
|
6268
|
-
* @returns Signal in IRiskSignalRow format with guaranteed priceOpen and effective
|
|
6575
|
+
* @returns Signal in IRiskSignalRow format with guaranteed priceOpen and effective SL/TP
|
|
6269
6576
|
*
|
|
6270
6577
|
* @example
|
|
6271
6578
|
* ```typescript
|
|
@@ -6273,24 +6580,24 @@ const POSITION_NEED_FETCH = Symbol("risk-need-fetch");
|
|
|
6273
6580
|
* const riskSignal = TO_RISK_SIGNAL(scheduledSignal, 45000);
|
|
6274
6581
|
* // riskSignal.priceOpen = 45000 (fallback to currentPrice)
|
|
6275
6582
|
*
|
|
6276
|
-
* // For signal with trailing SL
|
|
6583
|
+
* // For signal with trailing SL/TP
|
|
6277
6584
|
* const riskSignal = TO_RISK_SIGNAL(activeSignal, 46000);
|
|
6278
|
-
* // riskSignal.priceStopLoss = activeSignal._trailingPriceStopLoss
|
|
6585
|
+
* // riskSignal.priceStopLoss = activeSignal._trailingPriceStopLoss (effective)
|
|
6586
|
+
* // riskSignal.priceTakeProfit = activeSignal._trailingPriceTakeProfit (effective)
|
|
6587
|
+
* // riskSignal.originalPriceStopLoss = activeSignal.priceStopLoss (original)
|
|
6588
|
+
* // riskSignal.originalPriceTakeProfit = activeSignal.priceTakeProfit (original)
|
|
6279
6589
|
* ```
|
|
6280
6590
|
*/
|
|
6281
6591
|
const TO_RISK_SIGNAL = (signal, currentPrice) => {
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
...structuredClone(signal),
|
|
6285
|
-
priceOpen: signal.priceOpen ?? currentPrice,
|
|
6286
|
-
priceStopLoss: signal._trailingPriceStopLoss,
|
|
6287
|
-
originalPriceStopLoss: signal.priceStopLoss,
|
|
6288
|
-
};
|
|
6289
|
-
}
|
|
6592
|
+
const hasTrailingSL = "_trailingPriceStopLoss" in signal && signal._trailingPriceStopLoss !== undefined;
|
|
6593
|
+
const hasTrailingTP = "_trailingPriceTakeProfit" in signal && signal._trailingPriceTakeProfit !== undefined;
|
|
6290
6594
|
return {
|
|
6291
6595
|
...structuredClone(signal),
|
|
6292
6596
|
priceOpen: signal.priceOpen ?? currentPrice,
|
|
6597
|
+
priceStopLoss: hasTrailingSL ? signal._trailingPriceStopLoss : signal.priceStopLoss,
|
|
6598
|
+
priceTakeProfit: hasTrailingTP ? signal._trailingPriceTakeProfit : signal.priceTakeProfit,
|
|
6293
6599
|
originalPriceStopLoss: signal.priceStopLoss,
|
|
6600
|
+
originalPriceTakeProfit: signal.priceTakeProfit,
|
|
6294
6601
|
};
|
|
6295
6602
|
};
|
|
6296
6603
|
/** Key generator for active position map */
|
|
@@ -7223,6 +7530,41 @@ class StrategyCoreService {
|
|
|
7223
7530
|
await this.validate(symbol, context);
|
|
7224
7531
|
return await this.strategyConnectionService.trailingStop(backtest, symbol, percentShift, currentPrice, context);
|
|
7225
7532
|
};
|
|
7533
|
+
/**
|
|
7534
|
+
* Adjusts the trailing take-profit distance for an active pending signal.
|
|
7535
|
+
* Validates context and delegates to StrategyConnectionService.
|
|
7536
|
+
*
|
|
7537
|
+
* @param backtest - Whether running in backtest mode
|
|
7538
|
+
* @param symbol - Trading pair symbol
|
|
7539
|
+
* @param percentShift - Percentage adjustment to TP distance (-100 to 100)
|
|
7540
|
+
* @param currentPrice - Current market price to check for intrusion
|
|
7541
|
+
* @param context - Strategy context with strategyName, exchangeName, frameName
|
|
7542
|
+
* @returns Promise that resolves when trailing TP is updated
|
|
7543
|
+
*
|
|
7544
|
+
* @example
|
|
7545
|
+
* ```typescript
|
|
7546
|
+
* // LONG: entry=100, originalTP=110, distance=10%, currentPrice=102
|
|
7547
|
+
* // Move TP further by 50%: newTP = 100 + 15% = 115
|
|
7548
|
+
* await strategyCoreService.trailingProfit(
|
|
7549
|
+
* false,
|
|
7550
|
+
* "BTCUSDT",
|
|
7551
|
+
* 50,
|
|
7552
|
+
* 102,
|
|
7553
|
+
* { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
|
|
7554
|
+
* );
|
|
7555
|
+
* ```
|
|
7556
|
+
*/
|
|
7557
|
+
this.trailingProfit = async (backtest, symbol, percentShift, currentPrice, context) => {
|
|
7558
|
+
this.loggerService.log("strategyCoreService trailingProfit", {
|
|
7559
|
+
symbol,
|
|
7560
|
+
percentShift,
|
|
7561
|
+
currentPrice,
|
|
7562
|
+
context,
|
|
7563
|
+
backtest,
|
|
7564
|
+
});
|
|
7565
|
+
await this.validate(symbol, context);
|
|
7566
|
+
return await this.strategyConnectionService.trailingProfit(backtest, symbol, percentShift, currentPrice, context);
|
|
7567
|
+
};
|
|
7226
7568
|
/**
|
|
7227
7569
|
* Moves stop-loss to breakeven when price reaches threshold.
|
|
7228
7570
|
* Validates context and delegates to StrategyConnectionService.
|
|
@@ -17926,6 +18268,7 @@ const CANCEL_METHOD_NAME = "strategy.cancel";
|
|
|
17926
18268
|
const PARTIAL_PROFIT_METHOD_NAME = "strategy.partialProfit";
|
|
17927
18269
|
const PARTIAL_LOSS_METHOD_NAME = "strategy.partialLoss";
|
|
17928
18270
|
const TRAILING_STOP_METHOD_NAME = "strategy.trailingStop";
|
|
18271
|
+
const TRAILING_PROFIT_METHOD_NAME = "strategy.trailingProfit";
|
|
17929
18272
|
const BREAKEVEN_METHOD_NAME = "strategy.breakeven";
|
|
17930
18273
|
/**
|
|
17931
18274
|
* Stops the strategy from generating new signals.
|
|
@@ -18121,6 +18464,45 @@ async function trailingStop(symbol, percentShift, currentPrice) {
|
|
|
18121
18464
|
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
18122
18465
|
await bt.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
|
|
18123
18466
|
}
|
|
18467
|
+
/**
|
|
18468
|
+
* Adjusts the trailing take-profit distance for an active pending signal.
|
|
18469
|
+
*
|
|
18470
|
+
* Updates the take-profit distance by a percentage adjustment relative to the original TP distance.
|
|
18471
|
+
* Negative percentShift brings TP closer to entry, positive percentShift moves it further.
|
|
18472
|
+
* Once direction is set on first call, subsequent calls must continue in same direction.
|
|
18473
|
+
*
|
|
18474
|
+
* Automatically detects backtest/live mode from execution context.
|
|
18475
|
+
*
|
|
18476
|
+
* @param symbol - Trading pair symbol
|
|
18477
|
+
* @param percentShift - Percentage adjustment to TP distance (-100 to 100)
|
|
18478
|
+
* @param currentPrice - Current market price to check for intrusion
|
|
18479
|
+
* @returns Promise that resolves when trailing TP is updated
|
|
18480
|
+
*
|
|
18481
|
+
* @example
|
|
18482
|
+
* ```typescript
|
|
18483
|
+
* import { trailingProfit } from "backtest-kit";
|
|
18484
|
+
*
|
|
18485
|
+
* // LONG: entry=100, originalTP=110, distance=10%, currentPrice=102
|
|
18486
|
+
* // Move TP further by 50%: newTP = 100 + 15% = 115
|
|
18487
|
+
* await trailingProfit("BTCUSDT", 50, 102);
|
|
18488
|
+
* ```
|
|
18489
|
+
*/
|
|
18490
|
+
async function trailingProfit(symbol, percentShift, currentPrice) {
|
|
18491
|
+
bt.loggerService.info(TRAILING_PROFIT_METHOD_NAME, {
|
|
18492
|
+
symbol,
|
|
18493
|
+
percentShift,
|
|
18494
|
+
currentPrice,
|
|
18495
|
+
});
|
|
18496
|
+
if (!ExecutionContextService.hasContext()) {
|
|
18497
|
+
throw new Error("trailingProfit requires an execution context");
|
|
18498
|
+
}
|
|
18499
|
+
if (!MethodContextService.hasContext()) {
|
|
18500
|
+
throw new Error("trailingProfit requires a method context");
|
|
18501
|
+
}
|
|
18502
|
+
const { backtest: isBacktest } = bt.executionContextService.context;
|
|
18503
|
+
const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
|
|
18504
|
+
await bt.strategyCoreService.trailingProfit(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
|
|
18505
|
+
}
|
|
18124
18506
|
/**
|
|
18125
18507
|
* Moves stop-loss to breakeven when price reaches threshold.
|
|
18126
18508
|
*
|
|
@@ -20066,6 +20448,7 @@ const BACKTEST_METHOD_NAME_CANCEL = "BacktestUtils.cancel";
|
|
|
20066
20448
|
const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.partialProfit";
|
|
20067
20449
|
const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.partialLoss";
|
|
20068
20450
|
const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.trailingStop";
|
|
20451
|
+
const BACKTEST_METHOD_NAME_TRAILING_PROFIT = "BacktestUtils.trailingProfit";
|
|
20069
20452
|
const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
|
|
20070
20453
|
/**
|
|
20071
20454
|
* Internal task function that runs backtest and handles completion.
|
|
@@ -20722,6 +21105,48 @@ class BacktestUtils {
|
|
|
20722
21105
|
}
|
|
20723
21106
|
await bt.strategyCoreService.trailingStop(true, symbol, percentShift, currentPrice, context);
|
|
20724
21107
|
};
|
|
21108
|
+
/**
|
|
21109
|
+
* Adjusts the trailing take-profit distance for an active pending signal.
|
|
21110
|
+
*
|
|
21111
|
+
* Updates the take-profit distance by a percentage adjustment relative to the original TP distance.
|
|
21112
|
+
* Negative percentShift brings TP closer to entry, positive percentShift moves it further.
|
|
21113
|
+
* Once direction is set on first call, subsequent calls must continue in same direction.
|
|
21114
|
+
*
|
|
21115
|
+
* @param symbol - Trading pair symbol
|
|
21116
|
+
* @param percentShift - Percentage adjustment to TP distance (-100 to 100)
|
|
21117
|
+
* @param currentPrice - Current market price to check for intrusion
|
|
21118
|
+
* @param context - Execution context with strategyName, exchangeName, and frameName
|
|
21119
|
+
* @returns Promise that resolves when trailing TP is updated
|
|
21120
|
+
*
|
|
21121
|
+
* @example
|
|
21122
|
+
* ```typescript
|
|
21123
|
+
* // LONG: entry=100, originalTP=110, distance=10%, currentPrice=102
|
|
21124
|
+
* // Move TP further by 50%: newTP = 100 + 15% = 115
|
|
21125
|
+
* await Backtest.trailingProfit("BTCUSDT", 50, 102, {
|
|
21126
|
+
* exchangeName: "binance",
|
|
21127
|
+
* frameName: "frame1",
|
|
21128
|
+
* strategyName: "my-strategy"
|
|
21129
|
+
* });
|
|
21130
|
+
* ```
|
|
21131
|
+
*/
|
|
21132
|
+
this.trailingProfit = async (symbol, percentShift, currentPrice, context) => {
|
|
21133
|
+
bt.loggerService.info(BACKTEST_METHOD_NAME_TRAILING_PROFIT, {
|
|
21134
|
+
symbol,
|
|
21135
|
+
percentShift,
|
|
21136
|
+
currentPrice,
|
|
21137
|
+
context,
|
|
21138
|
+
});
|
|
21139
|
+
bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_TRAILING_PROFIT);
|
|
21140
|
+
bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_TRAILING_PROFIT);
|
|
21141
|
+
{
|
|
21142
|
+
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
21143
|
+
riskName &&
|
|
21144
|
+
bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_PROFIT);
|
|
21145
|
+
riskList &&
|
|
21146
|
+
riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_PROFIT));
|
|
21147
|
+
}
|
|
21148
|
+
await bt.strategyCoreService.trailingProfit(true, symbol, percentShift, currentPrice, context);
|
|
21149
|
+
};
|
|
20725
21150
|
/**
|
|
20726
21151
|
* Moves stop-loss to breakeven when price reaches threshold.
|
|
20727
21152
|
*
|
|
@@ -20926,6 +21351,7 @@ const LIVE_METHOD_NAME_CANCEL = "LiveUtils.cancel";
|
|
|
20926
21351
|
const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.partialProfit";
|
|
20927
21352
|
const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.partialLoss";
|
|
20928
21353
|
const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.trailingStop";
|
|
21354
|
+
const LIVE_METHOD_NAME_TRAILING_PROFIT = "LiveUtils.trailingProfit";
|
|
20929
21355
|
/**
|
|
20930
21356
|
* Internal task function that runs live trading and handles completion.
|
|
20931
21357
|
* Consumes live trading results and updates instance state flags.
|
|
@@ -21560,6 +21986,49 @@ class LiveUtils {
|
|
|
21560
21986
|
frameName: "",
|
|
21561
21987
|
});
|
|
21562
21988
|
};
|
|
21989
|
+
/**
|
|
21990
|
+
* Adjusts the trailing take-profit distance for an active pending signal.
|
|
21991
|
+
*
|
|
21992
|
+
* Updates the take-profit distance by a percentage adjustment relative to the original TP distance.
|
|
21993
|
+
* Negative percentShift brings TP closer to entry, positive percentShift moves it further.
|
|
21994
|
+
* Once direction is set on first call, subsequent calls must continue in same direction.
|
|
21995
|
+
*
|
|
21996
|
+
* @param symbol - Trading pair symbol
|
|
21997
|
+
* @param percentShift - Percentage adjustment to TP distance (-100 to 100)
|
|
21998
|
+
* @param currentPrice - Current market price to check for intrusion
|
|
21999
|
+
* @param context - Execution context with strategyName and exchangeName
|
|
22000
|
+
* @returns Promise that resolves when trailing TP is updated
|
|
22001
|
+
*
|
|
22002
|
+
* @example
|
|
22003
|
+
* ```typescript
|
|
22004
|
+
* // LONG: entry=100, originalTP=110, distance=10%, currentPrice=102
|
|
22005
|
+
* // Move TP further by 50%: newTP = 100 + 15% = 115
|
|
22006
|
+
* await Live.trailingProfit("BTCUSDT", 50, 102, {
|
|
22007
|
+
* exchangeName: "binance",
|
|
22008
|
+
* strategyName: "my-strategy"
|
|
22009
|
+
* });
|
|
22010
|
+
* ```
|
|
22011
|
+
*/
|
|
22012
|
+
this.trailingProfit = async (symbol, percentShift, currentPrice, context) => {
|
|
22013
|
+
bt.loggerService.info(LIVE_METHOD_NAME_TRAILING_PROFIT, {
|
|
22014
|
+
symbol,
|
|
22015
|
+
percentShift,
|
|
22016
|
+
currentPrice,
|
|
22017
|
+
context,
|
|
22018
|
+
});
|
|
22019
|
+
bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_TRAILING_PROFIT);
|
|
22020
|
+
bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_TRAILING_PROFIT);
|
|
22021
|
+
{
|
|
22022
|
+
const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
|
|
22023
|
+
riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT);
|
|
22024
|
+
riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_PROFIT));
|
|
22025
|
+
}
|
|
22026
|
+
await bt.strategyCoreService.trailingProfit(false, symbol, percentShift, currentPrice, {
|
|
22027
|
+
strategyName: context.strategyName,
|
|
22028
|
+
exchangeName: context.exchangeName,
|
|
22029
|
+
frameName: "",
|
|
22030
|
+
});
|
|
22031
|
+
};
|
|
21563
22032
|
/**
|
|
21564
22033
|
* Moves stop-loss to breakeven when price reaches threshold.
|
|
21565
22034
|
*
|
|
@@ -24500,5 +24969,6 @@ exports.setColumns = setColumns;
|
|
|
24500
24969
|
exports.setConfig = setConfig;
|
|
24501
24970
|
exports.setLogger = setLogger;
|
|
24502
24971
|
exports.stop = stop;
|
|
24972
|
+
exports.trailingProfit = trailingProfit;
|
|
24503
24973
|
exports.trailingStop = trailingStop;
|
|
24504
24974
|
exports.validate = validate;
|