# source: https://raw.githubusercontent.com/DOUGLASGUEDESATRIA/OSIRIS_TRADE/fe3c09567f3798d1cad6d7a306fa4f6131d91e17/user_data/strategies/OsirisHyperScalp_v1.py
"""
Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisHyperScalp_v1__20260329_060843 — High-Frequency Precision Scalper
====================================================
TARGET: 10+ trades/day | 70%+ WR | 2:1 R:R

MECHANISM (Multi-TF Reversal Scalp):
  Higher TF (5m) defines TREND DIRECTION only.
  Entry TF (1m) catches MICRO-REVERSALS within that trend.

  The insight: In a confirmed uptrend, 1m dips almost always bounce.
  We only trade WITH the trend, catching pullbacks at high-probability
  reversal zones (Bollinger lower band + RSI extreme + volume spike).

  LONG SETUP (5m uptrend):
    1. 5m EMA fast > slow (trend confirmed)
    2. 1m RSI drops to extreme oversold
    3. 1m price touches/pierces lower Bollinger Band
    4. 1m reversal candle (green after red, or hammer)
    5. Volume spike confirms institutional interest

  SHORT SETUP (5m downtrend):
    1. 5m EMA fast < slow
    2. 1m RSI spikes to extreme overbought
    3. 1m price touches/pierces upper Bollinger Band
    4. 1m reversal candle (red after green)
    5. Volume spike
    
  EXITS:
    TP = 2 × SL (hard 2:1 R:R)
    SL = ATR-based (adaptive to volatility)  
    Timeout = max hold minutes
    Breakeven move after 1× SL profit
"""

import logging
from datetime import datetime
from functools import reduce

import numpy as np
import pandas as pd
from pandas import DataFrame

from freqtrade.persistence import Trade
from freqtrade.strategy import (
    IStrategy,
    DecimalParameter,
    IntParameter,
    merge_informative_pair,
)
import talib.abstract as ta

logger = logging.getLogger(__name__)


