💬 Forum

BinHV27CombinedStrategy

remiotore/ccxt-freqtrade/strategies/LS_converted.py · ★3 · ⑂2 · first seen 2026-07-28 · repo updated 2026-01-11

Basics mode: futures timeframe: 1h interface version: 3
Settings stoploss: -1.0 has minimal roi custom stoploss hyperopt hyperopt params: 45
Indicators EMA Linear_Regression RSI SMA talib
15 related strategies ( identical code, similar name)

Each tile is a different kind of check — from an instant code lint to full sandboxed backtests and forward tests on recent data. Not sure what a check actually proves? See the FAQ →

Source

Download Raw
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
"""
Freqtrade strategy equivalent to the TradingView Pine Script (BinHV27_combined).
Replicates the same parameters, indicators, entry/exit logic, dynamic stop-loss, etc.

You must enable "can_short = True" in order to allow short entries, and configure
Freqtrade for margin or futures accordingly.

Disclaimer: The code is provided as an illustrative example; you may need to
adjust it to your environment or for minor syntax changes if using newer versions
of freqtrade/pandas_ta.
"""
import logging
from typing import Dict, List, Optional, Any
import pandas as pd
import numpy as np
# Freqtrade imports
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy import IntParameter, DecimalParameter, BooleanParameter
from freqtrade.persistence import Trade
from freqtrade.exchange import timeframe_to_minutes
# Third-party libraries
import talib.abstract as ta
logger = logging.getLogger(__name__)
#################################################################
# Helper functions
#################################################################

def linreg(series: pd.Series, period: int=2) -> pd.Series:
    """
    Mimics TradingView's ta.linreg(series, period=2, offset=0).
    In TradingView, linreg(...) = linear regression of 'period' bars with 0 offset.
    For an exact TSF/linreg(2,0), the slope typically uses 1…period sample.
    The formula for TSF (Time Series Forecast) at bar i is:
        TSF(i) = a0 + a1 * (period-1)
    where a1 is slope, a0 intercept.
    
    A simpler approach for small periods:
      - We can use a direct slope calculation between current bar and bar N bars ago
        or
      - Use a standard least-squares polynomial fit on the last `period` points.
    
    Because period=2 is very short, the slope is basically (series - series.shift(1)).
    We'll do a minimal approach that tries to replicate tv's linreg(…,2).
    """
    # For period=2 specifically, it's basically last 2 bars slope-based forecast:
    # slope = (val1 - val0)/1, intercept = val1 - slope*1
    # TSF(current) = intercept + slope*(period-1) = intercept + slope
    # which effectively equals val1 for offset=0. 
    # However, to emulate typical "linreg" with a 'least squares' approach for period=2:
    # Using a standard formula for "least squares" on 2 bars is almost the same as the last known price.
    # We'll implement a generic approach for any period using polyfit:
    linreg_vals = series.rolling(period).apply(lambda x: np.polyval(np.polyfit(range(period), x.values, 1), period - 1), raw=False)
    return linreg_vals

def hlc3(df: pd.DataFrame) -> pd.Series:
    """Return typical price (H+L+C)/3 for each row."""
    return (df['high'] + df['low'] + df['close']) / 3.0
#################################################################
# Main Strategy Class
#################################################################

