💬 Forum

BollingerBandsStrategyV9

🏆 League #1922 / 1922

jtscwhitney/freqtrade-trading-bot/user_data/strategies/BollingerBandsStrategyV9.py · ★1 · first seen 2026-07-16 · repo updated 2026-04-16 · ⬇ 1 download

Basics mode: spot timeframe: 15m interface version: 3
Settings stoploss: -0.1 has minimal roi trailing custom stoploss process only new candles startup candle count: 500 hyperopt hyperopt params: 8
Indicators ATR Bollinger_Bands EMA MFI talib
Concepts mean_reversion trailing
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
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
# ---
# @version 5
# @strategy BollingerBandsStrategyV9
# @description Bollinger Bands strategy with EMA, MFI, and ATR indicators
# @author Converted from TypeScript
# @tags bollinger, ema, mfi, atr, kraken
# ---

import logging
from datetime import datetime
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
from freqtrade.persistence import Trade
from pandas import DataFrame
import pandas as pd
import numpy as np
import talib.abstract as ta
from typing import Dict, List, Optional, Tuple
from freqtrade.exchange import timeframe_to_minutes

logger = logging.getLogger(__name__)


class BollingerBandsStrategyV9(IStrategy):
    """
    Bollinger Bands Strategy V9
    
    This strategy uses:
    - Exponential Moving Average (EMA) for trend direction
    - Bollinger Bands for price levels
    - Money Flow Index (MFI) for overbought/oversold conditions
    - Average True Range (ATR) for dynamic stop loss management
    
    Initialization:
    - Requires 500 candles for proper indicator initialization
    - All indicators are calculated using the 500 candle warm-up period
    - Ensures stable and accurate indicator values before trading begins
    
    Entry Logic:
    - Sell: Price closes above upper BB, BB upper < EMA, candle high < EMA, MFI > 60
    - Buy: Price closes below lower BB, BB lower > EMA, candle low > EMA, MFI < 40
    
    Exit Logic:
    - Dynamic stop loss based on ATR and Bollinger Bands middle
    - Trailing stop loss that adjusts based on market conditions
    """
    
    # Strategy interface version
    INTERFACE_VERSION = 3
    
    # Timeframe
    timeframe = '15m'
    
    # Minimal ROI table - disabled in favor of custom exit logic
    minimal_roi = {
        "0": 100  # Effectively disabled
    }
    
    # Stop loss - will be overridden by custom logic
    stoploss = -0.10
    
    # Use custom stoploss
    use_custom_stoploss = True
    
    # Trailing stop
    trailing_stop = False
    trailing_stop_positive = 0.01
    trailing_stop_positive_offset = 0.02
    trailing_only_offset_is_reached = False
    
    # Order types
    order_types = {
        'entry': 'limit',
        'exit': 'limit',
        'emergency_exit': 'market',
        'force_entry': 'market',
        'force_exit': 'market',
        'stoploss': 'market',
        'stoploss_on_exchange': False,
        'stoploss_on_exchange_interval': 60,
    }
    
    # Order time in force
    order_time_in_force = {
        'entry': 'gtc',
        'exit': 'gtc',
    }
    
    # Process only new candles
    process_only_new_candles = True
    
    # Number of candles the strategy requires before producing valid signals
    # Set to 500 to ensure proper initialization of all indicators
    startup_candle_count: int = 500
    
    # Strategy parameters (optimized for 500 candle initialization)
    # All parameters are designed to work within the 500 candle startup period
    ema_length = IntParameter(400, 600, default=500, space="buy")
    bbands_length = IntParameter(40, 60, default=50, space="buy")
    bbands_stdev = DecimalParameter(1.5, 2.5, default=2.0, space="buy")
    mfi_length = IntParameter(10, 20, default=14, space="buy")
    atr_length = IntParameter(10, 20, default=14, space="buy")
    risk_factor = DecimalParameter(1.0, 2.0, default=1.5, space="buy")
    overbought_min_mfi = IntParameter(55, 70, default=60, space="buy")
    oversold_max_mfi = IntParameter(30, 45, default=40, space="buy")
    
    def __init__(self, config: dict) -> None:
        super().__init__(config)
        
        # State management (equivalent to TypeScript state)
        self.potential_order_side: Optional[str] = None
        self.is_potential_order_signaled: bool = False
        self.last_processed_candle: Optional[DataFrame] = None
        
        # Rolling window tracking for verification
        self.last_calculation_timestamp: Optional[datetime] = None
        self.calculation_count: int = 0
        
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Add technical indicators to the dataframe
        Initialize with 500 candles to ensure all indicators are properly calculated
        Each new candle triggers recalculation of all indicators using rolling 500-candle window
        """
        # Track calculation frequency and timing for verification
        self.calculation_count += 1
        current_time = datetime.now()
        
        # Log the current dataframe length for verification
        logger.debug(f"Processing {len(dataframe)} candles for {metadata['pair']} - Rolling window calculation #{self.calculation_count}")
        
        # Log time since last calculation to verify rolling behavior
        if self.last_calculation_timestamp:
            time_diff = (current_time - self.last_calculation_timestamp).total_seconds()
            logger.debug(f"Time since last calculation: {time_diff:.2f} seconds")
        
        self.last_calculation_timestamp = current_time
        
        # Check if we have enough data for proper initialization
        if len(dataframe) < self.startup_candle_count:
            logger.warning(f"Insufficient data for {metadata['pair']}: {len(dataframe)} candles, need {self.startup_candle_count}")
            # Return dataframe with NaN indicators if insufficient data
            dataframe['ema'] = np.nan
            dataframe['bb_lowerband'] = np.nan
            dataframe['bb_middleband'] = np.nan
            dataframe['bb_upperband'] = np.nan
            dataframe['mfi'] = np.nan
            dataframe['atr'] = np.nan
            dataframe['atr_lower'] = np.nan
            dataframe['atr_upper'] = np.nan
            dataframe['price_above_ema'] = False
            dataframe['price_below_ema'] = False
            dataframe['high_above_ema'] = False
            dataframe['low_below_ema'] = False
            dataframe['bb_upper_vs_ema'] = False
            dataframe['bb_lower_vs_ema'] = False
            return dataframe
        
        # Ensure we're using a rolling window of exactly 500 candles for calculation
        # Freqtrade with process_only_new_candles=True still passes the full dataframe
        # but we want to ensure our indicators use the most recent 500 candles
        rolling_window_size = min(len(dataframe), self.startup_candle_count)
        logger.debug(f"Using rolling window of {rolling_window_size} candles for indicator calculation")
        
        # Use the most recent 500 candles for indicator calculation (rolling window)
        # This ensures indicators are recalculated based on the latest 500 candles each time
        recent_data = dataframe.tail(rolling_window_size).copy()
        
        # Exponential Moving Average - calculated on rolling window
        dataframe['ema'] = ta.EMA(dataframe['close'], timeperiod=self.ema_length.value)
        
        # Bollinger Bands - calculated on rolling window
        bollinger = ta.BBANDS(
            dataframe['close'], 
            timeperiod=self.bbands_length.value,
            nbdevup=self.bbands_stdev.value,
            nbdevdn=self.bbands_stdev.value,
            matype=0
        )
        dataframe['bb_lowerband'] = bollinger[0]  # lowerband
        dataframe['bb_middleband'] = bollinger[1]  # middleband
        dataframe['bb_upperband'] = bollinger[2]  # upperband
        
        # Money Flow Index - calculated on rolling window
        dataframe['mfi'] = ta.MFI(
            dataframe['high'],
            dataframe['low'],
            dataframe['close'],
            dataframe['volume'],
            timeperiod=self.mfi_length.value
        )
        
        # Average True Range - calculated on rolling window
        dataframe['atr'] = ta.ATR(
            dataframe['high'],
            dataframe['low'],
            dataframe['close'],
            timeperiod=self.atr_length.value
        )
        
        # ATR-based stop loss levels
        dataframe['atr_lower'] = dataframe['close'] - (dataframe['atr'] * self.risk_factor.value)
        dataframe['atr_upper'] = dataframe['close'] + (dataframe['atr'] * self.risk_factor.value)
        
        # Additional indicators for state management
        dataframe['price_above_ema'] = dataframe['close'] > dataframe['ema']
        dataframe['price_below_ema'] = dataframe['close'] < dataframe['ema']
        dataframe['high_above_ema'] = dataframe['high'] > dataframe['ema']
        dataframe['low_below_ema'] = dataframe['low'] < dataframe['ema']
        
        # Bollinger Bands relative to EMA
        dataframe['bb_upper_vs_ema'] = dataframe['bb_upperband'] < dataframe['ema']
        dataframe['bb_lower_vs_ema'] = dataframe['bb_lowerband'] > dataframe['ema']
        
        # Validate that indicators are properly initialized
        # Check if the last few candles have valid indicator values
        required_indicators = ['ema', 'bb_lowerband', 'bb_middleband', 'bb_upperband', 'mfi', 'atr']
        last_valid_idx = None
        
        for i in range(len(dataframe) - 1, max(0, len(dataframe) - 10), -1):
            if all(pd.notna(dataframe.iloc[i][col]) for col in required_indicators):
                last_valid_idx = i
                break
        
        if last_valid_idx is None:
            logger.warning(f"No valid indicator data found for {metadata['pair']} after 500 candle initialization")
        else:
            # Log rolling window verification
            logger.debug(f"Rolling window calculation verified for {metadata['pair']}:")
            logger.debug(f"  - Total candles processed: {len(dataframe)}")
            logger.debug(f"  - Rolling window size: {rolling_window_size}")
            logger.debug(f"  - Last valid indicator data at index: {last_valid_idx}")
            logger.debug(f"  - Indicators recalculated using most recent {rolling_window_size} candles")
            
            # Log current indicator values for verification
            if last_valid_idx is not None:
                last_candle = dataframe.iloc[last_valid_idx]
                logger.debug(f"  - Current EMA: {last_candle['ema']:.4f}")
                logger.debug(f"  - Current BB Upper: {last_candle['bb_upperband']:.4f}")
                logger.debug(f"  - Current BB Lower: {last_candle['bb_lowerband']:.4f}")
                logger.debug(f"  - Current MFI: {last_candle['mfi']:.2f}")
                logger.debug(f"  - Current ATR: {last_candle['atr']:.4f}")
        
        return dataframe
    
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Define entry signal for the given dataframe
        """
        # Initialize entry signals
        dataframe['enter_long'] = False
        dataframe['enter_short'] = False
        dataframe['enter_tag'] = ''
        
        # Log detailed information for each candle to help debug trading decisions
        for i in range(len(dataframe)):
            if i < len(dataframe) - 1:  # Skip the last candle as it might not have complete data
                continue
                
            current_candle = dataframe.iloc[i]
            
            # Skip if we don't have valid indicator data
            if not all(pd.notna(current_candle[col]) for col in ['ema', 'bb_upperband', 'bb_lowerband', 'mfi', 'atr']):
                continue
            
            # Log current candle data
            logger.info(f"=== CANDLE ANALYSIS FOR {metadata['pair']} ===")
            logger.info(f"Timestamp: {current_candle.name}")
            logger.info(f"OHLC: O={current_candle['open']:.4f}, H={current_candle['high']:.4f}, L={current_candle['low']:.4f}, C={current_candle['close']:.4f}")
            logger.info(f"Volume: {current_candle['volume']:.2f}")
            
            # Log indicator values
            logger.info(f"INDICATORS:")
            logger.info(f"  EMA({self.ema_length.value}): {current_candle['ema']:.4f}")
            logger.info(f"  BB Upper: {current_candle['bb_upperband']:.4f}")
            logger.info(f"  BB Middle: {current_candle['bb_middleband']:.4f}")
            logger.info(f"  BB Lower: {current_candle['bb_lowerband']:.4f}")
            logger.info(f"  MFI({self.mfi_length.value}): {current_candle['mfi']:.2f}")
            logger.info(f"  ATR({self.atr_length.value}): {current_candle['atr']:.4f}")
            
            # Log condition checks for SELL signal
            logger.info(f"SELL SIGNAL CONDITIONS:")
            bb_upper_vs_ema = current_candle['bb_upper_vs_ema']
            high_above_ema = current_candle['high_above_ema']
            close_above_bb_upper = current_candle['close'] > current_candle['bb_upperband']
            mfi_overbought = current_candle['mfi'] > self.overbought_min_mfi.value
            
            logger.info(f"  BB Upper < EMA: {bb_upper_vs_ema} (BB Upper: {current_candle['bb_upperband']:.4f} < EMA: {current_candle['ema']:.4f})")
            logger.info(f"  Candle High < EMA: {not high_above_ema} (High: {current_candle['high']:.4f} < EMA: {current_candle['ema']:.4f})")
            logger.info(f"  Close > BB Upper: {close_above_bb_upper} (Close: {current_candle['close']:.4f} > BB Upper: {current_candle['bb_upperband']:.4f})")
            logger.info(f"  MFI > {self.overbought_min_mfi.value}: {mfi_overbought} (MFI: {current_candle['mfi']:.2f})")
            
            sell_signal = bb_upper_vs_ema and not high_above_ema and close_above_bb_upper and mfi_overbought
            logger.info(f"  SELL SIGNAL RESULT: {sell_signal}")
            
            # Log condition checks for BUY signal
            logger.info(f"BUY SIGNAL CONDITIONS:")
            bb_lower_vs_ema = current_candle['bb_lower_vs_ema']
            low_below_ema = current_candle['low_below_ema']
            close_below_bb_lower = current_candle['close'] < current_candle['bb_lowerband']
            mfi_oversold = current_candle['mfi'] < self.oversold_max_mfi.value
            
            logger.info(f"  BB Lower > EMA: {bb_lower_vs_ema} (BB Lower: {current_candle['bb_lowerband']:.4f} > EMA: {current_candle['ema']:.4f})")
            logger.info(f"  Candle Low > EMA: {not low_below_ema} (Low: {current_candle['low']:.4f} > EMA: {current_candle['ema']:.4f})")
            logger.info(f"  Close < BB Lower: {close_below_bb_lower} (Close: {current_candle['close']:.4f} < BB Lower: {current_candle['bb_lowerband']:.4f})")
            logger.info(f"  MFI < {self.oversold_max_mfi.value}: {mfi_oversold} (MFI: {current_candle['mfi']:.2f})")
            
            buy_signal = bb_lower_vs_ema and not low_below_ema and close_below_bb_lower and mfi_oversold
            logger.info(f"  BUY SIGNAL RESULT: {buy_signal}")
            
            logger.info(f"=== END CANDLE ANALYSIS ===\n")
        
        # Sell signal conditions (from TypeScript logic)
        sell_conditions = (
            # BB upper is below EMA
            dataframe['bb_upper_vs_ema'] &
            # Candle high is below EMA
            ~dataframe['high_above_ema'] &
            # Price closes above BB upper
            (dataframe['close'] > dataframe['bb_upperband']) &
            # MFI is overbought
            (dataframe['mfi'] > self.overbought_min_mfi.value)
        )
        
        # Buy signal conditions (from TypeScript logic)
        buy_conditions = (
            # BB lower is above EMA
            dataframe['bb_lower_vs_ema'] &
            # Candle low is above EMA
            ~dataframe['low_below_ema'] &
            # Price closes below BB lower
            (dataframe['close'] < dataframe['bb_lowerband']) &
            # MFI is oversold
            (dataframe['mfi'] < self.oversold_max_mfi.value)
        )
        
        # Apply conditions
        dataframe.loc[sell_conditions, 'enter_short'] = True
        dataframe.loc[sell_conditions, 'enter_tag'] = 'bb_sell'
        
        dataframe.loc[buy_conditions, 'enter_long'] = True
        dataframe.loc[buy_conditions, 'enter_tag'] = 'bb_buy'
        
        # Log signal summary for the last candle
        if len(dataframe) > 0:
            last_candle = dataframe.iloc[-1]
            has_buy_signal = last_candle.get('enter_long', False)
            has_sell_signal = last_candle.get('enter_short', False)
            
            if not has_buy_signal and not has_sell_signal:
                logger.info(f"NO SIGNALS GENERATED for {metadata['pair']} - All conditions not met")
            elif has_buy_signal:
                logger.info(f"BUY SIGNAL GENERATED for {metadata['pair']} - Tag: {last_candle.get('enter_tag', 'unknown')}")
            elif has_sell_signal:
                logger.info(f"SELL SIGNAL GENERATED for {metadata['pair']} - Tag: {last_candle.get('enter_tag', 'unknown')}")
        
        return dataframe
    
    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Define exit signal for the given dataframe
        This will be handled by custom stop loss logic
        """
        # Disable standard exit signals - we use custom stop loss
        dataframe['exit_long'] = False
        dataframe['exit_short'] = False
        dataframe['exit_tag'] = ''
        
        return dataframe
    
    def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, 
                       current_rate: float, current_profit: float, **kwargs) -> float:
        """
        Custom stop loss logic based on ATR and Bollinger Bands
        """
        # Get current dataframe
        df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if df.empty:
            logger.warning(f"No dataframe available for {pair} in custom_stoploss")
            return self.stoploss
            
        # Get the last candle
        last_candle = df.iloc[-1].squeeze()
        
        # Check if we have all required indicators
        if not all(pd.notna(last_candle[col]) for col in ['atr_lower', 'atr_upper', 'bb_middleband']):
            logger.warning(f"Missing indicator data for {pair} in custom_stoploss")
            return self.stoploss
        
        # Log detailed stop loss calculation
        logger.info(f"=== STOP LOSS CALCULATION FOR {pair} ===")
        logger.info(f"Trade ID: {trade.id}")
        logger.info(f"Trade Side: {'SHORT' if trade.is_short else 'LONG'}")
        logger.info(f"Entry Price: {trade.open_rate:.4f}")
        logger.info(f"Current Rate: {current_rate:.4f}")
        logger.info(f"Current Profit: {current_profit:.4f} ({current_profit*100:.2f}%)")
        logger.info(f"Current Stop Loss: {trade.stop_loss:.4f}")
        
        logger.info(f"INDICATOR VALUES:")
        logger.info(f"  ATR Lower: {last_candle['atr_lower']:.4f}")
        logger.info(f"  ATR Upper: {last_candle['atr_upper']:.4f}")
        logger.info(f"  BB Middle: {last_candle['bb_middleband']:.4f}")
        logger.info(f"  ATR: {last_candle['atr']:.4f}")
        logger.info(f"  Risk Factor: {self.risk_factor.value}")
        
        # Calculate dynamic stop loss based on trade side
        if trade.is_short:
            logger.info(f"SHORT POSITION STOP LOSS LOGIC:")
            # For short positions, use ATR upper or BB middle, whichever is lower
            if last_candle['atr_upper'] <= last_candle['bb_middleband']:
                logger.info(f"  ATR Upper ({last_candle['atr_upper']:.4f}) <= BB Middle ({last_candle['bb_middleband']:.4f})")
                if last_candle['atr_upper'] <= trade.stop_loss:
                    new_stop_loss = last_candle['atr_upper']
                    logger.info(f"  Using ATR Upper as new stop loss: {new_stop_loss:.4f}")
                else:
                    new_stop_loss = trade.stop_loss
                    logger.info(f"  Keeping current stop loss: {new_stop_loss:.4f}")
            else:
                logger.info(f"  BB Middle ({last_candle['bb_middleband']:.4f}) < ATR Upper ({last_candle['atr_upper']:.4f})")
                if last_candle['bb_middleband'] <= trade.stop_loss:
                    new_stop_loss = last_candle['bb_middleband']
                    logger.info(f"  Using BB Middle as new stop loss: {new_stop_loss:.4f}")
                else:
                    new_stop_loss = trade.stop_loss
                    logger.info(f"  Keeping current stop loss: {new_stop_loss:.4f}")
        else:
            logger.info(f"LONG POSITION STOP LOSS LOGIC:")
            # For long positions, use ATR lower or BB middle, whichever is higher
            if last_candle['atr_lower'] >= last_candle['bb_middleband']:
                logger.info(f"  ATR Lower ({last_candle['atr_lower']:.4f}) >= BB Middle ({last_candle['bb_middleband']:.4f})")
                if last_candle['atr_lower'] >= trade.stop_loss:
                    new_stop_loss = last_candle['atr_lower']
                    logger.info(f"  Using ATR Lower as new stop loss: {new_stop_loss:.4f}")
                else:
                    new_stop_loss = trade.stop_loss
                    logger.info(f"  Keeping current stop loss: {new_stop_loss:.4f}")
            else:
                logger.info(f"  BB Middle ({last_candle['bb_middleband']:.4f}) > ATR Lower ({last_candle['atr_lower']:.4f})")
                if last_candle['bb_middleband'] >= trade.stop_loss:
                    new_stop_loss = last_candle['bb_middleband']
                    logger.info(f"  Using BB Middle as new stop loss: {new_stop_loss:.4f}")
                else:
                    new_stop_loss = trade.stop_loss
                    logger.info(f"  Keeping current stop loss: {new_stop_loss:.4f}")
        
        # Convert to relative stop loss
        if trade.is_short:
            stop_loss_pct = (new_stop_loss - current_rate) / current_rate
        else:
            stop_loss_pct = (new_stop_loss - current_rate) / current_rate
        
        # Ensure stop loss is not too tight
        original_stop_loss_pct = stop_loss_pct
        stop_loss_pct = max(stop_loss_pct, -0.05)  # Minimum 5% stop loss
        
        logger.info(f"STOP LOSS CALCULATION:")
        logger.info(f"  New Stop Loss Price: {new_stop_loss:.4f}")
        logger.info(f"  Original Stop Loss %: {original_stop_loss_pct:.4f} ({original_stop_loss_pct*100:.2f}%)")
        logger.info(f"  Final Stop Loss %: {stop_loss_pct:.4f} ({stop_loss_pct*100:.2f}%)")
        logger.info(f"  Stop Loss Adjusted: {'Yes' if original_stop_loss_pct != stop_loss_pct else 'No'}")
        logger.info(f"=== END STOP LOSS CALCULATION ===\n")
        
        return stop_loss_pct
    
    def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
                   current_profit: float, **kwargs) -> Optional[Tuple[str, str]]:
        """
        Custom exit logic - check if price has hit stop loss level
        """
        # Get current dataframe
        df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if df.empty:
            return None
            
        last_candle = df.iloc[-1].squeeze()
        
        # Check if we have required indicators
        if not all(pd.notna(last_candle[col]) for col in ['atr_lower', 'atr_upper', 'bb_middleband']):
            return None
        
        # Check exit conditions based on trade side
        if trade.is_short:
            # For short positions, exit if price goes above stop loss
            if current_rate >= trade.stop_loss:
                return 'custom_exit', 'stop_loss_hit'
        else:
            # For long positions, exit if price goes below stop loss
            if current_rate <= trade.stop_loss:
                return 'custom_exit', 'stop_loss_hit'
        
        return None
    
    def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
                           time_in_force: str, current_time: datetime, entry_tag: str,
                           side: str, **kwargs) -> bool:
        """
        Additional confirmation before trade entry
        """
        # Get current dataframe
        df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if df.empty:
            logger.warning(f"No dataframe available for {pair} in confirm_trade_entry")
            return False
            
        last_candle = df.iloc[-1].squeeze()
        
        # Additional validation
        if not all(pd.notna(last_candle[col]) for col in ['ema', 'bb_upperband', 'bb_lowerband', 'mfi', 'atr']):
            logger.warning(f"Missing indicators for {pair}, skipping trade")
            return False
        
        # Log detailed trade entry confirmation
        logger.info(f"=== TRADE ENTRY CONFIRMATION FOR {pair} ===")
        logger.info(f"Side: {side}")
        logger.info(f"Entry Price: {rate:.4f}")
        logger.info(f"Amount: {amount:.8f}")
        logger.info(f"Order Type: {order_type}")
        logger.info(f"Entry Tag: {entry_tag}")
        logger.info(f"Time: {current_time}")
        
        logger.info(f"ENTRY CONDITIONS AT TIME OF TRADE:")
        logger.info(f"  Close Price: {last_candle['close']:.4f}")
        logger.info(f"  EMA: {last_candle['ema']:.4f}")
        logger.info(f"  BB Upper: {last_candle['bb_upperband']:.4f}")
        logger.info(f"  BB Middle: {last_candle['bb_middleband']:.4f}")
        logger.info(f"  BB Lower: {last_candle['bb_lowerband']:.4f}")
        logger.info(f"  MFI: {last_candle['mfi']:.2f}")
        logger.info(f"  ATR: {last_candle['atr']:.4f}")
        
        if side == 'long':
            logger.info(f"  BB Lower > EMA: {last_candle['bb_lower_vs_ema']}")
            logger.info(f"  Candle Low > EMA: {not last_candle['low_below_ema']}")
            logger.info(f"  Close < BB Lower: {last_candle['close'] < last_candle['bb_lowerband']}")
            logger.info(f"  MFI < {self.oversold_max_mfi.value}: {last_candle['mfi'] < self.oversold_max_mfi.value}")
        else:
            logger.info(f"  BB Upper < EMA: {last_candle['bb_upper_vs_ema']}")
            logger.info(f"  Candle High < EMA: {not last_candle['high_above_ema']}")
            logger.info(f"  Close > BB Upper: {last_candle['close'] > last_candle['bb_upperband']}")
            logger.info(f"  MFI > {self.overbought_min_mfi.value}: {last_candle['mfi'] > self.overbought_min_mfi.value}")
        
        logger.info(f"=== TRADE ENTRY CONFIRMED ===\n")
        
        return True
    
    def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
                          rate: float, time_in_force: str, exit_reason: str,
                          current_time: datetime, **kwargs) -> bool:
        """
        Additional confirmation before trade exit
        """
        # Get current dataframe for exit analysis
        df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        last_candle = df.iloc[-1].squeeze() if not df.empty else None
        
        # Log detailed trade exit confirmation
        logger.info(f"=== TRADE EXIT CONFIRMATION FOR {pair} ===")
        logger.info(f"Trade ID: {trade.id}")
        logger.info(f"Trade Side: {'SHORT' if trade.is_short else 'LONG'}")
        logger.info(f"Entry Price: {trade.open_rate:.4f}")
        logger.info(f"Exit Price: {rate:.4f}")
        logger.info(f"Amount: {amount:.8f}")
        logger.info(f"Exit Reason: {exit_reason}")
        logger.info(f"Order Type: {order_type}")
        logger.info(f"Time: {current_time}")
        
        # Calculate trade performance
        if trade.is_short:
            profit_pct = (trade.open_rate - rate) / trade.open_rate
        else:
            profit_pct = (rate - trade.open_rate) / trade.open_rate
        
        logger.info(f"TRADE PERFORMANCE:")
        logger.info(f"  Profit/Loss: {profit_pct:.4f} ({profit_pct*100:.2f}%)")
        logger.info(f"  Trade Duration: {current_time - trade.open_date}")
        
        if last_candle is not None:
            logger.info(f"EXIT CONDITIONS AT TIME OF EXIT:")
            logger.info(f"  Close Price: {last_candle['close']:.4f}")
            logger.info(f"  EMA: {last_candle['ema']:.4f}")
            logger.info(f"  BB Upper: {last_candle['bb_upperband']:.4f}")
            logger.info(f"  BB Middle: {last_candle['bb_middleband']:.4f}")
            logger.info(f"  BB Lower: {last_candle['bb_lowerband']:.4f}")
            logger.info(f"  MFI: {last_candle['mfi']:.2f}")
            logger.info(f"  ATR: {last_candle['atr']:.4f}")
            logger.info(f"  Stop Loss: {trade.stop_loss:.4f}")
        
        logger.info(f"=== TRADE EXIT CONFIRMED ===\n")
        
        return True
    
    def custom_entry_price(self, pair: str, current_time: datetime, proposed_rate: float,
                          entry_tag: str, side: str, **kwargs) -> float:
        """
        Custom entry price logic
        """
        # Use current market price for better fill rates
        return proposed_rate
    
    def custom_exit_price(self, pair: str, trade: Trade, current_time: datetime,
                         proposed_rate: float, **kwargs) -> float:
        """
        Custom exit price logic
        """
        # Use current market price for better fill rates
        return proposed_rate
    
    def bot_loop_start(self, **kwargs) -> None:
        """
        Called at the start of the bot iteration
        """
        logger.info(f"BollingerBandsStrategyV9 started - Timeframe: {self.timeframe}")
        logger.info(f"Startup candle count: {self.startup_candle_count} (500 candles for proper initialization)")
        logger.info(f"EMA Length: {self.ema_length.value}")
        logger.info(f"BBands Length: {self.bbands_length.value}, StDev: {self.bbands_stdev.value}")
        logger.info(f"MFI Length: {self.mfi_length.value}")
        logger.info(f"ATR Length: {self.atr_length.value}, Risk Factor: {self.risk_factor.value}")
        logger.info(f"Overbought MFI: {self.overbought_min_mfi.value}, Oversold MFI: {self.oversold_max_mfi.value}")
        logger.info("Strategy will wait for 500 candles before generating trading signals")
        logger.info("ROLLING WINDOW BEHAVIOR:")
        logger.info("  - Each new candle triggers full indicator recalculation")
        logger.info("  - All indicators use the most recent 500 candles (rolling window)")
        logger.info("  - process_only_new_candles=True ensures efficient processing")
        logger.info("  - Indicators are recalculated based on rolling 500-candle data each interval")