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