backtest-kit 1.10.1 → 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";
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";
2733
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
@@ -2790,6 +2893,19 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
2790
2893
  const thresholdPrice = signal.priceOpen * (1 + breakevenThresholdPercent / 100);
2791
2894
  const isThresholdReached = currentPrice >= thresholdPrice;
2792
2895
  if (isThresholdReached && breakevenPrice > trailingStopLoss) {
2896
+ // Check for price intrusion before setting new SL
2897
+ if (currentPrice < breakevenPrice) {
2898
+ // Price already crossed the breakeven level - skip setting SL
2899
+ self.params.logger.debug("BREAKEVEN_FN: price intrusion detected, skipping SL update", {
2900
+ signalId: signal.id,
2901
+ position: signal.position,
2902
+ priceOpen: signal.priceOpen,
2903
+ breakevenPrice,
2904
+ currentPrice,
2905
+ reason: "currentPrice below breakevenPrice (LONG position)"
2906
+ });
2907
+ return false;
2908
+ }
2793
2909
  // Breakeven is better than current trailing SL - upgrade to breakeven
2794
2910
  signal._trailingPriceStopLoss = breakevenPrice;
2795
2911
  self.params.logger.info("BREAKEVEN_FN: upgraded negative trailing stop to breakeven", {
@@ -2842,6 +2958,19 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
2842
2958
  const thresholdPrice = signal.priceOpen * (1 - breakevenThresholdPercent / 100);
2843
2959
  const isThresholdReached = currentPrice <= thresholdPrice;
2844
2960
  if (isThresholdReached && breakevenPrice < trailingStopLoss) {
2961
+ // Check for price intrusion before setting new SL
2962
+ if (currentPrice > breakevenPrice) {
2963
+ // Price already crossed the breakeven level - skip setting SL
2964
+ self.params.logger.debug("BREAKEVEN_FN: price intrusion detected, skipping SL update", {
2965
+ signalId: signal.id,
2966
+ position: signal.position,
2967
+ priceOpen: signal.priceOpen,
2968
+ breakevenPrice,
2969
+ currentPrice,
2970
+ reason: "currentPrice above breakevenPrice (SHORT position)"
2971
+ });
2972
+ return false;
2973
+ }
2845
2974
  // Breakeven is better than current trailing SL - upgrade to breakeven
2846
2975
  signal._trailingPriceStopLoss = breakevenPrice;
2847
2976
  self.params.logger.info("BREAKEVEN_FN: upgraded negative trailing stop to breakeven", {
@@ -2912,6 +3041,31 @@ const BREAKEVEN_FN = (self, signal, currentPrice) => {
2912
3041
  });
2913
3042
  return false;
2914
3043
  }
3044
+ // Check for price intrusion before setting new SL
3045
+ if (signal.position === "long" && currentPrice < breakevenPrice) {
3046
+ // LONG: Price already crossed the breakeven level - skip setting SL
3047
+ self.params.logger.debug("BREAKEVEN_FN: price intrusion detected, skipping SL update", {
3048
+ signalId: signal.id,
3049
+ position: signal.position,
3050
+ priceOpen: signal.priceOpen,
3051
+ breakevenPrice,
3052
+ currentPrice,
3053
+ reason: "currentPrice below breakevenPrice (LONG position)"
3054
+ });
3055
+ return false;
3056
+ }
3057
+ if (signal.position === "short" && currentPrice > breakevenPrice) {
3058
+ // SHORT: Price already crossed the breakeven level - skip setting SL
3059
+ self.params.logger.debug("BREAKEVEN_FN: price intrusion detected, skipping SL update", {
3060
+ signalId: signal.id,
3061
+ position: signal.position,
3062
+ priceOpen: signal.priceOpen,
3063
+ breakevenPrice,
3064
+ currentPrice,
3065
+ reason: "currentPrice above breakevenPrice (SHORT position)"
3066
+ });
3067
+ return false;
3068
+ }
2915
3069
  // Move SL to breakeven (entry price)
2916
3070
  signal._trailingPriceStopLoss = breakevenPrice;
2917
3071
  self.params.logger.info("BREAKEVEN_FN executed", {
@@ -3520,13 +3674,14 @@ const CHECK_PENDING_SIGNAL_COMPLETION_FN = async (self, signal, averagePrice) =>
3520
3674
  if (elapsedTime >= maxTimeToWait) {
3521
3675
  return await CLOSE_PENDING_SIGNAL_FN(self, signal, averagePrice, "time_expired");
3522
3676
  }
3523
- // Check take profit
3524
- if (signal.position === "long" && averagePrice >= signal.priceTakeProfit) {
3525
- 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
3526
3681
  "take_profit");
3527
3682
  }
3528
- if (signal.position === "short" && averagePrice <= signal.priceTakeProfit) {
3529
- 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
3530
3685
  "take_profit");
3531
3686
  }
3532
3687
  // Check stop loss (use trailing SL if set, otherwise original SL)
@@ -3588,8 +3743,9 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
3588
3743
  await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
3589
3744
  }
3590
3745
  if (currentDistance > 0) {
3591
- // Moving towards TP
3592
- 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;
3593
3749
  const progressPercent = (currentDistance / tpDistance) * 100;
3594
3750
  percentTp = Math.min(progressPercent, 100);
3595
3751
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
@@ -3611,8 +3767,9 @@ const RETURN_PENDING_SIGNAL_ACTIVE_FN = async (self, signal, currentPrice) => {
3611
3767
  await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, currentPrice, currentTime, self.params.execution.context.backtest);
3612
3768
  }
3613
3769
  if (currentDistance > 0) {
3614
- // Moving towards TP
3615
- 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;
3616
3773
  const progressPercent = (currentDistance / tpDistance) * 100;
3617
3774
  percentTp = Math.min(progressPercent, 100);
3618
3775
  await CALL_PARTIAL_PROFIT_CALLBACKS_FN(self, self.params.execution.context.symbol, signal, currentPrice, percentTp, currentTime, self.params.execution.context.backtest);
@@ -3874,11 +4031,12 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
3874
4031
  }
3875
4032
  // Check TP/SL only if not expired
3876
4033
  // КРИТИЧНО: используем averagePrice (VWAP) для проверки достижения TP/SL (как в live mode)
3877
- // КРИТИЧНО: используем trailing SL если установлен
4034
+ // КРИТИЧНО: используем trailing SL и TP если установлены
3878
4035
  const effectiveStopLoss = signal._trailingPriceStopLoss ?? signal.priceStopLoss;
4036
+ const effectiveTakeProfit = signal._trailingPriceTakeProfit ?? signal.priceTakeProfit;
3879
4037
  if (!shouldClose && signal.position === "long") {
3880
4038
  // Для LONG: TP срабатывает если VWAP >= TP, SL если VWAP <= SL
3881
- if (averagePrice >= signal.priceTakeProfit) {
4039
+ if (averagePrice >= effectiveTakeProfit) {
3882
4040
  shouldClose = true;
3883
4041
  closeReason = "take_profit";
3884
4042
  }
@@ -3889,7 +4047,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
3889
4047
  }
3890
4048
  if (!shouldClose && signal.position === "short") {
3891
4049
  // Для SHORT: TP срабатывает если VWAP <= TP, SL если VWAP >= SL
3892
- if (averagePrice <= signal.priceTakeProfit) {
4050
+ if (averagePrice <= effectiveTakeProfit) {
3893
4051
  shouldClose = true;
3894
4052
  closeReason = "take_profit";
3895
4053
  }
@@ -3902,7 +4060,7 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
3902
4060
  // КРИТИЧНО: используем точную цену TP/SL для закрытия (как в live mode)
3903
4061
  let closePrice;
3904
4062
  if (closeReason === "take_profit") {
3905
- closePrice = signal.priceTakeProfit;
4063
+ closePrice = effectiveTakeProfit; // используем trailing TP если установлен
3906
4064
  }
3907
4065
  else if (closeReason === "stop_loss") {
3908
4066
  closePrice = effectiveStopLoss;
@@ -3923,8 +4081,9 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
3923
4081
  await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, averagePrice, currentCandleTimestamp, self.params.execution.context.backtest);
3924
4082
  }
3925
4083
  if (currentDistance > 0) {
3926
- // Moving towards TP
3927
- 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;
3928
4087
  const progressPercent = (currentDistance / tpDistance) * 100;
3929
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);
3930
4089
  }
@@ -3944,8 +4103,9 @@ const PROCESS_PENDING_SIGNAL_CANDLES_FN = async (self, signal, candles) => {
3944
4103
  await CALL_BREAKEVEN_CHECK_FN(self, self.params.execution.context.symbol, signal, averagePrice, currentCandleTimestamp, self.params.execution.context.backtest);
3945
4104
  }
3946
4105
  if (currentDistance > 0) {
3947
- // Moving towards TP
3948
- 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;
3949
4109
  const progressPercent = (currentDistance / tpDistance) * 100;
3950
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);
3951
4111
  }
@@ -4073,6 +4233,112 @@ class ClientStrategy {
4073
4233
  });
4074
4234
  return this._scheduledSignal ? TO_PUBLIC_SIGNAL(this._scheduledSignal) : null;
4075
4235
  }
4236
+ /**
4237
+ * Checks if breakeven threshold has been reached for the current pending signal.
4238
+ *
4239
+ * Uses the same formula as BREAKEVEN_FN to determine if price has moved far enough
4240
+ * to cover transaction costs (slippage + fees) and allow breakeven to be set.
4241
+ * Threshold: (CC_PERCENT_SLIPPAGE + CC_PERCENT_FEE) * 2 transactions
4242
+ *
4243
+ * For LONG position:
4244
+ * - Returns true when: currentPrice >= priceOpen * (1 + threshold%)
4245
+ * - Example: entry=100, threshold=0.4% → true when price >= 100.4
4246
+ *
4247
+ * For SHORT position:
4248
+ * - Returns true when: currentPrice <= priceOpen * (1 - threshold%)
4249
+ * - Example: entry=100, threshold=0.4% → true when price <= 99.6
4250
+ *
4251
+ * Special cases:
4252
+ * - Returns false if no pending signal exists
4253
+ * - Returns true if trailing stop is already in profit zone (breakeven already achieved)
4254
+ * - Returns false if threshold not reached yet
4255
+ *
4256
+ * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
4257
+ * @param currentPrice - Current market price to check against threshold
4258
+ * @returns Promise<boolean> - true if breakeven threshold reached, false otherwise
4259
+ *
4260
+ * @example
4261
+ * ```typescript
4262
+ * // Check if breakeven is available for LONG position (entry=100, threshold=0.4%)
4263
+ * const canBreakeven = await strategy.getBreakeven("BTCUSDT", 100.5);
4264
+ * // Returns true (price >= 100.4)
4265
+ *
4266
+ * if (canBreakeven) {
4267
+ * await strategy.breakeven("BTCUSDT", 100.5, false);
4268
+ * }
4269
+ * ```
4270
+ */
4271
+ async getBreakeven(symbol, currentPrice) {
4272
+ this.params.logger.debug("ClientStrategy getBreakeven", {
4273
+ symbol,
4274
+ currentPrice,
4275
+ });
4276
+ // No pending signal - breakeven not available
4277
+ if (!this._pendingSignal) {
4278
+ return false;
4279
+ }
4280
+ const signal = this._pendingSignal;
4281
+ // Calculate breakeven threshold based on slippage and fees
4282
+ // Need to cover: entry slippage + entry fee + exit slippage + exit fee
4283
+ // Total: (slippage + fee) * 2 transactions
4284
+ const breakevenThresholdPercent = (GLOBAL_CONFIG.CC_PERCENT_SLIPPAGE + GLOBAL_CONFIG.CC_PERCENT_FEE) * 2 + GLOBAL_CONFIG.CC_BREAKEVEN_THRESHOLD;
4285
+ // Check if trailing stop is already set
4286
+ if (signal._trailingPriceStopLoss !== undefined) {
4287
+ const trailingStopLoss = signal._trailingPriceStopLoss;
4288
+ if (signal.position === "long") {
4289
+ // LONG: trailing SL is positive if it's above entry (in profit zone)
4290
+ const isPositiveTrailing = trailingStopLoss > signal.priceOpen;
4291
+ if (isPositiveTrailing) {
4292
+ // Trailing stop is already protecting profit - breakeven achieved
4293
+ return true;
4294
+ }
4295
+ // Trailing stop is negative (below entry)
4296
+ // Check if we can upgrade it to breakeven
4297
+ const thresholdPrice = signal.priceOpen * (1 + breakevenThresholdPercent / 100);
4298
+ const isThresholdReached = currentPrice >= thresholdPrice;
4299
+ const breakevenPrice = signal.priceOpen;
4300
+ // Can upgrade to breakeven if threshold reached and breakeven is better than current trailing SL
4301
+ return isThresholdReached && breakevenPrice > trailingStopLoss;
4302
+ }
4303
+ else {
4304
+ // SHORT: trailing SL is positive if it's below entry (in profit zone)
4305
+ const isPositiveTrailing = trailingStopLoss < signal.priceOpen;
4306
+ if (isPositiveTrailing) {
4307
+ // Trailing stop is already protecting profit - breakeven achieved
4308
+ return true;
4309
+ }
4310
+ // Trailing stop is negative (above entry)
4311
+ // Check if we can upgrade it to breakeven
4312
+ const thresholdPrice = signal.priceOpen * (1 - breakevenThresholdPercent / 100);
4313
+ const isThresholdReached = currentPrice <= thresholdPrice;
4314
+ const breakevenPrice = signal.priceOpen;
4315
+ // Can upgrade to breakeven if threshold reached and breakeven is better than current trailing SL
4316
+ return isThresholdReached && breakevenPrice < trailingStopLoss;
4317
+ }
4318
+ }
4319
+ // No trailing stop set - proceed with normal breakeven logic
4320
+ const currentStopLoss = signal.priceStopLoss;
4321
+ const breakevenPrice = signal.priceOpen;
4322
+ // Calculate threshold price
4323
+ let thresholdPrice;
4324
+ let isThresholdReached;
4325
+ let canMoveToBreakeven;
4326
+ if (signal.position === "long") {
4327
+ // LONG: threshold reached when price goes UP by breakevenThresholdPercent from entry
4328
+ thresholdPrice = signal.priceOpen * (1 + breakevenThresholdPercent / 100);
4329
+ isThresholdReached = currentPrice >= thresholdPrice;
4330
+ // Can move to breakeven only if threshold reached and SL is below entry
4331
+ canMoveToBreakeven = isThresholdReached && currentStopLoss < breakevenPrice;
4332
+ }
4333
+ else {
4334
+ // SHORT: threshold reached when price goes DOWN by breakevenThresholdPercent from entry
4335
+ thresholdPrice = signal.priceOpen * (1 - breakevenThresholdPercent / 100);
4336
+ isThresholdReached = currentPrice <= thresholdPrice;
4337
+ // Can move to breakeven only if threshold reached and SL is above entry
4338
+ canMoveToBreakeven = isThresholdReached && currentStopLoss > breakevenPrice;
4339
+ }
4340
+ return canMoveToBreakeven;
4341
+ }
4076
4342
  /**
4077
4343
  * Returns the stopped state of the strategy.
4078
4344
  *
@@ -4736,30 +5002,38 @@ class ClientStrategy {
4736
5002
  * - Throws if percentShift is not a finite number
4737
5003
  * - Throws if percentShift < -100 or > 100
4738
5004
  * - Throws if percentShift === 0
5005
+ * - Throws if currentPrice is not a positive finite number
4739
5006
  * - Skips if new SL would cross entry price
5007
+ * - Skips if currentPrice already crossed new SL level (price intrusion protection)
4740
5008
  *
4741
5009
  * @param symbol - Trading pair symbol (e.g., "BTCUSDT")
4742
5010
  * @param percentShift - Percentage shift of SL distance [-100, 100], excluding 0
5011
+ * @param currentPrice - Current market price to check for intrusion
4743
5012
  * @param backtest - Whether running in backtest mode (controls persistence)
4744
5013
  * @returns Promise that resolves when trailing SL is updated and persisted
4745
5014
  *
4746
5015
  * @example
4747
5016
  * ```typescript
4748
- * // LONG position: entry=100, originalSL=90, distance=10
5017
+ * // LONG position: entry=100, originalSL=90, distance=10%, currentPrice=102
4749
5018
  *
4750
- * // Move SL 50% closer to entry (tighten)
4751
- * await strategy.trailingStop("BTCUSDT", -50, false);
4752
- * // newSL = 100 - 10*(1-0.5) = 95
5019
+ * // Move SL 50% closer to entry (tighten): reduces distance by 50%
5020
+ * await strategy.trailingStop("BTCUSDT", -50, 102, false);
5021
+ * // newDistance = 10% - 50% = 5%, newSL = 100 * (1 - 0.05) = 95
4753
5022
  *
4754
- * // Move SL 30% away from entry (loosen, allow more drawdown)
4755
- * await strategy.trailingStop("BTCUSDT", 30, false);
4756
- * // newSL = 100 - 10*(1+0.3) = 87 (SKIPPED: worse than current 95)
5023
+ * // Move SL 30% away from entry (loosen): increases distance by 30%
5024
+ * await strategy.trailingStop("BTCUSDT", 30, 102, false);
5025
+ * // newDistance = 10% + 30% = 40%, newSL = 100 * (1 - 0.4) = 60
5026
+ *
5027
+ * // Price intrusion example: currentPrice=92, trying to set SL=95
5028
+ * await strategy.trailingStop("BTCUSDT", -50, 92, false);
5029
+ * // SKIPPED: currentPrice (92) < newSL (95) - would trigger immediate stop
4757
5030
  * ```
4758
5031
  */
4759
- async trailingStop(symbol, percentShift, backtest) {
5032
+ async trailingStop(symbol, percentShift, currentPrice, backtest) {
4760
5033
  this.params.logger.debug("ClientStrategy trailingStop", {
4761
5034
  symbol,
4762
5035
  percentShift,
5036
+ currentPrice,
4763
5037
  hasPendingSignal: this._pendingSignal !== null,
4764
5038
  });
4765
5039
  // Validation: must have pending signal
@@ -4776,6 +5050,72 @@ class ClientStrategy {
4776
5050
  if (percentShift === 0) {
4777
5051
  throw new Error(`ClientStrategy trailingStop: percentShift cannot be 0`);
4778
5052
  }
5053
+ // Validation: currentPrice must be valid
5054
+ if (typeof currentPrice !== "number" || !isFinite(currentPrice) || currentPrice <= 0) {
5055
+ throw new Error(`ClientStrategy trailingStop: currentPrice must be a positive finite number, got ${currentPrice}`);
5056
+ }
5057
+ // Calculate what the new stop loss would be
5058
+ const signal = this._pendingSignal;
5059
+ const slDistancePercent = Math.abs((signal.priceOpen - signal.priceStopLoss) / signal.priceOpen * 100);
5060
+ const newSlDistancePercent = slDistancePercent + percentShift;
5061
+ let newStopLoss;
5062
+ if (signal.position === "long") {
5063
+ newStopLoss = signal.priceOpen * (1 - newSlDistancePercent / 100);
5064
+ }
5065
+ else {
5066
+ newStopLoss = signal.priceOpen * (1 + newSlDistancePercent / 100);
5067
+ }
5068
+ // Check for price intrusion before executing trailing logic
5069
+ if (signal.position === "long" && currentPrice < newStopLoss) {
5070
+ // LONG: Price already crossed the new stop loss level - skip setting SL
5071
+ this.params.logger.debug("ClientStrategy trailingStop: price intrusion detected, skipping SL update", {
5072
+ signalId: signal.id,
5073
+ position: signal.position,
5074
+ priceOpen: signal.priceOpen,
5075
+ newStopLoss,
5076
+ currentPrice,
5077
+ reason: "currentPrice below newStopLoss (LONG position)"
5078
+ });
5079
+ return;
5080
+ }
5081
+ if (signal.position === "short" && currentPrice > newStopLoss) {
5082
+ // SHORT: Price already crossed the new stop loss level - skip setting SL
5083
+ this.params.logger.debug("ClientStrategy trailingStop: price intrusion detected, skipping SL update", {
5084
+ signalId: signal.id,
5085
+ position: signal.position,
5086
+ priceOpen: signal.priceOpen,
5087
+ newStopLoss,
5088
+ currentPrice,
5089
+ reason: "currentPrice above newStopLoss (SHORT position)"
5090
+ });
5091
+ return;
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
+ }
4779
5119
  // Execute trailing logic
4780
5120
  TRAILING_STOP_FN(this, this._pendingSignal, percentShift);
4781
5121
  // Persist updated signal state (inline setPendingSignal content)
@@ -4792,6 +5132,136 @@ class ClientStrategy {
4792
5132
  await PersistSignalAdapter.writeSignalData(this._pendingSignal, this.params.execution.context.symbol, this.params.strategyName, this.params.exchangeName);
4793
5133
  }
4794
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
+ }
4795
5265
  }
4796
5266
 
4797
5267
  const RISK_METHOD_NAME_GET_DATA = "RiskUtils.getData";
@@ -4961,6 +5431,7 @@ class RiskUtils {
4961
5431
  strategyName: context.strategyName,
4962
5432
  });
4963
5433
  bt.strategyValidationService.validate(context.strategyName, RISK_METHOD_NAME_GET_DATA);
5434
+ bt.exchangeValidationService.validate(context.exchangeName, RISK_METHOD_NAME_GET_DATA);
4964
5435
  {
4965
5436
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
4966
5437
  riskName &&
@@ -5017,6 +5488,7 @@ class RiskUtils {
5017
5488
  strategyName: context.strategyName,
5018
5489
  });
5019
5490
  bt.strategyValidationService.validate(context.strategyName, RISK_METHOD_NAME_GET_REPORT);
5491
+ bt.exchangeValidationService.validate(context.exchangeName, RISK_METHOD_NAME_GET_REPORT);
5020
5492
  {
5021
5493
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
5022
5494
  riskName &&
@@ -5065,6 +5537,7 @@ class RiskUtils {
5065
5537
  path,
5066
5538
  });
5067
5539
  bt.strategyValidationService.validate(context.strategyName, RISK_METHOD_NAME_DUMP);
5540
+ bt.exchangeValidationService.validate(context.exchangeName, RISK_METHOD_NAME_DUMP);
5068
5541
  {
5069
5542
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
5070
5543
  riskName &&
@@ -5275,6 +5748,46 @@ class StrategyConnectionService {
5275
5748
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
5276
5749
  return await strategy.getScheduledSignal(symbol);
5277
5750
  };
5751
+ /**
5752
+ * Checks if breakeven threshold has been reached for the current pending signal.
5753
+ *
5754
+ * Uses the same formula as BREAKEVEN_FN to determine if price has moved far enough
5755
+ * to cover transaction costs and allow breakeven to be set.
5756
+ *
5757
+ * Delegates to ClientStrategy.getBreakeven() with current execution context.
5758
+ *
5759
+ * @param backtest - Whether running in backtest mode
5760
+ * @param symbol - Trading pair symbol
5761
+ * @param currentPrice - Current market price to check against threshold
5762
+ * @param context - Execution context with strategyName, exchangeName, frameName
5763
+ * @returns Promise<boolean> - true if breakeven threshold reached, false otherwise
5764
+ *
5765
+ * @example
5766
+ * ```typescript
5767
+ * // Check if breakeven is available for LONG position (entry=100, threshold=0.4%)
5768
+ * const canBreakeven = await strategyConnectionService.getBreakeven(
5769
+ * false,
5770
+ * "BTCUSDT",
5771
+ * 100.5,
5772
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
5773
+ * );
5774
+ * // Returns true (price >= 100.4)
5775
+ *
5776
+ * if (canBreakeven) {
5777
+ * await strategyConnectionService.breakeven(false, "BTCUSDT", 100.5, context);
5778
+ * }
5779
+ * ```
5780
+ */
5781
+ this.getBreakeven = async (backtest, symbol, currentPrice, context) => {
5782
+ this.loggerService.log("strategyConnectionService getBreakeven", {
5783
+ symbol,
5784
+ context,
5785
+ currentPrice,
5786
+ backtest,
5787
+ });
5788
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
5789
+ return await strategy.getBreakeven(symbol, currentPrice);
5790
+ };
5278
5791
  /**
5279
5792
  * Retrieves the stopped state of the strategy.
5280
5793
  *
@@ -5509,30 +6022,72 @@ class StrategyConnectionService {
5509
6022
  * @param backtest - Whether running in backtest mode
5510
6023
  * @param symbol - Trading pair symbol
5511
6024
  * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
6025
+ * @param currentPrice - Current market price to check for intrusion
5512
6026
  * @param context - Execution context with strategyName, exchangeName, frameName
5513
6027
  * @returns Promise that resolves when trailing SL is updated
5514
6028
  *
5515
6029
  * @example
5516
6030
  * ```typescript
5517
- * // LONG: entry=100, originalSL=90, distance=10
5518
- * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
6031
+ * // LONG: entry=100, originalSL=90, distance=10%, currentPrice=102
6032
+ * // Tighten stop by 50%: newSL = 100 - 5% = 95
5519
6033
  * await strategyConnectionService.trailingStop(
5520
6034
  * false,
5521
6035
  * "BTCUSDT",
5522
6036
  * -50,
6037
+ * 102,
5523
6038
  * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
5524
6039
  * );
5525
6040
  * ```
5526
6041
  */
5527
- this.trailingStop = async (backtest, symbol, percentShift, context) => {
6042
+ this.trailingStop = async (backtest, symbol, percentShift, currentPrice, context) => {
5528
6043
  this.loggerService.log("strategyConnectionService trailingStop", {
5529
6044
  symbol,
5530
6045
  context,
5531
6046
  percentShift,
6047
+ currentPrice,
6048
+ backtest,
6049
+ });
6050
+ const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
6051
+ await strategy.trailingStop(symbol, percentShift, currentPrice, backtest);
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,
5532
6087
  backtest,
5533
6088
  });
5534
6089
  const strategy = this.getStrategy(symbol, context.strategyName, context.exchangeName, context.frameName, backtest);
5535
- await strategy.trailingStop(symbol, percentShift, backtest);
6090
+ await strategy.trailingProfit(symbol, percentShift, currentPrice, backtest);
5536
6091
  };
5537
6092
  /**
5538
6093
  * Delegates to ClientStrategy.breakeven() with current execution context.
@@ -6003,16 +6558,19 @@ const POSITION_NEED_FETCH = Symbol("risk-need-fetch");
6003
6558
  *
6004
6559
  * - Falls back to currentPrice if priceOpen is not set (for ISignalDto/scheduled signals)
6005
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)
6006
6562
  * - Preserves original stop-loss in originalPriceStopLoss for reference
6563
+ * - Preserves original take-profit in originalPriceTakeProfit for reference
6007
6564
  *
6008
6565
  * Use cases:
6009
6566
  * - Risk validation before opening a position (checkSignal)
6010
6567
  * - Pre-flight validation of scheduled signals
6011
6568
  * - Calculating position size based on stop-loss distance
6569
+ * - Calculating risk-reward ratio using effective SL/TP
6012
6570
  *
6013
6571
  * @param signal - Signal DTO or row (may not have priceOpen for scheduled signals)
6014
6572
  * @param currentPrice - Current market price, used as fallback for priceOpen if not set
6015
- * @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
6016
6574
  *
6017
6575
  * @example
6018
6576
  * ```typescript
@@ -6020,24 +6578,24 @@ const POSITION_NEED_FETCH = Symbol("risk-need-fetch");
6020
6578
  * const riskSignal = TO_RISK_SIGNAL(scheduledSignal, 45000);
6021
6579
  * // riskSignal.priceOpen = 45000 (fallback to currentPrice)
6022
6580
  *
6023
- * // For signal with trailing SL
6581
+ * // For signal with trailing SL/TP
6024
6582
  * const riskSignal = TO_RISK_SIGNAL(activeSignal, 46000);
6025
- * // 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)
6026
6587
  * ```
6027
6588
  */
6028
6589
  const TO_RISK_SIGNAL = (signal, currentPrice) => {
6029
- if ("_trailingPriceStopLoss" in signal) {
6030
- return {
6031
- ...structuredClone(signal),
6032
- priceOpen: signal.priceOpen ?? currentPrice,
6033
- priceStopLoss: signal._trailingPriceStopLoss,
6034
- originalPriceStopLoss: signal.priceStopLoss,
6035
- };
6036
- }
6590
+ const hasTrailingSL = "_trailingPriceStopLoss" in signal && signal._trailingPriceStopLoss !== undefined;
6591
+ const hasTrailingTP = "_trailingPriceTakeProfit" in signal && signal._trailingPriceTakeProfit !== undefined;
6037
6592
  return {
6038
6593
  ...structuredClone(signal),
6039
6594
  priceOpen: signal.priceOpen ?? currentPrice,
6595
+ priceStopLoss: hasTrailingSL ? signal._trailingPriceStopLoss : signal.priceStopLoss,
6596
+ priceTakeProfit: hasTrailingTP ? signal._trailingPriceTakeProfit : signal.priceTakeProfit,
6040
6597
  originalPriceStopLoss: signal.priceStopLoss,
6598
+ originalPriceTakeProfit: signal.priceTakeProfit,
6041
6599
  };
6042
6600
  };
6043
6601
  /** Key generator for active position map */
@@ -6673,6 +7231,46 @@ class StrategyCoreService {
6673
7231
  await this.validate(symbol, context);
6674
7232
  return await this.strategyConnectionService.getScheduledSignal(backtest, symbol, context);
6675
7233
  };
7234
+ /**
7235
+ * Checks if breakeven threshold has been reached for the current pending signal.
7236
+ *
7237
+ * Validates strategy existence and delegates to connection service
7238
+ * to check if price has moved far enough to cover transaction costs.
7239
+ *
7240
+ * Does not require execution context as this is a state query operation.
7241
+ *
7242
+ * @param backtest - Whether running in backtest mode
7243
+ * @param symbol - Trading pair symbol
7244
+ * @param currentPrice - Current market price to check against threshold
7245
+ * @param context - Execution context with strategyName, exchangeName, frameName
7246
+ * @returns Promise<boolean> - true if breakeven threshold reached, false otherwise
7247
+ *
7248
+ * @example
7249
+ * ```typescript
7250
+ * // Check if breakeven is available for LONG position (entry=100, threshold=0.4%)
7251
+ * const canBreakeven = await strategyCoreService.getBreakeven(
7252
+ * false,
7253
+ * "BTCUSDT",
7254
+ * 100.5,
7255
+ * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
7256
+ * );
7257
+ * // Returns true (price >= 100.4)
7258
+ *
7259
+ * if (canBreakeven) {
7260
+ * await strategyCoreService.breakeven(false, "BTCUSDT", 100.5, context);
7261
+ * }
7262
+ * ```
7263
+ */
7264
+ this.getBreakeven = async (backtest, symbol, currentPrice, context) => {
7265
+ this.loggerService.log("strategyCoreService getBreakeven", {
7266
+ symbol,
7267
+ currentPrice,
7268
+ context,
7269
+ backtest,
7270
+ });
7271
+ await this.validate(symbol, context);
7272
+ return await this.strategyConnectionService.getBreakeven(backtest, symbol, currentPrice, context);
7273
+ };
6676
7274
  /**
6677
7275
  * Checks if the strategy has been stopped.
6678
7276
  *
@@ -6902,30 +7500,68 @@ class StrategyCoreService {
6902
7500
  * @param backtest - Whether running in backtest mode
6903
7501
  * @param symbol - Trading pair symbol
6904
7502
  * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
7503
+ * @param currentPrice - Current market price to check for intrusion
6905
7504
  * @param context - Execution context with strategyName, exchangeName, frameName
6906
7505
  * @returns Promise that resolves when trailing SL is updated
6907
7506
  *
6908
7507
  * @example
6909
7508
  * ```typescript
6910
- * // LONG: entry=100, originalSL=90, distance=10
6911
- * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
7509
+ * // LONG: entry=100, originalSL=90, distance=10%, currentPrice=102
7510
+ * // Tighten stop by 50%: newSL = 100 - 5% = 95
6912
7511
  * await strategyCoreService.trailingStop(
6913
7512
  * false,
6914
7513
  * "BTCUSDT",
6915
7514
  * -50,
7515
+ * 102,
6916
7516
  * { strategyName: "my-strategy", exchangeName: "binance", frameName: "" }
6917
7517
  * );
6918
7518
  * ```
6919
7519
  */
6920
- this.trailingStop = async (backtest, symbol, percentShift, context) => {
7520
+ this.trailingStop = async (backtest, symbol, percentShift, currentPrice, context) => {
6921
7521
  this.loggerService.log("strategyCoreService trailingStop", {
6922
7522
  symbol,
6923
7523
  percentShift,
7524
+ currentPrice,
7525
+ context,
7526
+ backtest,
7527
+ });
7528
+ await this.validate(symbol, context);
7529
+ return await this.strategyConnectionService.trailingStop(backtest, symbol, percentShift, currentPrice, context);
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,
6924
7560
  context,
6925
7561
  backtest,
6926
7562
  });
6927
7563
  await this.validate(symbol, context);
6928
- return await this.strategyConnectionService.trailingStop(backtest, symbol, percentShift, context);
7564
+ return await this.strategyConnectionService.trailingProfit(backtest, symbol, percentShift, currentPrice, context);
6929
7565
  };