class BinHV27CombinedStrategy(IStrategy):
    """
    Converted from Pine Script:
    - Strategy name: "BinHV27_combined"
    - Emulates all parameters, logic, dynamic stoploss, custom exit.
    """
    ####################################################
    # --- Standard Freqtrade parameters
    ####################################################
    INTERFACE_VERSION = 3
    # This example uses 1h as base timeframe. 
    # Adjust if your Pine Script used a different base resolution.
    timeframe = '1h'
    # For multi-timeframe analysis, we fetch 4h data via informative pair.
    informative_timeframe = '4h'
    # Enable short for margin/futures mode. 
    can_short = True
    # Minimal ROI and stoploss just as placeholders. We'll override with custom stoploss.
    minimal_roi = {'0': 100}  # effectively no static ROI exit
    stoploss = -1  # we rely on custom_stoploss.
    # If you want to allow custom exit, set to True
    use_custom_stoploss = True
    use_custom_exit = True
    ####################################################
    # --- Pine Script Inputs as Freqtrade parameters ---
    ####################################################
    # 1) Buy parameters (long)
    entry_long_adx1 = IntParameter(10, 100, default=25, space='buy', optimize=False)
    entry_long_emarsi1 = IntParameter(10, 100, default=20, space='buy', optimize=False)
    entry_long_adx2 = IntParameter(20, 100, default=30, space='buy', optimize=False)
    entry_long_emarsi2 = IntParameter(20, 100, default=20, space='buy', optimize=False)
    entry_long_adx3 = IntParameter(10, 100, default=35, space='buy', optimize=False)
    entry_long_emarsi3 = IntParameter(10, 100, default=20, space='buy', optimize=False)
    entry_long_adx4 = IntParameter(20, 100, default=30, space='buy', optimize=False)
    entry_long_emarsi4 = IntParameter(20, 100, default=25, space='buy', optimize=False)
    # 2) Buy parameters (short)
    entry_short_adx1 = IntParameter(10, 100, default=62, space='buy', optimize=False)
    entry_short_emarsi1 = IntParameter(10, 100, default=29, space='buy', optimize=False)
    entry_short_adx2 = IntParameter(20, 100, default=29, space='buy', optimize=False)
    entry_short_emarsi2 = IntParameter(20, 100, default=30, space='buy', optimize=False)
    entry_short_adx3 = IntParameter(10, 100, default=33, space='buy', optimize=False)
    entry_short_emarsi3 = IntParameter(10, 100, default=22, space='buy', optimize=False)
    entry_short_adx4 = IntParameter(20, 100, default=88, space='buy', optimize=False)
    entry_short_emarsi4 = IntParameter(20, 100, default=57, space='buy', optimize=False)
    # 3) Dynamic stop parameters (long)
    pHSL_long = DecimalParameter(-0.99, -0.04, default=-0.25, space='sell', optimize=False)
    pPF_1_long = DecimalParameter(0.008, 0.1, default=0.012, space='sell', optimize=False)
    pSL_1_long = DecimalParameter(0.008, 0.1, default=0.01, space='sell', optimize=False)
    pPF_2_long = DecimalParameter(0.04, 0.2, default=0.05, space='sell', optimize=False)
    pSL_2_long = DecimalParameter(0.04, 0.2, default=0.04, space='sell', optimize=False)
    # 4) Dynamic stop parameters (short)
    pHSL_short = DecimalParameter(-0.99, -0.04, default=-0.863, space='sell', optimize=False)
    pPF_1_short = DecimalParameter(0.008, 0.1, default=0.018, space='sell', optimize=False)
    pSL_1_short = DecimalParameter(0.008, 0.1, default=0.015, space='sell', optimize=False)
    pPF_2_short = DecimalParameter(0.04, 0.2, default=0.197, space='sell', optimize=False)
    pSL_2_short = DecimalParameter(0.04, 0.2, default=0.157, space='sell', optimize=False)
    # 5) Exit parameters (long)
    exit_long_emarsi1 = IntParameter(10, 100, default=75, space='sell', optimize=False)
    exit_long_adx2 = IntParameter(10, 100, default=30, space='sell', optimize=False)
    exit_long_emarsi2 = IntParameter(20, 100, default=80, space='sell', optimize=False)
    exit_long_emarsi3 = IntParameter(20, 100, default=75, space='sell', optimize=False)
    # 6) Exit parameters (short)
    exit_short_emarsi1 = IntParameter(10, 100, default=30, space='sell', optimize=False)
    exit_short_adx2 = IntParameter(10, 100, default=21, space='sell', optimize=False)
    exit_short_emarsi2 = IntParameter(20, 100, default=71, space='sell', optimize=False)
    exit_short_emarsi3 = IntParameter(20, 100, default=72, space='sell', optimize=False)
    # 7) Exit switches
    exit_long_1 = BooleanParameter(default=True, space='sell', optimize=False)
    exit_long_2 = BooleanParameter(default=True, space='sell', optimize=False)
    exit_long_3 = BooleanParameter(default=True, space='sell', optimize=False)
    exit_long_4 = BooleanParameter(default=True, space='sell', optimize=False)
    exit_long_5 = BooleanParameter(default=True, space='sell', optimize=False)
    exit_short_1 = BooleanParameter(default=False, space='sell', optimize=False)
    exit_short_2 = BooleanParameter(default=True, space='sell', optimize=False)
    exit_short_3 = BooleanParameter(default=True, space='sell', optimize=False)
    exit_short_4 = BooleanParameter(default=True, space='sell', optimize=False)
    exit_short_5 = BooleanParameter(default=False, space='sell', optimize=False)
    # 8) Leverage param (informational only)
    leverage_num = IntParameter(1, 5, default=1, space='buy', optimize=False)
    ###############################################################
    # Informative pairs: We fetch 4h data for multi-timeframe logic
    ###############################################################

    def informative_pairs(self) -> List[tuple]:
        """
        Define additional (pair, timeframe) combinations to fetch for analysis.
        We'll request the same pair at the 4h timeframe.
        """
        pairs = []
        # We want to fetch the base pair at 4h
        pairs.append((self.dp.current_pair, self.informative_timeframe))
        return pairs

    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """
        Populate indicators for the 1h (base) timeframe.
        We'll also merge the 4h indicators (linreg-based) after computing them
        in a separate method or by using the informative pair logic.
        """
        # -------------------------------------------------------------
        # 1) Standard indicators on 1h (base timeframe)
        # -------------------------------------------------------------
        # RSI(5)
        dataframe['rsi'] = ta.RSI(dataframe, timeperiod=5)
        # EMA of RSI(5)
        dataframe['emarsi'] = ta.EMA(dataframe['rsi'], timeperiod=5)
        # DMI(14) => ADX, plusDI, minusDI
        adx_all = ta.DX(dataframe, timeperiod=14)
        # ta.DX returns ADX alone. We also need +DI, -DI from a custom approach:
        # We'll replicate them quickly:
        # plusDI = 100 * (EMA( Max( (+DM,0) ), 14 ) / ATR(14))
        # minusDI = 100 * (EMA( Max( (-DM,0) ), 14 ) / ATR(14))
        # or we can do a quick approach from pandas_ta. 
        # If you prefer pure TA-Lib, you'd do:
        #   +DI = ta.PLUS_DI(dataframe, 14)
        #   -DI = ta.MINUS_DI(dataframe, 14)
        # but let's do that:
        dataframe['plus_di'] = ta.PLUS_DI(dataframe, timeperiod=14)
        dataframe['minus_di'] = ta.MINUS_DI(dataframe, timeperiod=14)
        dataframe['adx'] = adx_all
        # -DIEMA(25) / +DIEMA(5)
        dataframe['minus_di_ema25'] = ta.EMA(dataframe['minus_di'], timeperiod=25)
        dataframe['plus_di_ema5'] = ta.EMA(dataframe['plus_di'], timeperiod=5)
        # EMA(60), EMA(120), SMA(120), SMA(240)
        dataframe['ema60'] = ta.EMA(dataframe, timeperiod=60)
        dataframe['ema120'] = ta.EMA(dataframe, timeperiod=120)
        dataframe['sma120'] = ta.SMA(dataframe, timeperiod=120)
        dataframe['sma240'] = ta.SMA(dataframe, timeperiod=240)
        # bigup / bigdown logic
        # bigup = (fastsma > slowsma) and ((fastsma - slowsma) > close/300)
        dataframe['fastsma'] = dataframe['sma120']
        dataframe['slowsma'] = dataframe['sma240']
        dataframe['bigup'] = (dataframe['fastsma'] > dataframe['slowsma']) & (dataframe['fastsma'] - dataframe['slowsma'] > dataframe['close'] / 300)
        dataframe['bigdown'] = ~dataframe['bigup']
        # "trend" for difference and checks
        dataframe['trend'] = dataframe['fastsma'] - dataframe['slowsma']
        # preparechangetrend = trend > trend[1]
        dataframe['trend_prev'] = dataframe['trend'].shift(1)
        dataframe['trend_prev2'] = dataframe['trend'].shift(2)
        dataframe['preparechangetrend'] = dataframe['trend'] > dataframe['trend_prev']
        dataframe['preparechangetrendconfirm'] = dataframe['preparechangetrend'] & (dataframe['trend_prev'] > dataframe['trend_prev2'])
        # continueup = (slowsma>slowsma[1]) and (slowsma[1]>slowsma[2])
        dataframe['slowsma_prev'] = dataframe['slowsma'].shift(1)
        dataframe['slowsma_prev2'] = dataframe['slowsma'].shift(2)
        dataframe['continueup'] = (dataframe['slowsma'] > dataframe['slowsma_prev']) & (dataframe['slowsma_prev'] > dataframe['slowsma_prev2'])
        # delta = fastsma - fastsma[1]
        dataframe['fastsma_prev'] = dataframe['fastsma'].shift(1)
        dataframe['delta'] = dataframe['fastsma'] - dataframe['fastsma_prev']
        dataframe['delta_prev'] = dataframe['delta'].shift(1)
        dataframe['slowingdown'] = dataframe['delta'] < dataframe['delta_prev']
        # Bollinger (20,2) for reference (not directly used in entry/exit, but keep it)
        bb_basis = ta.SMA(hlc3(dataframe), timeperiod=20)
        bb_std = 2.0 * pd.Series.rolling(hlc3(dataframe), window=20).std()
        dataframe['bb_upperband'] = bb_basis + bb_std
        dataframe['bb_middleband'] = bb_basis
        dataframe['bb_lowerband'] = bb_basis - bb_std
        # -------------------------------------------------------------
        # 2) Fetch and merge 4h informative data
        # -------------------------------------------------------------
        informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=self.informative_timeframe)
        # We compute h_close4H, h_high4H, h_low4H => then do hlc3_4h => then linreg(2)
        informative['h_close4H'] = informative['close']
        informative['h_high4H'] = informative['high']
        informative['h_low4H'] = informative['low']
        informative['hlc3_4h'] = hlc3(informative)
        # replicate ta.linreg(hlc3_4h, 2, 0)
        informative['tsf_4h'] = linreg(informative['hlc3_4h'], period=2)
        # Only keep the columns we need, and rename them for clarity
        informative = informative[['date', 'hlc3_4h', 'tsf_4h']]
        # Merge with base timeframe
        dataframe = dataframe.merge(informative, on='date', how='left', suffixes=('', '_4h'))
        # Now the allow_long, allow_short logic:
        # allow_long = (tsf_4h / hlc3_4h) > 1.01
        # allow_short = (tsf_4h / hlc3_4h) < 0.99
        dataframe['allow_long'] = dataframe['tsf_4h'] / dataframe['hlc3_4h'] > 1.01
        dataframe['allow_short'] = dataframe['tsf_4h'] / dataframe['hlc3_4h'] < 0.99
        return dataframe

    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """
        Populate buy (long) and sell (short) signals.
        Because Freqtrade "buy" = open a long position in spot mode,
        and "sell" = close a long. To handle short, we must do a
        separate approach or rely on custom signals (or "entry_tag" approach).
        However, with "can_short = True", we can define "buy" for short if we set
        a signal = is_short: True. We'll do that below in populate_sell_trend or custom logic.
        """
        # --- LONG ENTRY LOGIC (mirror Pine Script) ---
        # Condition: enterLong = (long_entry_1 OR long_entry_2 OR long_entry_3 OR long_entry_4)
        # For readability, define a few aliases:
        df = dataframe
        # Shorter references to parameters:
        a1 = self.entry_long_adx1.value
        e1 = self.entry_long_emarsi1.value
        a2 = self.entry_long_adx2.value
        e2 = self.entry_long_emarsi2.value
        a3 = self.entry_long_adx3.value
        e3 = self.entry_long_emarsi3.value
        a4 = self.entry_long_adx4.value
        e4 = self.entry_long_emarsi4.value
        # Each sub-condition:
        long_entry_1 = df['allow_long'] & (df['slowsma'] > 0) & (df['close'] < df['ema120']) & (df['close'] < df['ema60']) & (df['minus_di'] > df['minus_di_ema25']) & (df['rsi'] >= df['rsi'].shift(1)) & ~df['preparechangetrend'] & ~df['continueup'] & (df['adx'] > a1) & df['bigdown'] & (df['emarsi'] <= e1)
        long_entry_2 = df['allow_long'] & (df['slowsma'] > 0) & (df['close'] < df['ema120']) & (df['close'] < df['ema60']) & (df['minus_di'] > df['minus_di_ema25']) & (df['rsi'] >= df['rsi'].shift(1)) & ~df['preparechangetrend'] & df['continueup'] & (df['adx'] > a2) & df['bigdown'] & (df['emarsi'] <= e2)
        long_entry_3 = df['allow_long'] & (df['slowsma'] > 0) & (df['close'] < df['ema120']) & (df['close'] < df['ema60']) & (df['minus_di'] > df['minus_di_ema25']) & (df['rsi'] >= df['rsi'].shift(1)) & ~df['continueup'] & (df['adx'] > a3) & df['bigup'] & (df['emarsi'] <= e3)
        long_entry_4 = df['allow_long'] & (df['slowsma'] > 0) & (df['close'] < df['ema120']) & (df['close'] < df['ema60']) & (df['minus_di'] > df['minus_di_ema25']) & (df['rsi'] >= df['rsi'].shift(1)) & df['continueup'] & (df['adx'] > a4) & df['bigup'] & (df['emarsi'] <= e4)
        df.loc[long_entry_1 | long_entry_2 | long_entry_3 | long_entry_4, ['enter_long', 'enter_tag']] = (1, 'enter_long')
        return df

    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        """
        This is used for short entries if `can_short = True`.
        In freqtrade, "sell" can close a long or open a short if "is_short" is set to True.
        We'll place the short-entry logic here by setting `df['sell'] = 1` with `is_short = True`.
        That instructs freqtrade to open a short position.
        However, remember that the standard "sell" also closes a long if there's an open long. 
        Freqtrade’s logic is a bit simpler if you use custom signals (entry_tag approach).
        We'll do the simpler approach: We instruct freqtrade to open short positions by
        returning an 'entry_tag' for short. 
        """
        df = dataframe
        # Shorter references:
        a1 = self.entry_short_adx1.value
        e1 = self.entry_short_emarsi1.value
        a2 = self.entry_short_adx2.value
        e2 = self.entry_short_emarsi2.value
        a3 = self.entry_short_adx3.value
        e3 = self.entry_short_emarsi3.value
        a4 = self.entry_short_adx4.value
        e4 = self.entry_short_emarsi4.value
        short_entry_1 = df['allow_short'] & (df['slowsma'] > 0) & (df['close'] < df['ema120']) & (df['close'] < df['ema60']) & (df['minus_di'] > df['minus_di_ema25']) & (df['rsi'] >= df['rsi'].shift(1)) & ~df['preparechangetrend'] & ~df['continueup'] & (df['adx'] > a1) & df['bigdown'] & (df['emarsi'] <= e1)
        short_entry_2 = df['allow_short'] & (df['slowsma'] > 0) & (df['close'] < df['ema120']) & (df['close'] < df['ema60']) & (df['minus_di'] > df['minus_di_ema25']) & (df['rsi'] >= df['rsi'].shift(1)) & ~df['preparechangetrend'] & df['continueup'] & (df['adx'] > a2) & df['bigdown'] & (df['emarsi'] <= e2)
        short_entry_3 = df['allow_short'] & (df['slowsma'] > 0) & (df['close'] < df['ema120']) & (df['close'] < df['ema60']) & (df['minus_di'] > df['minus_di_ema25']) & (df['rsi'] >= df['rsi'].shift(1)) & ~df['continueup'] & (df['adx'] > a3) & df['bigup'] & (df['emarsi'] <= e3)
        short_entry_4 = df['allow_short'] & (df['slowsma'] > 0) & (df['close'] < df['ema120']) & (df['close'] < df['ema60']) & (df['minus_di'] > df['minus_di_ema25']) & (df['rsi'] >= df['rsi'].shift(1)) & df['continueup'] & (df['adx'] > a4) & df['bigup'] & (df['emarsi'] <= e4)
        # Combine short signals:
        short_signal = short_entry_1 | short_entry_2 | short_entry_3 | short_entry_4
        # In freqtrade, to open short from "sell" side, we do:
        df.loc[short_signal, ['exit_long', 'sell_tag', 'is_short']] = (1, 'enter_short', True)
        # Otherwise, do nothing:
        return df
    ######################################################
    # Custom Stoploss to replicate the dynamic stops
    ######################################################

    def custom_stoploss(self, pair: str, trade: Trade, current_time: pd.Timestamp, current_rate: float, current_profit: float, **kwargs) -> float:
        """
        Replicates the dynamic stoploss logic from the Pine Script:
          - pHSL
          - pPF_1 / pSL_1
          - pPF_2 / pSL_2
          - linear interpolation if current_profit is between pPF_1 and pPF_2
          - For short, everything is reversed in terms of direction
        Returns the stoploss (e.g. -0.10 for 10% stop).
        If we want no stop (or very large) in certain conditions, return 1 to disable or a big negative.
        """
        # Distinguish if this trade is short or long:
        is_short = trade.is_short
        if not is_short:
            # Long position
            hsl = self.pHSL_long.value  # Hard stop
            pf_1 = self.pPF_1_long.value
            sl_1 = self.pSL_1_long.value
            pf_2 = self.pPF_2_long.value
            sl_2 = self.pSL_2_long.value
            if current_profit < pf_1:
                return abs(hsl)
            elif current_profit > pf_2:
                # stop = sl_2 + (current_profit - pf_2)
                return sl_2 + (current_profit - pf_2)
            else:
                # linear interpolation
                pct = (current_profit - pf_1) / (pf_2 - pf_1)
                return sl_1 + pct * (sl_2 - sl_1)
        else:
            # Short position
            hsl = self.pHSL_short.value
            pf_1 = self.pPF_1_short.value
            sl_1 = self.pSL_1_short.value
            pf_2 = self.pPF_2_short.value
            sl_2 = self.pSL_2_short.value
            if current_profit < pf_1:
                return abs(hsl)
            elif current_profit > pf_2:
                return sl_2 + (current_profit - pf_2)
            else:
                pct = (current_profit - pf_1) / (pf_2 - pf_1)
                return sl_1 + pct * (sl_2 - sl_1)
    ######################################################
    # Custom Exit to replicate all “exit_*” conditions
    ######################################################

    def custom_exit(self, pair: str, trade: Trade, current_time: pd.Timestamp, current_rate: float, current_profit: float, **kwargs) -> Optional[str]:
        """
        Checks the additional exit conditions from the Pine Script:
        exit_long_1/2/3/4/5 and exit_short_1/2/3/4/5.  
        
        Return a non-empty string (exit signal name) to close the position.
        Return None to keep the position open.
        """
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe is None or dataframe.empty:
            return None
        # We need the last row that matches current_time exactly (or the nearest index)
        # Because of possible candle alignment issues, let's do a safer approach:
        # find the candle in 'dataframe' that is <= current_time
        row = dataframe.loc[dataframe['date'] == current_time]
        if row.empty:
            # If no exact match, find last index less than current_time
            row = dataframe.loc[dataframe['date'] < current_time]
            if row.empty:
                return None
            row = row.iloc[-1]  # last available
        else:
            row = row.iloc[-1]
        is_short = trade.is_short
        # Gather booleans from strategy
        preparechangetrendconfirm = bool(row['preparechangetrendconfirm'])
        continueup = bool(row['continueup'])
        slowingdown = bool(row['slowingdown'])
        bigdown = bool(row['bigdown'])
        bigup = bool(row['bigup'])
        # (minusdi < plusdi)
        minus_di = row['minus_di']
        plus_di = row['plus_di']
        # Also read the parameter toggles
        el1 = self.exit_long_1.value
        el2 = self.exit_long_2.value
        el3 = self.exit_long_3.value
        el4 = self.exit_long_4.value
        el5 = self.exit_long_5.value
        es1 = self.exit_short_1.value
        es2 = self.exit_short_2.value
        es3 = self.exit_short_3.value
        es4 = self.exit_short_4.value
        es5 = self.exit_short_5.value
        # Additional columns we need for conditions:
        close = row['close']
        ema60 = row['ema60']
        ema120 = row['ema120']
        slowsma = row['slowsma']
        adxVal = row['adx']
        emarsi = row['emarsi']
        # -----------
        # Multi-Exit logic for LONG
        # -----------
        if not is_short:
            # "longPos" exit conditions:
            exit_cond_long_1 = el1 and (not preparechangetrendconfirm) and (not continueup) and (close > ema60 or close > ema120) and (ema120 > 0) and bigdown
            exit_cond_long_2 = el2 and (not preparechangetrendconfirm) and (not continueup) and (close > ema120) and (ema120 > 0) and (emarsi > self.exit_long_emarsi1.value or close > slowsma) and bigdown
            exit_cond_long_3 = el3 and (not preparechangetrendconfirm) and (close > ema120) and (ema120 > 0) and (adxVal > self.exit_long_adx2.value) and (emarsi >= self.exit_long_emarsi2.value) and bigup
            exit_cond_long_4 = el4 and preparechangetrendconfirm and (not continueup) and slowingdown and (emarsi >= self.exit_long_emarsi3.value) and (slowsma > 0)
            exit_cond_long_5 = el5 and preparechangetrendconfirm and (minus_di < plus_di) and (close > ema60) and (slowsma > 0)
            exit_long = exit_cond_long_1 or exit_cond_long_2 or exit_cond_long_3 or exit_cond_long_4 or exit_cond_long_5
            if exit_long:
                return 'CustomExitLong'
        else:
            # -----------
            # Multi-Exit logic for SHORT
            # -----------
            exit_cond_short_1 = es1 and (not preparechangetrendconfirm) and (not continueup) and (close > ema60 or close > ema120) and (ema120 > 0) and bigdown
            exit_cond_short_2 = es2 and (not preparechangetrendconfirm) and (not continueup) and (close > ema120) and (ema120 > 0) and (emarsi > self.exit_short_emarsi1.value or close > slowsma) and bigdown
            exit_cond_short_3 = es3 and (not preparechangetrendconfirm) and (close > ema120) and (ema120 > 0) and (adxVal > self.exit_short_adx2.value) and (emarsi >= self.exit_short_emarsi2.value) and bigup
            exit_cond_short_4 = es4 and preparechangetrendconfirm and (not continueup) and slowingdown and (emarsi >= self.exit_short_emarsi3.value) and (slowsma > 0)
            exit_cond_short_5 = es5 and preparechangetrendconfirm and (minus_di < plus_di) and (close > ema60) and (slowsma > 0)
            exit_short = exit_cond_short_1 or exit_cond_short_2 or exit_cond_short_3 or exit_cond_short_4 or exit_cond_short_5
            if exit_short:
                return 'CustomExitShort'
        # If no exit triggered:
        return None