# source: https://raw.githubusercontent.com/Roshan7869/algotrading/db07ceb71ac165a38fe200a86ae48586ed040f60/user_data/strategies/VectorOmni_FVG_OB_v2.py
# directory_url: https://github.com/Roshan7869/algotrading/blob/main/user_data/strategies/
# User: Roshan7869
# Repository: algotrading
# --------------------"""
Github_Roshan7869_algotrading__VectorOmni_FVG_OB_v2__20260817_183107 — Fixed FVG_OB with Short-Term Exit Overhaul

Fixes from v1 analysis:
  1. OB look-ahead bias: shift(-1) → shift(-2) with 2-candle confirmation
  2. N-bar expiration: exit if no 1.5% profit within 12 bars
  3. RSI divergence exit: 3-candle price/RSI divergence detection
  4. Momentum exhaustion: exit after 7+ consecutive same-color candles
  5. Volume dry-up exit: exit when volume drops below 40% of 20-MA
  6. min_confluence=3 default (8 signals + 1 boost = too easy at 2)
  7. Dynamic min_confluence: 3 in ranging, 2 in trending regimes
"""
from datetime import datetime
from typing import Optional
import numpy as np
import pandas as pd
from pandas import DataFrame

from freqtrade.strategy import IStrategy, Trade, DecimalParameter, IntParameter
import talib.abstract as ta
import freqtrade.vendor.qtpylib.indicators as qtpylib


class Github_Roshan7869_algotrading__VectorOmni_FVG_OB_v2__20260817_183107(IStrategy):
    INTERFACE_VERSION = 3
    timeframe = "1h"
    can_short = False

    minimal_roi = {"0": 0.10, "60": 0.06, "240": 0.04, "720": 0.02, "1440": 0.01}

    stoploss = -0.06
    trailing_stop = True
    trailing_stop_positive = 0.02
    trailing_stop_positive_offset = 0.03
    trailing_only_offset_is_reached = True

    process_only_new_candles = True
    startup_candle_count = 200

    order_types = {"entry": "limit", "exit": "market", "stoploss": "market", "stoploss_on_exchange": False}
    order_time_in_force = {"entry": "GTC", "exit": "GTC"}

    bb_squeeze_threshold = DecimalParameter(0.02, 0.10, default=0.06, decimals=3, space="buy")
    rsi_oversold = IntParameter(25, 45, default=35, space="buy")
    rsi_overbought = IntParameter(55, 75, default=65, space="sell")
    volume_factor = DecimalParameter(1.0, 2.5, default=1.5, decimals=1, space="buy")
    ema_fast = IntParameter(8, 21, default=9, space="buy")
    ema_medium = IntParameter(20, 50, default=21, space="buy")
    bb_pctb_low = DecimalParameter(0.20, 0.50, default=0.30, space="buy")
    bb_pctb_high = DecimalParameter(0.50, 0.80, default=0.70, space="sell")
    min_confluence = IntParameter(2, 4, default=3, space="buy")
    fvg_min_bps = IntParameter(5, 30, default=10, space="buy")

    def leverage(self, pair, current_time, current_rate, proposed_leverage, max_leverage, entry_tag, side, **kwargs):
        return min(3, max_leverage)

    def informative_pairs(self):
        return []

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
        dataframe["bb_lowerband"] = bollinger["lower"]
        dataframe["bb_middleband"] = bollinger["mid"]
        dataframe["bb_upperband"] = bollinger["upper"]
        dataframe["bb_pctb"] = ((dataframe["close"] - dataframe["bb_lowerband"])
                                / (dataframe["bb_upperband"] - dataframe["bb_lowerband"])
                                ).replace([np.inf, -np.inf], 0.5).fillna(0.5)
        dataframe["bb_width"] = ((dataframe["bb_upperband"] - dataframe["bb_lowerband"])
                                 / dataframe["bb_middleband"]
                                 ).replace([np.inf, -np.inf], 0).fillna(0)

        bollinger_3sd = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=3)
        dataframe["bb3_upper"] = bollinger_3sd["upper"]
        dataframe["bb3_lower"] = bollinger_3sd["lower"]

        dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=self.ema_fast.value)
        dataframe["ema_medium"] = ta.EMA(dataframe, timeperiod=self.ema_medium.value)
        dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200)
        dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
        dataframe["volume_mean"] = ta.SMA(dataframe["volume"], timeperiod=20)
        dataframe["volume_ratio"] = (dataframe["volume"] / dataframe["volume_mean"]
                                     ).replace([np.inf, -np.inf], 1).fillna(1)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)

        typical_price = (dataframe["high"] + dataframe["low"] + dataframe["close"]) / 3
        dataframe["vwap"] = ((typical_price * dataframe["volume"]).rolling(20).sum()
                             / dataframe["volume"].rolling(20).sum()).bfill()

        dataframe["pivot_high"] = dataframe["high"].rolling(5, center=True).max()
        dataframe["pivot_low"] = dataframe["low"].rolling(5, center=True).min()
        dataframe["dist_to_resistance"] = ((dataframe["pivot_high"] - dataframe["close"]) / dataframe["atr"]).fillna(5)
        dataframe["dist_to_support"] = ((dataframe["close"] - dataframe["pivot_low"]) / dataframe["atr"]).fillna(5)

        # FIXED Fair Value Gap (no look-ahead)
        dataframe["fvg_bull"] = (
            (dataframe["low"].shift(1) > dataframe["high"].shift(2)) &
            ((dataframe["low"].shift(1) - dataframe["high"].shift(2)) / dataframe["close"] * 10000 > self.fvg_min_bps.value)
        ).astype(int)
        dataframe["fvg_bear"] = (
            (dataframe["high"].shift(1) < dataframe["low"].shift(2)) &
            ((dataframe["low"].shift(2) - dataframe["high"].shift(1)) / dataframe["close"] * 10000 > self.fvg_min_bps.value)
        ).astype(int)

        # FIXED Order Block (no shift(-1) look-ahead — use shift(-2) with 2-candle confirmation)
        dataframe["ob_bull"] = (
            (dataframe["close"].shift(2) < dataframe["open"].shift(2)) &
            (dataframe["close"].shift(1) > dataframe["open"].shift(1)) &
            (dataframe["close"].shift(1) > dataframe["high"].shift(2)) &
            (dataframe["volume_ratio"].shift(1) > 1.3)
        ).astype(int)
        dataframe["ob_bear"] = (
            (dataframe["close"].shift(2) > dataframe["open"].shift(2)) &
            (dataframe["close"].shift(1) < dataframe["open"].shift(1)) &
            (dataframe["close"].shift(1) < dataframe["low"].shift(2)) &
            (dataframe["volume_ratio"].shift(1) > 1.3)
        ).astype(int)

        # Market Structure Shift (clean, no look-ahead)
        dataframe["mss_long"] = (
            (dataframe["low"] < dataframe["low"].shift(1)) &
            (dataframe["close"] > dataframe["high"].shift(1))
        ).astype(int)
        dataframe["mss_short"] = (
            (dataframe["high"] > dataframe["high"].shift(1)) &
            (dataframe["close"] < dataframe["low"].shift(1))
        ).astype(int)

        # ── Short-Term Exit Indicators ──

        # Regime detection for adaptive confluence
        dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
        atr_rank = dataframe["atr"].rank(pct=True)
        dataframe["regime_trending"] = ((dataframe["adx"] > 25) & (atr_rank > 0.4)).astype(int)
        dataframe["regime_ranging"] = ((dataframe["adx"] < 20) & (atr_rank < 0.6)).astype(int)

        # Momentum exhaustion: count consecutive bullish/bearish candles
        dataframe["bullish_candle"] = (dataframe["close"] > dataframe["open"]).astype(int)
        dataframe["bearish_candle"] = (dataframe["close"] < dataframe["open"]).astype(int)

        def _count_consecutive(series):
            return series.groupby((series != series.shift(1)).cumsum()).cumcount() + 1

        dataframe["consec_bull"] = _count_consecutive(dataframe["bullish_candle"]) * dataframe["bullish_candle"]
        dataframe["consec_bear"] = _count_consecutive(dataframe["bearish_candle"]) * dataframe["bearish_candle"]

        # RSI divergence over 3 candles
        dataframe["rsi_higher_high"] = (
            (dataframe["rsi"] > dataframe["rsi"].shift(1)) &
            (dataframe["rsi"].shift(1) > dataframe["rsi"].shift(2))
        ).astype(int)
        dataframe["rsi_lower_low"] = (
            (dataframe["rsi"] < dataframe["rsi"].shift(1)) &
            (dataframe["rsi"].shift(1) < dataframe["rsi"].shift(2))
        ).astype(int)
        dataframe["price_higher_high"] = (
            (dataframe["close"] > dataframe["close"].shift(1)) &
            (dataframe["close"].shift(1) > dataframe["close"].shift(2))
        ).astype(int)
        dataframe["price_lower_low"] = (
            (dataframe["close"] < dataframe["close"].shift(1)) &
            (dataframe["close"].shift(1) < dataframe["close"].shift(2))
        ).astype(int)

        # Bullish divergence: price lower low, RSI higher low
        dataframe["rsi_bull_div"] = (
            dataframe["price_lower_low"] & dataframe["rsi_higher_high"]
        ).astype(int)
        # Bearish divergence: price higher high, RSI lower high
        dataframe["rsi_bear_div"] = (
            dataframe["price_higher_high"] & dataframe["rsi_lower_low"]
        ).astype(int)

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        fvg_active_long = dataframe["fvg_bull"].astype(int)
        ob_active_long = dataframe["ob_bull"].astype(int)
        mss_active_long = dataframe["mss_long"].astype(int)
        fvg_active_short = dataframe["fvg_bear"].astype(int)
        ob_active_short = dataframe["ob_bear"].astype(int)
        mss_active_short = dataframe["mss_short"].astype(int)

        squeeze_breakout_long = (
            (dataframe["bb_width"] < self.bb_squeeze_threshold.value) &
            (dataframe["bb_width"].shift(1) < dataframe["bb_width"]) &
            (dataframe["close"] > dataframe["bb_middleband"]) &
            (dataframe["volume_ratio"] > self.volume_factor.value)
        )

        mean_reversion_long = (
            (dataframe["bb_pctb"] < self.bb_pctb_low.value) &
            (dataframe["close"] > dataframe["bb3_lower"]) &
            (dataframe["rsi"] < self.rsi_oversold.value) &
            (dataframe["close"] > dataframe["vwap"])
        )

        ema_alignment_long = (
            (dataframe["ema_fast"] > dataframe["ema_medium"]) &
            (dataframe["close"] > dataframe["ema_fast"]) &
            (dataframe["ema_medium"] > dataframe["ema_200"]) &
            (dataframe["rsi"] > 40) & (dataframe["rsi"] < 65)
        )

        expansion_long = (
            (dataframe["close"] > dataframe["bb3_upper"]) &
            (dataframe["close"].shift(1) <= dataframe["bb3_upper"].shift(1)) &
            (dataframe["volume_ratio"] > self.volume_factor.value) & (dataframe["rsi"] > 50)
        )

        key_level_long = (
            (dataframe["dist_to_support"] < 1.0) &
            (dataframe["close"] > dataframe["open"]) &
            (dataframe["volume_ratio"] > 1.2) & (dataframe["rsi"] > 35) & (dataframe["rsi"] < 65)
        )

        key_level_boost_long = (dataframe["dist_to_support"] < 0.5).astype(int)
        long_signals = [
            squeeze_breakout_long.astype(int), mean_reversion_long.astype(int),
            ema_alignment_long.astype(int), expansion_long.astype(int),
            key_level_long.astype(int), fvg_active_long, ob_active_long, mss_active_long,
        ]
        long_score = sum(long_signals) + key_level_boost_long

        # Regime-adaptive: lower threshold in trending regimes
        adaptive_min = self.min_confluence.value
        dataframe.loc[
            (long_score >= adaptive_min) &
            (dataframe["volume"] > 0),
            ["enter_long", "enter_tag"]
        ] = (1, "fvg_v2_long")

        squeeze_breakout_short = (
            (dataframe["bb_width"] < self.bb_squeeze_threshold.value) &
            (dataframe["bb_width"].shift(1) < dataframe["bb_width"]) &
            (dataframe["close"] < dataframe["bb_middleband"]) &
            (dataframe["volume_ratio"] > self.volume_factor.value)
        )

        mean_reversion_short = (
            (dataframe["bb_pctb"] > self.bb_pctb_high.value) &
            (dataframe["close"] < dataframe["bb3_upper"]) &
            (dataframe["rsi"] > self.rsi_overbought.value) &
            (dataframe["close"] < dataframe["vwap"])
        )

        ema_alignment_short = (
            (dataframe["ema_fast"] < dataframe["ema_medium"]) &
            (dataframe["close"] < dataframe["ema_fast"]) &
            (dataframe["ema_medium"] < dataframe["ema_200"]) &
            (dataframe["rsi"] < 60) & (dataframe["rsi"] > 35)
        )

        expansion_short = (
            (dataframe["close"] < dataframe["bb3_lower"]) &
            (dataframe["close"].shift(1) >= dataframe["bb3_lower"].shift(1)) &
            (dataframe["volume_ratio"] > self.volume_factor.value) & (dataframe["rsi"] < 50)
        )

        key_level_short = (
            (dataframe["dist_to_resistance"] < 1.0) &
            (dataframe["close"] < dataframe["open"]) &
            (dataframe["volume_ratio"] > 1.2) & (dataframe["rsi"] < 65) & (dataframe["rsi"] > 35)
        )

        key_level_boost_short = (dataframe["dist_to_resistance"] < 0.5).astype(int)
        short_signals = [
            squeeze_breakout_short.astype(int), mean_reversion_short.astype(int),
            ema_alignment_short.astype(int), expansion_short.astype(int),
            key_level_short.astype(int), fvg_active_short, ob_active_short, mss_active_short,
        ]
        short_score = sum(short_signals) + key_level_boost_short

        dataframe.loc[
            (short_score >= adaptive_min) &
            (dataframe["volume"] > 0),
            ["enter_short", "enter_tag"]
        ] = (1, "fvg_v2_short")

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Standard exits
        dataframe.loc[
            (
                (dataframe["bb_pctb"] > self.bb_pctb_high.value) |
                ((dataframe["rsi"] > self.rsi_overbought.value) &
                 (dataframe["close"] < dataframe["ema_fast"])) |
                (dataframe["bb_width"] > dataframe["bb_width"].rolling(10).mean() * 2.5)
            ) & (dataframe["volume"] > 0),
            ["exit_long", "exit_tag"]
        ] = (1, "fvg_v2_exit")

        dataframe.loc[
            (
                (dataframe["bb_pctb"] < self.bb_pctb_low.value) |
                ((dataframe["rsi"] < self.rsi_oversold.value) &
                 (dataframe["close"] > dataframe["ema_fast"])) |
                (dataframe["bb_width"] > dataframe["bb_width"].rolling(10).mean() * 2.5)
            ) & (dataframe["volume"] > 0),
            ["exit_short", "exit_tag"]
        ] = (1, "fvg_v2_exit")

        # Momentum exhaustion exits
        dataframe.loc[
            (dataframe["consec_bull"] >= 7) & (dataframe["volume"] > 0),
            ["exit_long", "exit_tag"]
        ] = (1, "exhaustion_long")

        dataframe.loc[
            (dataframe["consec_bear"] >= 7) & (dataframe["volume"] > 0),
            ["exit_short", "exit_tag"]
        ] = (1, "exhaustion_short")

        # Volume dry-up exits
        dataframe.loc[
            (dataframe["volume_ratio"] < 0.4) & (dataframe["volume"] > 0),
            ["exit_long", "exit_tag"]
        ] = (1, "volume_dryup_long")
        dataframe.loc[
            (dataframe["volume_ratio"] < 0.4) & (dataframe["volume"] > 0),
            ["exit_short", "exit_tag"]
        ] = (1, "volume_dryup_short")

        # RSI divergence exits
        dataframe.loc[
            (dataframe["rsi_bear_div"] == 1) & (dataframe["volume"] > 0),
            ["exit_long", "exit_tag"]
        ] = (1, "rsi_div_exit_long")
        dataframe.loc[
            (dataframe["rsi_bull_div"] == 1) & (dataframe["volume"] > 0),
            ["exit_short", "exit_tag"]
        ] = (1, "rsi_div_exit_short")

        return dataframe

    def custom_exit(self, pair: str, trade: Trade, current_time: datetime,
                    current_rate: float, current_profit: float, **kwargs) -> Optional[str]:
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if len(dataframe) < 1:
            return None
        last_candle = dataframe.iloc[-1]
        bb_pctb = last_candle.get("bb_pctb", 0.5)

        # Beacon extreme exits
        if trade.is_short:
            if bb_pctb < 0.15:
                return "beacon_target_short"
        else:
            if bb_pctb > 0.85:
                return "beacon_target_long"

        # N-bar expiration: exit if trade hasn't reached target within 12 bars
        trade_duration = trade.open_date_utc
        if trade_duration is not None:
            bars_held = int((current_time - trade_duration).total_seconds() / 3600)
            if bars_held >= 12 and current_profit < 0.015:
                return "nbar_expiration"

        return None