6930
7566
  /**
6931
7567
  * Moves stop-loss to breakeven when price reaches threshold.
@@ -7034,6 +7670,7 @@ class RiskGlobalService {
7034
7670
  this.loggerService = inject(TYPES.loggerService);
7035
7671
  this.riskConnectionService = inject(TYPES.riskConnectionService);
7036
7672
  this.riskValidationService = inject(TYPES.riskValidationService);
7673
+ this.exchangeValidationService = inject(TYPES.exchangeValidationService);
7037
7674
  /**
7038
7675
  * Validates risk configuration.
7039
7676
  * Memoized to avoid redundant validations for the same risk-exchange-frame combination.
@@ -7046,6 +7683,7 @@ class RiskGlobalService {
7046
7683
  payload,
7047
7684
  });
7048
7685
  this.riskValidationService.validate(payload.riskName, "riskGlobalService validate");
7686
+ this.exchangeValidationService.validate(payload.exchangeName, "riskGlobalService validate");
7049
7687
  });
7050
7688
  /**
7051
7689
  * Checks if a signal should be allowed based on risk limits.
@@ -15318,6 +15956,10 @@ class PartialGlobalService {
15318
15956
  * Risk validation service for validating risk existence.
15319
15957
  */
15320
15958
  this.riskValidationService = inject(TYPES.riskValidationService);
