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 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
- * - Ensures external code never sees private _trailingPriceStopLoss field
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 stop-loss and hidden internals
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
- if (signal._trailingPriceStopLoss !== undefined) {
2162
- return {
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
- // Calculate distance between entry and original stop-loss AS PERCENTAGE of entry price
2676
- const slDistancePercent = Math.abs((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen * 100);
2677
- // Calculate new stop-loss distance percentage by adding shift
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 = slDistancePercent + percentShift;
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, originalSL=90 (10%), shift=-15% → newDistance=-5% → 100 * 1.05 = 105 (profit zone)
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, originalSL=110 (10%), shift=-15% → newDistance=-5% → 100 * 0.95 = 95 (profit zone)
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
- originalDistancePercent: slDistancePercent,
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
- const movingUp = newStopLoss > currentStopLoss;
2724
- const movingDown = newStopLoss < currentStopLoss;
2725
- // Determine initial direction based on first trailing SL vs original SL
2726
- const initialDirection = signal._trailingPriceStopLoss > signal.priceStopLoss ? "up" : "down";
2727
- let shouldUpdate = false;
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: movingUp ? "up" : movingDown ? "down" : "same",
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
- originalDistancePercent: slDistancePercent,
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
- if (signal.position === "long" && averagePrice >= signal.priceTakeProfit) {
3578
- return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceTakeProfit, // КРИТИЧНО: используем точную цену TP
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 <= signal.priceTakeProfit) {
3582
- return await CLOSE_PENDING_SIGNAL_FN(self, signal, signal.priceTakeProfit, // КРИТИЧНО: используем точную цену TP
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 tpDistance = signal.priceTakeProfit - signal.priceOpen;
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 tpDistance = signal.priceOpen - signal.priceTakeProfit;
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 >= signal.priceTakeProfit) {
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 <= signal.priceTakeProfit) {
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 = signal.priceTakeProfit;
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 tpDistance = signal.priceTakeProfit - signal.priceOpen;
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 tpDistance = signal.priceOpen - signal.priceTakeProfit;
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 stop-loss
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
- if ("_trailingPriceStopLoss" in signal) {
6283
- return {
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;