""" strategy/rr_signals.py ─────────────────────── RR-v1 — Range Mean Reversion Logic: 1. 1H regime filter : ADX(14) < 18 → market is ranging (not trending) 2. 15m entry signals : Bollinger Bands(20,2) + RSI(14) Long : close < lower BB AND RSI < 30 AND next candle bullish/wick rejection Short: close > upper BB AND RSI > 70 AND next candle bearish confirmation 3. Stop loss : max(recent swing, 1.2 × ATR) 4. Take profits: TP1 = BB midline (SMA20), TP2 = opposite BB Extra columns: signal : 1 | -1 | 0 signal_type : 'rr_long' | 'rr_short' | '' sl_price : stop loss tp1, tp2 : take profit levels """ import numpy as np import pandas as pd # ─── Parameters ────────────────────────────────────────────────────────────── ADX_RANGE_MAX = 18 # ADX must be below this for ranging regime RSI_OVERSOLD = 30 # RSI threshold for longs RSI_OVERBOUGHT = 70 # RSI threshold for shorts ATR_SL_MULT = 1.2 # SL = max(swing, 1.2 × ATR) SWING_LOOKBACK = 5 # candles to look back for swing SL # ─── Main Signal Generator ──────────────────────────────────────────────────── def generate_rr_signals(df: pd.DataFrame) -> pd.DataFrame: """ Scan every 15m candle for RR-v1 mean reversion entries. Expects df to have: - Standard OHLCV + rsi, atr, adx - bb_lower, bb_upper, bb_mid (Bollinger Bands) - htf_adx: ADX from 1H timeframe for regime filter """ df = df.copy() df['signal'] = 0 df['signal_type'] = '' df['sl_price'] = np.nan df['tp1'] = np.nan df['tp2'] = np.nan required = ['rsi', 'atr', 'bb_lower', 'bb_upper', 'bb_mid', 'htf_adx'] sig_col = df.columns.get_loc('signal') stype_col = df.columns.get_loc('signal_type') sl_col = df.columns.get_loc('sl_price') tp1_col = df.columns.get_loc('tp1') tp2_col = df.columns.get_loc('tp2') for i in range(SWING_LOOKBACK + 1, len(df) - 1): row = df.iloc[i] next_row = df.iloc[i + 1] # confirmation candle if row[required].isna().any(): continue # ── Regime filter: only trade when 1H ADX < 18 (ranging) ────────── if row['htf_adx'] >= ADX_RANGE_MAX: continue # ── Long setup ───────────────────────────────────────────────────── if ( row['close'] < row['bb_lower'] and row['rsi'] < RSI_OVERSOLD ): # Confirmation: next candle bullish OR shows wick rejection bullish_confirm = next_row['close'] > next_row['open'] wick_rejection = (next_row['close'] - next_row['low']) > ( 0.6 * (next_row['high'] - next_row['low'])) if bullish_confirm or wick_rejection: entry = next_row['close'] # SL = max(recent swing low, 1.2x ATR below entry) swing_low = df['low'].iloc[i - SWING_LOOKBACK:i + 1].min() sl = min(swing_low * 0.999, entry - ATR_SL_MULT * row['atr']) risk = entry - sl if risk > 0: df.iloc[i + 1, sig_col] = 1 df.iloc[i + 1, stype_col] = 'rr_long' df.iloc[i + 1, sl_col] = sl df.iloc[i + 1, tp1_col] = row['bb_mid'] # BB midline df.iloc[i + 1, tp2_col] = row['bb_upper'] # opposite BB # ── Short setup ──────────────────────────────────────────────────── elif ( row['close'] > row['bb_upper'] and row['rsi'] > RSI_OVERBOUGHT ): bearish_confirm = next_row['close'] < next_row['open'] wick_rejection = (next_row['high'] - next_row['close']) > ( 0.6 * (next_row['high'] - next_row['low'])) if bearish_confirm or wick_rejection: entry = next_row['close'] swing_high = df['high'].iloc[i - SWING_LOOKBACK:i + 1].max() sl = max(swing_high * 1.001, entry + ATR_SL_MULT * row['atr']) risk = sl - entry if risk > 0: df.iloc[i + 1, sig_col] = -1 df.iloc[i + 1, stype_col] = 'rr_short' df.iloc[i + 1, sl_col] = sl df.iloc[i + 1, tp1_col] = row['bb_mid'] df.iloc[i + 1, tp2_col] = row['bb_lower'] return df