15959
+ /**
15960
+ * Exchange validation service for validating exchange existence.
15961
+ */
15962
+ this.exchangeValidationService = inject(TYPES.exchangeValidationService);
15321
15963
  /**
15322
15964
  * Validates strategy and associated risk configuration.
15323
15965
  * Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
@@ -15331,6 +15973,7 @@ class PartialGlobalService {
15331
15973
  methodName,
15332
15974
  });
15333
15975
  this.strategyValidationService.validate(context.strategyName, methodName);
15976
+ this.exchangeValidationService.validate(context.exchangeName, methodName);
15334
15977
  const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
15335
15978
  riskName && this.riskValidationService.validate(riskName, methodName);
15336
15979
  riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
@@ -16292,6 +16935,10 @@ class BreakevenGlobalService {
16292
16935
  * Risk validation service for validating risk existence.
16293
16936
  */
16294
16937
  this.riskValidationService = inject(TYPES.riskValidationService);
16938
+ /**
16939
+ * Exchange validation service for validating exchange existence.
16940
+ */
16941
+ this.exchangeValidationService = inject(TYPES.exchangeValidationService);
16295
16942
  /**
16296
16943
  * Validates strategy and associated risk configuration.
16297
16944
  * Memoized to avoid redundant validations for the same strategy-exchange-frame combination.
@@ -16305,6 +16952,7 @@ class BreakevenGlobalService {
16305
16952
  methodName,
16306
16953
  });
16307
16954
  this.strategyValidationService.validate(context.strategyName, methodName);
16955
+ this.exchangeValidationService.validate(context.exchangeName, methodName);
16308
16956
  const { riskName, riskList } = this.strategySchemaService.get(context.strategyName);
16309
16957
  riskName && this.riskValidationService.validate(riskName, methodName);
16310
16958
  riskList && riskList.forEach((riskName) => this.riskValidationService.validate(riskName, methodName));
@@ -17618,6 +18266,7 @@ const CANCEL_METHOD_NAME = "strategy.cancel";
17618
18266
  const PARTIAL_PROFIT_METHOD_NAME = "strategy.partialProfit";
17619
18267
  const PARTIAL_LOSS_METHOD_NAME = "strategy.partialLoss";
17620
18268
  const TRAILING_STOP_METHOD_NAME = "strategy.trailingStop";
18269
+ const TRAILING_PROFIT_METHOD_NAME = "strategy.trailingProfit";
17621
18270
  const BREAKEVEN_METHOD_NAME = "strategy.breakeven";
17622
18271
  /**
17623
18272
  * Stops the strategy from generating new signals.
@@ -17785,21 +18434,23 @@ async function partialLoss(symbol, percentToClose) {
17785
18434
  *
17786
18435
  * @param symbol - Trading pair symbol
17787
18436
  * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
18437
+ * @param currentPrice - Current market price to check for intrusion
17788
18438
  * @returns Promise that resolves when trailing SL is updated
17789
18439
  *
17790
18440
  * @example
17791
18441
  * ```typescript
17792
18442
  * import { trailingStop } from "backtest-kit";
17793
18443
  *
17794
- * // LONG: entry=100, originalSL=90, distance=10
17795
- * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
17796
- * await trailingStop("BTCUSDT", -50);
18444
+ * // LONG: entry=100, originalSL=90, distance=10%, currentPrice=102
18445
+ * // Tighten stop by 50%: newSL = 100 - 5% = 95
18446
+ * await trailingStop("BTCUSDT", -50, 102);
17797
18447
  * ```
17798
18448
  */
