# source: https://raw.githubusercontent.com/RoboticAutomations/freqtradestrategies/52c4bd40414645f079a980cf4486dc7af4061efb/dirty%20LA/from-AIRA/orderbook-volume/VVRPStrategyBETA0_1__2.py
# directory_url: https://github.com/RoboticAutomations/freqtradestrategies/blob/main/dirty LA/from-AIRA/orderbook-volume/
# User: RoboticAutomations
# Repository: freqtradestrategies
# --------------------# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort: skip_file
# --- Do not remove these imports ---
# --- Do not remove these libs ---
from freqtrade.strategy.interface import IStrategy
from pandas import DataFrame
import talib.abstract as ta
from freqtrade.strategy.parameters import CategoricalParameter, DecimalParameter, IntParameter
import freqtrade.vendor.qtpylib.indicators as qtpylib
import pandas as pd
from functools import reduce
from datetime import datetime, timedelta
from freqtrade.strategy import merge_informative_pair, Trade
import numpy as np
from freqtrade.strategy import stoploss_from_open
import numexpr as ne
pd.options.mode.chained_assignment = None  # default='warn'
ne.set_num_threads(32)
pd.options.mode.chained_assignment = None  # default='warn'

def _ema(s: pd.Series, n: int) -> pd.Series:
    return s.ewm(span=int(n), adjust=False).mean()


def _rsi(close: pd.Series, n: int = 14) -> pd.Series:
    n = int(n)
    delta = close.diff()
    gain = delta.clip(lower=0).ewm(alpha=1/float(n), adjust=False).mean()
    loss = (-delta.clip(upper=0)).ewm(alpha=1/float(n), adjust=False).mean()
    rs = gain / loss.replace(0, np.nan)
    rsi = 100 - (100 / (1 + rs))
    return rsi.replace([np.inf, -np.inf], np.nan).fillna(50.0).astype(float)


def _atr(df: pd.DataFrame, n: int = 14) -> pd.Series:
    high, low, close = df["high"], df["low"], df["close"]
    prev_close = close.shift(1)
    tr = pd.concat([
        (high - low).abs(),
        (high - prev_close).abs(),
        (low - prev_close).abs()
    ], axis=1).max(axis=1)
    return tr.ewm(alpha=1/float(n), adjust=False).mean().fillna(0).astype(float)


def _rolling_range(df: pd.DataFrame, window: int) -> tuple[pd.Series, pd.Series]:

    window = int(max(2, window))
    range_high = df["high"].rolling(window, min_periods=1).max()
    range_low = df["low"].rolling(window, min_periods=1).min()
    return range_high.astype(float), range_low.astype(float)


def _stepped_range(df: pd.DataFrame, window: int, step_bars: int) -> tuple[pd.Series, pd.Series]:
    window = int(max(2, window))
    step_bars = int(max(1, step_bars))

    # 1. Calculate a standard rolling max/min (trailing)
    # This represents the High/Low of the last 'window' bars at any given point
    rolling_high = df["high"].rolling(window, min_periods=1).max()
    rolling_low = df["low"].rolling(window, min_periods=1).min()

    # 2. Resample/Step this data
    # We want to hold the value constant for 'step_bars'
    # To avoid lookahead, we take the value at the *start* of the step (which is based on trailing data)
    # and hold it for the duration of the step.
    
    # Create a series identifying the step blocks
    # e.g., 0, 0, 0, 1, 1, 1...
    # We can simply sample the rolling data every 'step_bars'
    
    range_high = pd.Series(index=df.index, dtype=float)
    range_low = pd.Series(index=df.index, dtype=float)

    # We iterate by steps to set the values
    # For a block starting at 'i', we use the rolling value from 'i' (which looks back 'window' bars)
    # This is safe because rolling_high[i] only knows 0..i
    for i in range(0, len(df), step_bars):
        val_high = rolling_high.iloc[i]
        val_low = rolling_low.iloc[i]
        
        # Apply this value to the next 'step_bars' (forward fill basically)
        end_idx = min(i + step_bars, len(df))
        range_high.iloc[i:end_idx] = val_high
        range_low.iloc[i:end_idx] = val_low

    return range_high, range_low