class Github_DOUGLASGUEDESATRIA_OSIRIS_TRADE__OsirisHyperScalp_v1__20260329_060843(IStrategy):
    INTERFACE_VERSION = 3
    can_short = True
    timeframe = "1m"

    stoploss = -0.05  # safety net
    minimal_roi = {"0": 100}  # disabled — exits via custom logic
    trailing_stop = False
    use_custom_stoploss = True
    startup_candle_count = 300
    process_only_new_candles = True

    # ═══════════════════════════════════════════════════════════════════
    # HYPEROPT PARAMETERS
    # ═══════════════════════════════════════════════════════════════════

    # ── Risk Management ──────────────────────────────────────────────
    # SL as multiplier of ATR (adaptive to volatility)
    sl_atr_mult = DecimalParameter(
        0.5, 3.0, default=1.5, decimals=1, space="sell", optimize=True
    )
    # TP multiplier of SL (R:R ratio)
    tp_rr = DecimalParameter(
        1.5, 3.0, default=2.0, decimals=1, space="sell", optimize=True
    )
    # Max SL cap in % (prevent huge stops in volatile periods)
    sl_max_pct = DecimalParameter(
        0.3, 1.5, default=0.8, decimals=1, space="sell", optimize=True
    )
    # Breakeven trigger (0 = disabled, 1.0 = move SL to entry at 1×SL profit)
    be_trigger = DecimalParameter(
        0.0, 1.5, default=1.0, decimals=1, space="sell", optimize=True
    )

    # ── 5m Trend Filter ──────────────────────────────────────────────
    trend_ema_fast = IntParameter(5, 20, default=8, space="buy", optimize=True)
    trend_ema_slow = IntParameter(15, 50, default=21, space="buy", optimize=True)
    # Trend strength: minimum EMA separation in %
    trend_min_sep = DecimalParameter(
        0.0, 0.5, default=0.05, decimals=2, space="buy", optimize=True
    )

    # ── 1m RSI Extremes ──────────────────────────────────────────────
    rsi_period = IntParameter(4, 14, default=6, space="buy", optimize=True)
    rsi_oversold = IntParameter(10, 35, default=20, space="buy", optimize=True)
    rsi_overbought = IntParameter(65, 90, default=80, space="buy", optimize=True)

    # ── Bollinger Bands ──────────────────────────────────────────────
    bb_period = IntParameter(10, 30, default=20, space="buy", optimize=True)
    bb_std = DecimalParameter(1.5, 3.0, default=2.0, decimals=1, space="buy", optimize=True)

    # ── Volume Confirmation ──────────────────────────────────────────
    vol_period = IntParameter(10, 40, default=20, space="buy", optimize=True)
    vol_mult = DecimalParameter(0.5, 3.0, default=1.2, decimals=1, space="buy", optimize=True)

    # ── Candle Pattern Strength ──────────────────────────────────────
    # 0 = any reversal candle, 1 = require wick ratio confirmation
    require_wick = IntParameter(0, 1, default=1, space="buy", optimize=True)

    # ── Time Management ──────────────────────────────────────────────
    max_hold_min = IntParameter(5, 60, default=30, space="sell", optimize=True)
    cooldown_min = IntParameter(1, 15, default=3, space="buy", optimize=True)
    
    # ── Session Filter (UTC hours) ───────────────────────────────────
    # Only trade during high-volume sessions
    session_start = IntParameter(0, 12, default=6, space="buy", optimize=True)
    session_end = IntParameter(12, 24, default=22, space="buy", optimize=True)

    _last_entry_time: dict = {}

    # ═══════════════════════════════════════════════════════════════════
    # INFORMATIVE PAIRS (5m for trend)
    # ═══════════════════════════════════════════════════════════════════
    def informative_pairs(self):
        pairs = self.dp.current_whitelist() if self.dp else []
        return [(p, "5m") for p in pairs]

    # ═══════════════════════════════════════════════════════════════════
    # INDICATORS
    # ═══════════════════════════════════════════════════════════════════
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # ── 1m Indicators ────────────────────────────────────────────
        # RSI (multiple periods for hyperopt)
        for p in range(4, 15):
            dataframe[f"rsi_{p}"] = ta.RSI(dataframe, timeperiod=p)

        # Bollinger Bands (multiple configs for hyperopt)
        for period in [10, 14, 20, 25, 30]:
            for std in [1.5, 2.0, 2.5, 3.0]:
                bb = ta.BBANDS(
                    dataframe, timeperiod=period, nbdevup=std, nbdevdn=std
                )
                k = f"bb_{period}_{str(std).replace('.','')}"
                dataframe[f"{k}_upper"] = bb["upperband"]
                dataframe[f"{k}_lower"] = bb["lowerband"]
                dataframe[f"{k}_mid"] = bb["middleband"]
                dataframe[f"{k}_width"] = (
                    (bb["upperband"] - bb["lowerband"]) / bb["middleband"]
                )

        # ATR for adaptive stops
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"] * 100

        # Volume SMA (multiple periods)
        for p in [10, 20, 30, 40]:
            dataframe[f"vol_sma_{p}"] = ta.SMA(dataframe["volume"], timeperiod=p)

        # Candle patterns
        dataframe["is_green"] = (dataframe["close"] > dataframe["open"]).astype(int)
        dataframe["is_red"] = (dataframe["close"] < dataframe["open"]).astype(int)
        dataframe["prev_green"] = dataframe["is_green"].shift(1)
        dataframe["prev_red"] = dataframe["is_red"].shift(1)

        # Body and wick ratios
        body = abs(dataframe["close"] - dataframe["open"])
        candle_range = dataframe["high"] - dataframe["low"]
        candle_range = candle_range.replace(0, np.nan)
        dataframe["body_ratio"] = body / candle_range

        # Lower wick ratio (for bullish reversal = hammer)
        dataframe["lower_wick"] = (
            dataframe[["close", "open"]].min(axis=1) - dataframe["low"]
        ) / candle_range
        # Upper wick ratio (for bearish reversal = shooting star)
        dataframe["upper_wick"] = (
            dataframe["high"] - dataframe[["close", "open"]].max(axis=1)
        ) / candle_range

        # Hour of day for session filter
        dataframe["hour"] = pd.to_datetime(dataframe["date"]).dt.hour

        # ── 5m Trend Indicators (merge from informative) ────────────
        inf_5m = self.dp.get_pair_dataframe(
            pair=metadata["pair"], timeframe="5m"
        )
        if not inf_5m.empty:
            # Compute all possible EMA periods on 5m
            for p in range(5, 51):
                inf_5m[f"ema5m_{p}"] = ta.EMA(inf_5m, timeperiod=p)
            inf_5m["rsi5m"] = ta.RSI(inf_5m, timeperiod=14)

            # Which columns to merge
            cols_to_merge = ["date"] + [f"ema5m_{p}" for p in range(5, 51)] + ["rsi5m"]
            m5 = merge_informative_pair(
                dataframe,
                inf_5m[cols_to_merge],
                self.timeframe,
                "5m",
                ffill=True,
            )
            for p in range(5, 51):
                dataframe[f"ema5m_{p}"] = m5[f"ema5m_{p}_5m"].values
            dataframe["rsi5m"] = m5["rsi5m_5m"].values
        else:
            for p in range(5, 51):
                dataframe[f"ema5m_{p}"] = ta.EMA(dataframe, timeperiod=p * 5)
            dataframe["rsi5m"] = ta.RSI(dataframe, timeperiod=70)

        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # ENTRY SIGNALS
    # ═══════════════════════════════════════════════════════════════════
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Resolve hyperopt params
        ef = int(self.trend_ema_fast.value)
        es = int(self.trend_ema_slow.value)
        rp = int(self.rsi_period.value)
        bp = int(self.bb_period.value)
        bs = float(self.bb_std.value)
        vp = int(self.vol_period.value)
        vm = float(self.vol_mult.value)
        rw = int(self.require_wick.value)
        sep = float(self.trend_min_sep.value) / 100.0
        ss = int(self.session_start.value)
        se = int(self.session_end.value)

        # Map BB std to key
        bs_key = str(bs).replace(".", "")
        bb_key = f"bb_{bp}_{bs_key}"

        # Ensure BB columns exist (pick closest available)
        available_periods = [10, 14, 20, 25, 30]
        available_stds = [1.5, 2.0, 2.5, 3.0]
        bp_actual = min(available_periods, key=lambda x: abs(x - bp))
        bs_actual = min(available_stds, key=lambda x: abs(x - bs))
        bb_key = f"bb_{bp_actual}_{str(bs_actual).replace('.', '')}"

        rsi = dataframe[f"rsi_{rp}"]
        rsi_prev = dataframe[f"rsi_{rp}"].shift(1)
        bb_lower = dataframe[f"{bb_key}_lower"]
        bb_upper = dataframe[f"{bb_key}_upper"]
        vol_sma_key = f"vol_sma_{min([10,20,30,40], key=lambda x: abs(x-vp))}"

        ema_fast_5m = dataframe[f"ema5m_{ef}"]
        ema_slow_5m = dataframe[f"ema5m_{es}"]

        # ── Common Filters ───────────────────────────────────────────
        has_data = rsi.notna() & dataframe["rsi5m"].notna() & (dataframe["volume"] > 0)
        vol_ok = dataframe["volume"] > (dataframe[vol_sma_key] * vm)
        
        # Session filter
        if ss < se:
            session_ok = (dataframe["hour"] >= ss) & (dataframe["hour"] < se)
        else:
            session_ok = (dataframe["hour"] >= ss) | (dataframe["hour"] < se)

        # ── 5m TREND ─────────────────────────────────────────────────
        trend_bull = (
            (ema_fast_5m > ema_slow_5m)
            & ((ema_fast_5m - ema_slow_5m) / ema_slow_5m > sep)
        )
        trend_bear = (
            (ema_fast_5m < ema_slow_5m)
            & ((ema_slow_5m - ema_fast_5m) / ema_slow_5m > sep)
        )

        # ── 1m REVERSAL SIGNALS ──────────────────────────────────────
        # RSI crossback from extreme (was extreme → recovering)
        rsi_bounce_up = (rsi_prev < self.rsi_oversold.value) & (rsi >= self.rsi_oversold.value)
        rsi_bounce_down = (rsi_prev > self.rsi_overbought.value) & (rsi <= self.rsi_overbought.value)

        # Price at/below BB lower (for long) or at/above BB upper (for short)
        bb_long = dataframe["low"] <= bb_lower
        bb_short = dataframe["high"] >= bb_upper

        # Reversal candle
        green_reversal = (dataframe["is_green"] == 1) & (dataframe["prev_red"] == 1)
        red_reversal = (dataframe["is_red"] == 1) & (dataframe["prev_green"] == 1)

        # Wick confirmation (optional)
        if rw:
            wick_long = dataframe["lower_wick"] > 0.3  # decent lower wick
            wick_short = dataframe["upper_wick"] > 0.3
        else:
            wick_long = True
            wick_short = True

        # ── LONG ENTRY ───────────────────────────────────────────────
        long_cond = (
            has_data
            & session_ok
            & vol_ok
            & trend_bull          # 5m uptrend confirmed
            & bb_long             # price touched lower BB
            & rsi_bounce_up       # RSI recovering from oversold
            & green_reversal      # reversal candle
            & wick_long           # wick confirmation
        )

        # ── SHORT ENTRY ──────────────────────────────────────────────
        short_cond = (
            has_data
            & session_ok
            & vol_ok
            & trend_bear          # 5m downtrend confirmed
            & bb_short            # price touched upper BB
            & rsi_bounce_down     # RSI falling from overbought
            & red_reversal        # reversal candle
            & wick_short          # wick confirmation
        )

        dataframe.loc[long_cond, "enter_long"] = 1
        dataframe.loc[short_cond, "enter_short"] = 1
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        return dataframe

    # ═══════════════════════════════════════════════════════════════════
    # TRADE MANAGEMENT
    # ═══════════════════════════════════════════════════════════════════
    def confirm_trade_entry(
        self, pair, order_type, amount, rate, time_in_force,
        current_time, entry_tag, side, **kwargs,
    ) -> bool:
        cd = int(self.cooldown_min.value)
        if cd > 0:
            last = self._last_entry_time.get(pair)
            if last is not None:
                diff = (current_time - last).total_seconds()
                if diff < cd * 60:
                    return False
        self._last_entry_time[pair] = current_time
        return True

    def custom_stoploss(
        self, pair, trade: Trade, current_time,
        current_rate: float, current_profit: float,
        after_fill: bool, **kwargs,
    ) -> float:
        # ATR-based adaptive stop
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe.empty:
            return -1

        last = dataframe.iloc[-1]
        atr_pct = last.get("atr_pct", 0.5)

        sl = atr_pct * float(self.sl_atr_mult.value) / 100.0
        max_sl = float(self.sl_max_pct.value) / 100.0
        sl = min(sl, max_sl)

        # Breakeven logic
        be = float(self.be_trigger.value)
        if be > 0 and current_profit >= sl * be:
            return -0.001  # break-even (tiny cushion)

        return -sl

    def custom_exit(
        self, pair, trade: Trade, current_time, current_rate,
        current_profit, **kwargs,
    ):
        # Get ATR for TP calculation
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe.empty:
            return None

        last = dataframe.iloc[-1]
        atr_pct = last.get("atr_pct", 0.5)
        sl = min(
            atr_pct * float(self.sl_atr_mult.value) / 100.0,
            float(self.sl_max_pct.value) / 100.0,
        )
        tp = sl * float(self.tp_rr.value)

        # TP hit
        if current_profit >= tp:
            return "hyperscalp_tp"

        # Timeout
        if trade.open_date_utc:
            elapsed = (current_time - trade.open_date_utc).total_seconds() / 60
            if elapsed >= int(self.max_hold_min.value):
                return "hyperscalp_timeout"

        return None

    def leverage(
        self, pair, current_time, current_rate, proposed_leverage,
        max_leverage, entry_tag, side, **kwargs,
    ) -> float:
        return 1.0