17799
- async function trailingStop(symbol, percentShift) {
18449
+ async function trailingStop(symbol, percentShift, currentPrice) {
17800
18450
  bt.loggerService.info(TRAILING_STOP_METHOD_NAME, {
17801
18451
  symbol,
17802
18452
  percentShift,
18453
+ currentPrice,
17803
18454
  });
17804
18455
  if (!ExecutionContextService.hasContext()) {
17805
18456
  throw new Error("trailingStop requires an execution context");
@@ -17809,7 +18460,46 @@ async function trailingStop(symbol, percentShift) {
17809
18460
  }
17810
18461
  const { backtest: isBacktest } = bt.executionContextService.context;
17811
18462
  const { exchangeName, frameName, strategyName } = bt.methodContextService.context;
17812
- await bt.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, { exchangeName, frameName, strategyName });
18463
+ await bt.strategyCoreService.trailingStop(isBacktest, symbol, percentShift, currentPrice, { exchangeName, frameName, strategyName });
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 });
17813
18503
  }
17814
18504
  /**
17815
18505
  * Moves stop-loss to breakeven when price reaches threshold.
@@ -19751,10 +20441,12 @@ const BACKTEST_METHOD_NAME_TASK = "BacktestUtils.task";
19751
20441
  const BACKTEST_METHOD_NAME_GET_STATUS = "BacktestUtils.getStatus";
19752
20442
  const BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL = "BacktestUtils.getPendingSignal";
19753
20443
  const BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL = "BacktestUtils.getScheduledSignal";
20444
+ const BACKTEST_METHOD_NAME_GET_BREAKEVEN = "BacktestUtils.getBreakeven";
19754
20445
  const BACKTEST_METHOD_NAME_CANCEL = "BacktestUtils.cancel";
19755
20446
  const BACKTEST_METHOD_NAME_PARTIAL_PROFIT = "BacktestUtils.partialProfit";
19756
20447
  const BACKTEST_METHOD_NAME_PARTIAL_LOSS = "BacktestUtils.partialLoss";
19757
20448
  const BACKTEST_METHOD_NAME_TRAILING_STOP = "BacktestUtils.trailingStop";
20449
+ const BACKTEST_METHOD_NAME_TRAILING_PROFIT = "BacktestUtils.trailingProfit";
19758
20450
  const BACKTEST_METHOD_NAME_GET_DATA = "BacktestUtils.getData";
19759
20451
  /**
19760
20452
  * Internal task function that runs backtest and handles completion.
@@ -20121,6 +20813,7 @@ class BacktestUtils {
20121
20813
  context,
20122
20814
  });
20123
20815
  bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL);
20816
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_PENDING_SIGNAL);
20124
20817
  {
20125
20818
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20126
20819
  riskName &&
@@ -20152,6 +20845,7 @@ class BacktestUtils {
20152
20845
  context,
20153
20846
  });
20154
20847
  bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL);
20848
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_SCHEDULED_SIGNAL);
20155
20849
  {
20156
20850
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20157
20851
  riskName &&
@@ -20161,6 +20855,47 @@ class BacktestUtils {
20161
20855
  }
20162
20856
  return await bt.strategyCoreService.getScheduledSignal(true, symbol, context);
20163
20857
  };
20858
+ /**
20859
+ * Checks if breakeven threshold has been reached for the current pending signal.
20860
+ *
20861
+ * Uses the same formula as BREAKEVEN_FN to determine if price has moved far enough
20862
+ * to cover transaction costs (slippage + fees) and allow breakeven to be set.
20863
+ *
20864
+ * @param symbol - Trading pair symbol
20865
+ * @param currentPrice - Current market price to check against threshold
20866
+ * @param context - Execution context with strategyName, exchangeName, frameName
20867
+ * @returns Promise<boolean> - true if breakeven threshold reached, false otherwise
20868
+ *
20869
+ * @example
20870
+ * ```typescript
20871
+ * const canBreakeven = await Backtest.getBreakeven("BTCUSDT", 100.5, {
20872
+ * strategyName: "my-strategy",
20873
+ * exchangeName: "binance",
20874
+ * frameName: "backtest_frame"
20875
+ * });
20876
+ * if (canBreakeven) {
20877
+ * console.log("Breakeven threshold reached");
20878
+ * await Backtest.breakeven("BTCUSDT", 100.5, context);
20879
+ * }
20880
+ * ```
20881
+ */
20882
+ this.getBreakeven = async (symbol, currentPrice, context) => {
20883
+ bt.loggerService.info(BACKTEST_METHOD_NAME_GET_BREAKEVEN, {
20884
+ symbol,
20885
+ currentPrice,
20886
+ context,
20887
+ });
20888
+ bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_BREAKEVEN);
20889
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_BREAKEVEN);
20890
+ {
20891
+ const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20892
+ riskName &&
20893
+ bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_BREAKEVEN);
20894
+ riskList &&
20895
+ riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_GET_BREAKEVEN));
20896
+ }
20897
+ return await bt.strategyCoreService.getBreakeven(true, symbol, currentPrice, context);
20898
+ };
20164
20899
  /**
20165
20900
  * Stops the strategy from generating new signals.
20166
20901
  *
@@ -20189,6 +20924,7 @@ class BacktestUtils {
20189
20924
  context,
20190
20925
  });
20191
20926
  bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_STOP);
20927
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_STOP);
20192
20928
  {
20193
20929
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20194
20930
  riskName &&
@@ -20228,6 +20964,7 @@ class BacktestUtils {
20228
20964
  cancelId,
20229
20965
  });
20230
20966
  bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_CANCEL);
20967
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_CANCEL);
20231
20968
  {
20232
20969
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20233
20970
  riskName &&
@@ -20271,6 +21008,7 @@ class BacktestUtils {
20271
21008
  context,
20272
21009
  });
20273
21010
  bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT);
21011
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_PARTIAL_PROFIT);
20274
21012
  {
20275
21013
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20276
21014
  riskName &&
@@ -20314,6 +21052,7 @@ class BacktestUtils {
20314
21052
  context,
20315
21053
  });
20316
21054
  bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_PARTIAL_LOSS);
21055
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_PARTIAL_LOSS);
20317
21056
  {
20318
21057
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20319
21058
  riskName &&
@@ -20331,27 +21070,30 @@ class BacktestUtils {
20331
21070
  *
20332
21071
  * @param symbol - Trading pair symbol
20333
21072
  * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
21073
+ * @param currentPrice - Current market price to check for intrusion
20334
21074
  * @param context - Execution context with strategyName, exchangeName, and frameName
20335
21075
  * @returns Promise that resolves when trailing SL is updated
20336
21076
  *
20337
21077
  * @example
20338
21078
  * ```typescript
20339
- * // LONG: entry=100, originalSL=90, distance=10
20340
- * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
20341
- * await Backtest.trailingStop("BTCUSDT", -50, {
21079
+ * // LONG: entry=100, originalSL=90, distance=10%, currentPrice=102
21080
+ * // Tighten stop by 50%: newSL = 100 - 5% = 95
21081
+ * await Backtest.trailingStop("BTCUSDT", -50, 102, {
20342
21082
  * exchangeName: "binance",
20343
21083
  * frameName: "frame1",
20344
21084
  * strategyName: "my-strategy"
20345
21085
  * });
20346
21086
  * ```
20347
21087
  */