def _vwap_with_value_area(
    df: pd.DataFrame,
    window: int,
    va_percent: float = 0.70,
) -> tuple[
    pd.Series, pd.Series, pd.Series, pd.Series
]:

    window = int(max(2, window))
    close = df["close"].astype(float)
    high = df["high"].astype(float)
    low = df["low"].astype(float)
    volume = df["volume"].astype(float)

    tp = (high + low + close) / 3.0

    # VWAP
    pv = tp * volume
    sum_pv = pv.rolling(window, min_periods=1).sum()
    sum_v = volume.rolling(window, min_periods=1).sum().replace(0, np.nan)
    vwap = (sum_pv / sum_v).ffill().bfill()

    # Standard Deviation pentru Value Area
    sum_p2v = ((tp ** 2) * volume).rolling(window, min_periods=1).sum()
    var = (sum_p2v / sum_v - vwap ** 2).clip(lower=0)
    stdev = np.sqrt(var).fillna(0)

    # Value Area aproximată ca VWAP ± factor x stdev
    va_factor = 1.0  # Aproximativ 1 stdev pentru 68%

    vah = (vwap + va_factor * stdev).astype(float)  # Value Area High
    val = (vwap - va_factor * stdev).astype(float)  # Value Area Low

    # POC
    n = len(df)
    poc = np.zeros(n)
    num_bins = 30

    for i in range(n):
        start_idx = max(0, i - window + 1)

        window_tp = tp.iloc[start_idx:i+1].values
        window_vol = volume.iloc[start_idx:i+1].values
        window_high = high.iloc[start_idx:i+1].values
        window_low = low.iloc[start_idx:i+1].values

        if len(window_tp) < 2:
            poc[i] = close.iloc[i]
            continue

        price_min = np.min(window_low)
        price_max = np.max(window_high)

        if price_max <= price_min:
            poc[i] = close.iloc[i]
            continue

        # Bins de pret
        bins = np.linspace(price_min, price_max, num_bins + 1)
        bin_centers = (bins[:-1] + bins[1:]) / 2

        bin_indices = np.digitize(window_tp, bins) - 1
        bin_indices = np.clip(bin_indices, 0, num_bins - 1)

        bin_volumes = np.zeros(num_bins)
        for j, bin_idx in enumerate(bin_indices):
            bin_volumes[bin_idx] += window_vol[j]

        # POC = centrul bin-ului cu cel mai mare volum
        poc_bin_idx = np.argmax(bin_volumes)
        poc[i] = bin_centers[poc_bin_idx]

    poc_series = pd.Series(poc, index=df.index).astype(float)

    return vwap.astype(float), poc_series, vah, val


def _volume_oscillator_normalized(df: pd.DataFrame, fast: int = 5, slow: int = 20) -> pd.Series:
    """
    Volume Oscillator Normalized: (EMA_fast(vol) - EMA_slow(vol)) / EMA_slow(vol) x 100
    Normalizat între -100 si 100
    """
    volume = df["volume"].astype(float)
    ema_fast = _ema(volume, fast)
    ema_slow = _ema(volume, slow).replace(0, np.nan)

    vol_osc = ((ema_fast - ema_slow) / ema_slow * 100).replace([np.inf, -np.inf], np.nan).fillna(0)

    vol_osc_norm = vol_osc.clip(-100, 100).astype(float)

    return vol_osc_norm


def _cumulative_delta_normalized(df: pd.DataFrame, window: int = 20) -> pd.Series:
    """
    Cumulative Delta Normalized: aproximare bazată pe close position în range
    Delta = (close - low) / (high - low) - 0.5, scalat
    Pozitiv = buying pressure
    Negativ = selling pressure
    Normalizat pe fereastra rulanta.
    """
    high = df["high"].astype(float)
    low = df["low"].astype(float)
    close = df["close"].astype(float)
    volume = df["volume"].astype(float)

    # Calculăm poziția close în range sunt calcule grele :)
    range_size = (high - low).replace(0, np.nan)
    close_position = (close - low) / range_size  # 0=low, 1=high

    # Delta per bară: pozitiv dacă close spre high, negativ dacă spre low
    delta_per_bar = (close_position - 0.5) * 2 * volume  # Scalat cu volum

    # Cumulative delta pe fereastră
    cum_delta = delta_per_bar.rolling(window, min_periods=1).sum()

    # Normalizare
    cum_delta_max = cum_delta.rolling(window * 2, min_periods=1).max().abs()
    cum_delta_min = cum_delta.rolling(window * 2, min_periods=1).min().abs()
    normalizer = pd.concat([cum_delta_max, cum_delta_min], axis=1).max(axis=1).replace(0, 1)

    cum_delta_norm = (cum_delta / normalizer * 100).clip(-100, 100).fillna(0).astype(float)

    return cum_delta_norm


