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