20348
- this.trailingStop = async (symbol, percentShift, context) => {
21088
+ this.trailingStop = async (symbol, percentShift, currentPrice, context) => {
20349
21089
  bt.loggerService.info(BACKTEST_METHOD_NAME_TRAILING_STOP, {
20350
21090
  symbol,
20351
21091
  percentShift,
21092
+ currentPrice,
20352
21093
  context,
20353
21094
  });
20354
21095
  bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_TRAILING_STOP);
21096
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_TRAILING_STOP);
20355
21097
  {
20356
21098
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20357
21099
  riskName &&
@@ -20359,7 +21101,49 @@ class BacktestUtils {
20359
21101
  riskList &&
20360
21102
  riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, BACKTEST_METHOD_NAME_TRAILING_STOP));
20361
21103
  }
20362
- await bt.strategyCoreService.trailingStop(true, symbol, percentShift, context);
21104
+ await bt.strategyCoreService.trailingStop(true, symbol, percentShift, currentPrice, context);
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);
20363
21147
  };
20364
21148
  /**
20365
21149
  * Moves stop-loss to breakeven when price reaches threshold.
@@ -20389,6 +21173,7 @@ class BacktestUtils {
20389
21173
  context,
20390
21174
  });
20391
21175
  bt.strategyValidationService.validate(context.strategyName, "Backtest.breakeven");
21176
+ bt.exchangeValidationService.validate(context.exchangeName, "Backtest.breakeven");
20392
21177
  {
20393
21178
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20394
21179
  riskName &&
@@ -20422,6 +21207,7 @@ class BacktestUtils {
20422
21207
  context,
20423
21208
  });
20424
21209
  bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_DATA);
21210
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_DATA);
20425
21211
  {
20426
21212
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20427
21213
  riskName &&
@@ -20456,6 +21242,7 @@ class BacktestUtils {
20456
21242
  context,
20457
21243
  });
20458
21244
  bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_GET_REPORT);
21245
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_GET_REPORT);
20459
21246
  {
20460
21247
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20461
21248
  riskName &&
@@ -20498,6 +21285,7 @@ class BacktestUtils {
20498
21285
  path,
20499
21286
  });
20500
21287
  bt.strategyValidationService.validate(context.strategyName, BACKTEST_METHOD_NAME_DUMP);
21288
+ bt.exchangeValidationService.validate(context.exchangeName, BACKTEST_METHOD_NAME_DUMP);
20501
21289
  {
20502
21290
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20503
21291
  riskName &&
@@ -20556,10 +21344,12 @@ const LIVE_METHOD_NAME_TASK = "LiveUtils.task";
20556
21344
  const LIVE_METHOD_NAME_GET_STATUS = "LiveUtils.getStatus";
20557
21345
  const LIVE_METHOD_NAME_GET_PENDING_SIGNAL = "LiveUtils.getPendingSignal";
20558
21346
  const LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL = "LiveUtils.getScheduledSignal";
21347
+ const LIVE_METHOD_NAME_GET_BREAKEVEN = "LiveUtils.getBreakeven";
20559
21348
  const LIVE_METHOD_NAME_CANCEL = "LiveUtils.cancel";
20560
21349
  const LIVE_METHOD_NAME_PARTIAL_PROFIT = "LiveUtils.partialProfit";
20561
21350
  const LIVE_METHOD_NAME_PARTIAL_LOSS = "LiveUtils.partialLoss";
20562
21351
  const LIVE_METHOD_NAME_TRAILING_STOP = "LiveUtils.trailingStop";
21352
+ const LIVE_METHOD_NAME_TRAILING_PROFIT = "LiveUtils.trailingProfit";
20563
21353
  /**
20564
21354
  * Internal task function that runs live trading and handles completion.
20565
21355
  * Consumes live trading results and updates instance state flags.
@@ -20897,6 +21687,7 @@ class LiveUtils {
20897
21687
  context,
20898
21688
  });
20899
21689
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL);
21690
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL);
20900
21691
  {
20901
21692
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20902
21693
  riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_PENDING_SIGNAL);
@@ -20930,6 +21721,7 @@ class LiveUtils {
20930
21721
  context,
20931
21722
  });
20932
21723
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL);
21724
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL);
20933
21725
  {
20934
21726
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20935
21727
  riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_SCHEDULED_SIGNAL);
@@ -20941,6 +21733,48 @@ class LiveUtils {
20941
21733
  frameName: "",
20942
21734
  });
20943
21735
  };
21736
+ /**
21737
+ * Checks if breakeven threshold has been reached for the current pending signal.
21738
+ *
21739
+ * Uses the same formula as BREAKEVEN_FN to determine if price has moved far enough
21740
+ * to cover transaction costs (slippage + fees) and allow breakeven to be set.
21741
+ *
21742
+ * @param symbol - Trading pair symbol
21743
+ * @param currentPrice - Current market price to check against threshold
21744
+ * @param context - Execution context with strategyName and exchangeName
21745
+ * @returns Promise<boolean> - true if breakeven threshold reached, false otherwise
21746
+ *
21747
+ * @example
21748
+ * ```typescript
21749
+ * const canBreakeven = await Live.getBreakeven("BTCUSDT", 100.5, {
21750
+ * strategyName: "my-strategy",
21751
+ * exchangeName: "binance"
21752
+ * });
21753
+ * if (canBreakeven) {
21754
+ * console.log("Breakeven threshold reached");
21755
+ * await Live.breakeven("BTCUSDT", 100.5, context);
21756
+ * }
21757
+ * ```
21758
+ */
21759
+ this.getBreakeven = async (symbol, currentPrice, context) => {
21760
+ bt.loggerService.info(LIVE_METHOD_NAME_GET_BREAKEVEN, {
21761
+ symbol,
21762
+ currentPrice,
21763
+ context,
21764
+ });
21765
+ bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_BREAKEVEN);
21766
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_BREAKEVEN);
21767
+ {
21768
+ const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21769
+ riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_BREAKEVEN);
21770
+ riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_BREAKEVEN));
21771
+ }
21772
+ return await bt.strategyCoreService.getBreakeven(false, symbol, currentPrice, {
21773
+ strategyName: context.strategyName,
21774
+ exchangeName: context.exchangeName,
21775
+ frameName: "",
21776
+ });
21777
+ };
20944
21778
  /**
20945
21779
  * Stops the strategy from generating new signals.
20946
21780
  *
@@ -20964,6 +21798,7 @@ class LiveUtils {
20964
21798
  context,
20965
21799
  });
20966
21800
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_STOP);
21801
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_STOP);
20967
21802
  {
20968
21803
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
20969
21804
  riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_STOP);
@@ -21005,6 +21840,7 @@ class LiveUtils {
21005
21840
  cancelId,
21006
21841
  });
21007
21842
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_CANCEL);
21843
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_CANCEL);
21008
21844
  {
21009
21845
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21010
21846
  riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_CANCEL);
@@ -21049,6 +21885,7 @@ class LiveUtils {
21049
21885
  context,
21050
21886
  });
21051
21887
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
21888
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
21052
21889
  {
21053
21890
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21054
21891
  riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_PROFIT);
@@ -21093,6 +21930,7 @@ class LiveUtils {
21093
21930
  context,
21094
21931
  });
21095
21932
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_PARTIAL_LOSS);
21933
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_PARTIAL_LOSS);
21096
21934
  {
21097
21935
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21098
21936
  riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_PARTIAL_LOSS);
@@ -21112,32 +21950,78 @@ class LiveUtils {
21112
21950
  *
21113
21951
  * @param symbol - Trading pair symbol
21114
21952
  * @param percentShift - Percentage adjustment to SL distance (-100 to 100)
21953
+ * @param currentPrice - Current market price to check for intrusion
21115
21954
  * @param context - Execution context with strategyName and exchangeName
21116
21955
  * @returns Promise that resolves when trailing SL is updated
21117
21956
  *
21118
21957
  * @example
21119
21958
  * ```typescript
21120
- * // LONG: entry=100, originalSL=90, distance=10
21121
- * // Tighten stop by 50%: newSL = 100 - 10*(1-0.5) = 95
21122
- * await Live.trailingStop("BTCUSDT", -50, {
21959
+ * // LONG: entry=100, originalSL=90, distance=10%, currentPrice=102
21960
+ * // Tighten stop by 50%: newSL = 100 - 5% = 95
21961
+ * await Live.trailingStop("BTCUSDT", -50, 102, {
21123
21962
  * exchangeName: "binance",
21124
21963
  * strategyName: "my-strategy"
21125
21964
  * });
21126
21965
  * ```
21127
21966
  */