class Github_RoboticAutomations_freqtradestrategies__VVRPStrategyBETA0_1__2__20260915_233254(IStrategy):
    """
    Volume Weighted Range

    Logica strategiei este simpla, acum am conceput_o pentru test :)
    - LONG când prețul cade sub range_low suport dinamic
    - SHORT când prețul urcă peste range_high rezistență dinamică
    - Confirmări: RSI, Volume Oscillator, Cumulative Delta
    - Exit la POC (VWAP) sau la range opus
    ~ Daca grupul dorește pot face și o versiune avansată cu multiple moduri de intrare/ieșire
      eventual
      sa posteze versiunea imbunatatită sa vad si eu unde am gresit

    ~ As vrea din suflet sa repostati strategia cu imbunatatirile voastre, chiar ma ajuta sa invat
      din propriile greseli :)

    Notă: Aceasta este o versiune beta inițială pentru testare și optimizare.
    Nu este recomandată pentru utilizare live fără teste și ajustări suplimentare.
    !!!!!!!!!!!!!!!  Salutarii de la DarkReaper :) !!!!!!!!!!!!!!!!!!!!!
    """

    timeframe = "15m" # merge si pe 1h si 30m 4h etc testeaza!!!
    can_short = True
    startup_candle_count = 200

    # Risk Management
    stoploss = -0.28
    trailing_stop = False
    trailing_stop_positive = 0.01
    trailing_stop_positive_offset = 0.02
    trailing_only_offset_is_reached = True

    minimal_roi = {
        "0": 0.99
    }

    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    # Range
    range_window = IntParameter(20, 100, default=48, space="buy", optimize=True)
    range_step = IntParameter(4, 24, default=8, space="buy", optimize=True) #trepte mai frecvente

    #  VWAP
    vwap_window = IntParameter(20, 200, default=48, space="buy", optimize=True) #POC mai reactiv


    rsi_length = IntParameter(7, 21, default=14, space="buy", optimize=True)
    rsi_oversold = IntParameter(20, 45, default=40, space="buy", optimize=True)  # Mai relaxat
    rsi_overbought = IntParameter(55, 80, default=60, space="buy", optimize=True)  # Mai relaxat


    vol_osc_fast = IntParameter(3, 10, default=5, space="buy", optimize=True)
    vol_osc_slow = IntParameter(15, 30, default=20, space="buy", optimize=True)
    vol_osc_threshold = DecimalParameter(-50, 50, default=0, decimals=0, space="buy", optimize=True)


    cum_delta_window = IntParameter(10, 50, default=20, space="buy", optimize=True)
    cum_delta_threshold = DecimalParameter(
        -30,
        30,
        default=0,
        decimals=0,
        space="buy",
        optimize=True,
    )

    # ===== Entry Mode =====
    entry_mode = CategoricalParameter(
        ["range_only", "range_rsi", "range_volume", "range_all"],
        default="range_only",  # Cel mai sensibil mod
        space="buy",
        optimize=True
    )


    exit_at_poc = CategoricalParameter(
        ["on", "off"],
        default="on",
        space="sell",
        optimize=True,
    )
    exit_at_opposite_range = CategoricalParameter(
        ["on", "off"],
        default="off",
        space="sell",
        optimize=True,
    )


    plot_config = {
        'main_plot': {
            'range_high': {'color': '#FF1744', 'linewidth': 2},
            'range_low': {'color': '#00E676', 'linewidth': 2},
            'vwap': {'color': '#2962FF', 'linewidth': 1.5},
            'poc': {'color': '#FF9800', 'linewidth': 1, 'linestyle': '--'},
            'vwap_value_area_high': {'color': '#00BCD4', 'linewidth': 1, 'linestyle': ':'},
            'vwap_value_area_low': {'color': '#00BCD4', 'linewidth': 1, 'linestyle': ':'},
        },
        'subplots': {
            "RSI": {
                'rsi': {'color': '#FF9800', 'linewidth': 1.5},
            },
            "Volume Oscillator": {
                'volume_osc_norm': {'color': '#00BCD4', 'linewidth': 1.5},
            },
            "Cumulative Delta": {
                'cum_delta_norm': {'color': '#2962FF', 'linewidth': 1.5},
            },
            "Volume": {
                'volume': {'color': '#78909C', 'type': 'bar'},
            },
        }
    }

    def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        df = dataframe.copy()


        df["range_high"], df["range_low"] = _stepped_range(
            df,
            self.range_window.value,
            self.range_step.value
        )

        (
            df["vwap"],
            df["poc"],
            df["vwap_value_area_high"],
            df["vwap_value_area_low"],
        ) = _vwap_with_value_area(
            df,
            self.vwap_window.value,
        )


        df["rsi"] = _rsi(df["close"], self.rsi_length.value)


        df["volume_osc_norm"] = _volume_oscillator_normalized(
            df,
            self.vol_osc_fast.value,
            self.vol_osc_slow.value
        )

        df["cum_delta_norm"] = _cumulative_delta_normalized(
            df,
            self.cum_delta_window.value
        )

        df["atr"] = _atr(df, 14)

        df["dist_from_range_high"] = (
            ((df["close"] - df["range_high"]) / df["range_high"]) * 100
        ).astype(float)
        df["dist_from_range_low"] = (
            ((df["close"] - df["range_low"]) / df["range_low"]) * 100
        ).astype(float)

        return df

    def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        df = dataframe.copy()
        df["enter_long"] = 0
        df["enter_short"] = 0

        range_size = df["range_high"] - df["range_low"]
        tolerance = range_size * 0.01

        range_long_touch = df["low"] <= (df["range_low"] + tolerance)
        range_long_break = df["close"] < df["range_low"]
        range_long = range_long_touch | range_long_break

        range_short_touch = df["high"] >= (df["range_high"] - tolerance)
        range_short_break = df["close"] > df["range_high"]
        range_short = range_short_touch | range_short_break

        rsi_oversold = df["rsi"] < float(self.rsi_oversold.value)
        rsi_overbought = df["rsi"] > float(self.rsi_overbought.value)
        rsi_neutral_low = df["rsi"] < 50  # RSI sub 50 = momentum descendent
        rsi_neutral_high = df["rsi"] > 50  # RSI peste 50 = momentum ascendent

        vol_positive = df["volume_osc_norm"] > float(self.vol_osc_threshold.value)
        vol_negative = df["volume_osc_norm"] < -float(self.vol_osc_threshold.value)

        delta_bullish = df["cum_delta_norm"] > float(self.cum_delta_threshold.value)
        delta_bearish = df["cum_delta_norm"] < -float(self.cum_delta_threshold.value)

        mode = self.entry_mode.value

        if mode == "range_only":
            # Doar range, fără alte filtre - CEL MAI SENSIBIL
            long_cond = range_long
            short_cond = range_short

        elif mode == "range_rsi":
            # Range + RSI relaxat (nu neapărat oversold/overbought)
            long_cond = range_long & (rsi_oversold | rsi_neutral_low)
            short_cond = range_short & (rsi_overbought | rsi_neutral_high)

        elif mode == "range_volume":
            # Range + Volume
            long_cond = range_long & vol_positive
            short_cond = range_short & vol_negative

        else:  # range_all
            # Toate condițiile - CEL MAI STRICT
            long_cond = range_long & rsi_oversold & delta_bullish
            short_cond = range_short & rsi_overbought & delta_bearish

        df.loc[long_cond, "enter_long"] = 1
        df.loc[short_cond, "enter_short"] = 1

        return df

    def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
        df = dataframe.copy()
        df["exit_long"] = 0
        df["exit_short"] = 0

        # Exit la POC
        if self.exit_at_poc.value == "on":
            # Long exit când pret atinge POC/VWAP
            poc_long_exit = df["close"] >= df["poc"]
            # Short exit când pret atinge POC/VWAP
            poc_short_exit = df["close"] <= df["poc"]
        else:
            poc_long_exit = pd.Series(False, index=df.index)
            poc_short_exit = pd.Series(False, index=df.index)

        if self.exit_at_opposite_range.value == "on":

            range_long_exit = df["close"] >= df["range_high"]

            range_short_exit = df["close"] <= df["range_low"]
        else:
            range_long_exit = pd.Series(False, index=df.index)
            range_short_exit = pd.Series(False, index=df.index)

        rsi_long_exit = df["rsi"] > 70  # Exit long când RSI overbought
        rsi_short_exit = df["rsi"] < 30  # Exit short când RSI oversold

        exit_long = poc_long_exit | range_long_exit | rsi_long_exit
        exit_short = poc_short_exit | range_short_exit | rsi_short_exit

        df.loc[exit_long.fillna(False), "exit_long"] = 1
        df.loc[exit_short.fillna(False), "exit_short"] = 1

        return df

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> float:
        """
        Stoploss dinamic bazat pe ATR.
        """
        try:
            df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
            if df is None or df.empty:
                return self.stoploss

            atr = float(df.iloc[-1].get("atr", np.nan))
            if not np.isfinite(atr) or atr <= 0:
                return self.stoploss

            atr_pct = atr / float(current_rate)

            # Profit protection
            if current_profit > 0.04:
                return -0.01  # Lock in la 1%
            if current_profit > 0.02:
                return -0.015  # Lock in la 1.5%

            # ATR-based stoploss (2x ATR)
            sl = -float(2.0 * atr_pct)
            return float(np.clip(sl, -0.06, -0.015))  # Min -6%, Max -1.5%

        except Exception:
            return self.stoploss