21128
- this.trailingStop = async (symbol, percentShift, context) => {
21967
+ this.trailingStop = async (symbol, percentShift, currentPrice, context) => {
21129
21968
  bt.loggerService.info(LIVE_METHOD_NAME_TRAILING_STOP, {
21130
21969
  symbol,
21131
21970
  percentShift,
21971
+ currentPrice,
21132
21972
  context,
21133
21973
  });
21134
21974
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_TRAILING_STOP);
21975
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_TRAILING_STOP);
21135
21976
  {
21136
21977
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21137
21978
  riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP);
21138
21979
  riskList && riskList.forEach((riskName) => bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_TRAILING_STOP));
21139
21980
  }
21140
- await bt.strategyCoreService.trailingStop(false, symbol, percentShift, {
21981
+ await bt.strategyCoreService.trailingStop(false, symbol, percentShift, currentPrice, {
21982
+ strategyName: context.strategyName,
21983
+ exchangeName: context.exchangeName,
21984
+ frameName: "",
21985
+ });
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, {
21141
22025
  strategyName: context.strategyName,
21142
22026
  exchangeName: context.exchangeName,
21143
22027
  frameName: "",
@@ -21171,6 +22055,7 @@ class LiveUtils {
21171
22055
  context,
21172
22056
  });
21173
22057
  bt.strategyValidationService.validate(context.strategyName, "Live.breakeven");
22058
+ bt.exchangeValidationService.validate(context.exchangeName, "Live.breakeven");
21174
22059
  {
21175
22060
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21176
22061
  riskName && bt.riskValidationService.validate(riskName, "Live.breakeven");
@@ -21206,6 +22091,7 @@ class LiveUtils {
21206
22091
  context,
21207
22092
  });
21208
22093
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_DATA);
22094
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_DATA);
21209
22095
  {
21210
22096
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21211
22097
  riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_DATA);
@@ -21238,6 +22124,7 @@ class LiveUtils {
21238
22124
  context,
21239
22125
  });
21240
22126
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_GET_REPORT);
22127
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_GET_REPORT);
21241
22128
  {
21242
22129
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21243
22130
  riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_GET_REPORT);
@@ -21278,6 +22165,7 @@ class LiveUtils {
21278
22165
  path,
21279
22166
  });
21280
22167
  bt.strategyValidationService.validate(context.strategyName, LIVE_METHOD_NAME_DUMP);
22168
+ bt.exchangeValidationService.validate(context.exchangeName, LIVE_METHOD_NAME_DUMP);
21281
22169
  {
21282
22170
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21283
22171
  riskName && bt.riskValidationService.validate(riskName, LIVE_METHOD_NAME_DUMP);
@@ -21371,6 +22259,7 @@ class ScheduleUtils {
21371
22259
  backtest,
21372
22260
  });
21373
22261
  bt.strategyValidationService.validate(context.strategyName, SCHEDULE_METHOD_NAME_GET_DATA);
22262
+ bt.exchangeValidationService.validate(context.exchangeName, SCHEDULE_METHOD_NAME_GET_DATA);
21374
22263
  {
21375
22264
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21376
22265
  riskName && bt.riskValidationService.validate(riskName, SCHEDULE_METHOD_NAME_GET_DATA);
@@ -21399,6 +22288,7 @@ class ScheduleUtils {
21399
22288
  backtest,
21400
22289
  });
21401
22290
  bt.strategyValidationService.validate(context.strategyName, SCHEDULE_METHOD_NAME_GET_REPORT);
22291
+ bt.exchangeValidationService.validate(context.exchangeName, SCHEDULE_METHOD_NAME_GET_REPORT);
21402
22292
  {
21403
22293
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21404
22294
  riskName && bt.riskValidationService.validate(riskName, SCHEDULE_METHOD_NAME_GET_REPORT);
@@ -21431,6 +22321,7 @@ class ScheduleUtils {
21431
22321
  path,
21432
22322
  });
21433
22323
  bt.strategyValidationService.validate(context.strategyName, SCHEDULE_METHOD_NAME_DUMP);
22324
+ bt.exchangeValidationService.validate(context.exchangeName, SCHEDULE_METHOD_NAME_DUMP);
21434
22325
  {
21435
22326
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21436
22327
  riskName && bt.riskValidationService.validate(riskName, SCHEDULE_METHOD_NAME_DUMP);
@@ -21519,6 +22410,7 @@ class Performance {
21519
22410
  */
21520
22411
  static async getData(symbol, context, backtest = false) {
21521
22412
  bt.strategyValidationService.validate(context.strategyName, PERFORMANCE_METHOD_NAME_GET_DATA);
22413
+ bt.exchangeValidationService.validate(context.exchangeName, PERFORMANCE_METHOD_NAME_GET_DATA);
21522
22414
  {
21523
22415
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21524
22416
  riskName && bt.riskValidationService.validate(riskName, PERFORMANCE_METHOD_NAME_GET_DATA);
@@ -21551,6 +22443,7 @@ class Performance {
21551
22443
  */
21552
22444
  static async getReport(symbol, context, backtest = false, columns) {
21553
22445
  bt.strategyValidationService.validate(context.strategyName, PERFORMANCE_METHOD_NAME_GET_REPORT);
22446
+ bt.exchangeValidationService.validate(context.exchangeName, PERFORMANCE_METHOD_NAME_GET_REPORT);
21554
22447
  {
21555
22448
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21556
22449
  riskName && bt.riskValidationService.validate(riskName, PERFORMANCE_METHOD_NAME_GET_REPORT);
@@ -21580,6 +22473,7 @@ class Performance {
21580
22473
  */
21581
22474
  static async dump(symbol, context, backtest = false, path = "./dump/performance", columns) {
21582
22475
  bt.strategyValidationService.validate(context.strategyName, PERFORMANCE_METHOD_NAME_DUMP);
22476
+ bt.exchangeValidationService.validate(context.exchangeName, PERFORMANCE_METHOD_NAME_DUMP);
21583
22477
  {
21584
22478
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
21585
22479
  riskName && bt.riskValidationService.validate(riskName, PERFORMANCE_METHOD_NAME_DUMP);
@@ -22170,6 +23064,7 @@ class HeatUtils {
22170
23064
  this.getData = async (context, backtest = false) => {
22171
23065
  bt.loggerService.info(HEAT_METHOD_NAME_GET_DATA, { strategyName: context.strategyName });
22172
23066
  bt.strategyValidationService.validate(context.strategyName, HEAT_METHOD_NAME_GET_DATA);
23067
+ bt.exchangeValidationService.validate(context.exchangeName, HEAT_METHOD_NAME_GET_DATA);
22173
23068
  {
22174
23069
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
22175
23070
  riskName && bt.riskValidationService.validate(riskName, HEAT_METHOD_NAME_GET_DATA);
@@ -22211,6 +23106,7 @@ class HeatUtils {
22211
23106
  this.getReport = async (context, backtest = false, columns) => {
22212
23107
  bt.loggerService.info(HEAT_METHOD_NAME_GET_REPORT, { strategyName: context.strategyName });
22213
23108
  bt.strategyValidationService.validate(context.strategyName, HEAT_METHOD_NAME_GET_REPORT);
23109
+ bt.exchangeValidationService.validate(context.exchangeName, HEAT_METHOD_NAME_GET_REPORT);
22214
23110
  {
22215
23111
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
22216
23112
  riskName && bt.riskValidationService.validate(riskName, HEAT_METHOD_NAME_GET_REPORT);
@@ -22249,6 +23145,7 @@ class HeatUtils {
22249
23145
  this.dump = async (context, backtest = false, path, columns) => {
22250
23146
  bt.loggerService.info(HEAT_METHOD_NAME_DUMP, { strategyName: context.strategyName, path });
22251
23147
  bt.strategyValidationService.validate(context.strategyName, HEAT_METHOD_NAME_DUMP);
23148
+ bt.exchangeValidationService.validate(context.exchangeName, HEAT_METHOD_NAME_DUMP);
22252
23149
  {
22253
23150
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
22254
23151
  riskName && bt.riskValidationService.validate(riskName, HEAT_METHOD_NAME_DUMP);
@@ -22581,6 +23478,7 @@ class PartialUtils {
22581
23478
  this.getData = async (symbol, context, backtest = false) => {
22582
23479
  bt.loggerService.info(PARTIAL_METHOD_NAME_GET_DATA, { symbol, strategyName: context.strategyName });
22583
23480
  bt.strategyValidationService.validate(context.strategyName, PARTIAL_METHOD_NAME_GET_DATA);
23481
+ bt.exchangeValidationService.validate(context.exchangeName, PARTIAL_METHOD_NAME_GET_DATA);
22584
23482
  {
22585
23483
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
22586
23484
  riskName && bt.riskValidationService.validate(riskName, PARTIAL_METHOD_NAME_GET_DATA);
@@ -22630,6 +23528,7 @@ class PartialUtils {
22630
23528
  this.getReport = async (symbol, context, backtest = false, columns) => {
22631
23529
  bt.loggerService.info(PARTIAL_METHOD_NAME_GET_REPORT, { symbol, strategyName: context.strategyName });
22632
23530
  bt.strategyValidationService.validate(context.strategyName, PARTIAL_METHOD_NAME_GET_REPORT);
23531
+ bt.exchangeValidationService.validate(context.exchangeName, PARTIAL_METHOD_NAME_GET_REPORT);
22633
23532
  {
22634
23533
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
22635
23534
  riskName && bt.riskValidationService.validate(riskName, PARTIAL_METHOD_NAME_GET_REPORT);
@@ -22672,6 +23571,7 @@ class PartialUtils {
22672
23571
  this.dump = async (symbol, context, backtest = false, path, columns) => {
22673
23572
  bt.loggerService.info(PARTIAL_METHOD_NAME_DUMP, { symbol, strategyName: context.strategyName, path });
22674
23573
  bt.strategyValidationService.validate(context.strategyName, PARTIAL_METHOD_NAME_DUMP);
23574
+ bt.exchangeValidationService.validate(context.exchangeName, PARTIAL_METHOD_NAME_DUMP);
22675
23575
  {
22676
23576
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
22677
23577
  riskName && bt.riskValidationService.validate(riskName, PARTIAL_METHOD_NAME_DUMP);
@@ -23861,6 +24761,7 @@ class BreakevenUtils {
23861
24761
  this.getData = async (symbol, context, backtest = false) => {
23862
24762
  bt.loggerService.info(BREAKEVEN_METHOD_NAME_GET_DATA, { symbol, strategyName: context.strategyName });
23863
24763
  bt.strategyValidationService.validate(context.strategyName, BREAKEVEN_METHOD_NAME_GET_DATA);
24764
+ bt.exchangeValidationService.validate(context.exchangeName, BREAKEVEN_METHOD_NAME_GET_DATA);
23864
24765
  {
23865
24766
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
23866
24767
  riskName && bt.riskValidationService.validate(riskName, BREAKEVEN_METHOD_NAME_GET_DATA);
@@ -23906,6 +24807,7 @@ class BreakevenUtils {
23906
24807
  this.getReport = async (symbol, context, backtest = false, columns) => {
23907
24808
  bt.loggerService.info(BREAKEVEN_METHOD_NAME_GET_REPORT, { symbol, strategyName: context.strategyName });
23908
24809
  bt.strategyValidationService.validate(context.strategyName, BREAKEVEN_METHOD_NAME_GET_REPORT);
24810
+ bt.exchangeValidationService.validate(context.exchangeName, BREAKEVEN_METHOD_NAME_GET_REPORT);
23909
24811
  {
23910
24812
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
23911
24813
  riskName && bt.riskValidationService.validate(riskName, BREAKEVEN_METHOD_NAME_GET_REPORT);
@@ -23948,6 +24850,7 @@ class BreakevenUtils {
23948
24850
  this.dump = async (symbol, context, backtest = false, path, columns) => {
23949
24851
  bt.loggerService.info(BREAKEVEN_METHOD_NAME_DUMP, { symbol, strategyName: context.strategyName, path });
23950
24852
  bt.strategyValidationService.validate(context.strategyName, BREAKEVEN_METHOD_NAME_DUMP);
24853
+ bt.exchangeValidationService.validate(context.exchangeName, BREAKEVEN_METHOD_NAME_DUMP);
23951
24854
  {
23952
24855
  const { riskName, riskList } = bt.strategySchemaService.get(context.strategyName);
23953
24856
  riskName && bt.riskValidationService.validate(riskName, BREAKEVEN_METHOD_NAME_DUMP);
@@ -23973,4 +24876,4 @@ class BreakevenUtils {
23973
24876
  */
23974
24877
  const Breakeven = new BreakevenUtils();
23975
24878
 
23976
- 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 };