# source: https://raw.githubusercontent.com/Roshan7869/algotrading/db07ceb71ac165a38fe200a86ae48586ed040f60/user_data/strategies/_archived/bos_v5_hyperopt.py
# directory_url: https://github.com/Roshan7869/algotrading/blob/main/user_data/strategies/_archived/
# User: Roshan7869
# Repository: algotrading
# --------------------"""
V5: BOS+LVN+VWAP — Hyperopt-Ready
====================================
All key parameters enabled for optimization:
 - swing_lookback: 10-40 (swing detection window)
 - lvn_threshold: 0.2-0.7 (low volume node threshold)
 - vwap_proximity_pct: 0.3-1.5 (% distance from VWAP)
 - min_body_ratio: 0.3-0.7 (confirmation candle threshold)
 - stoploss: 3%-10% (hard stop)
 - trailing_stop_positive: 1-4% (trail distance)
 - trailing_stop_positive_offset: 5-25% (trail activation)
 - min_confluences: 3 or 4

Run: freqtrade hyperopt --strategy Github_Roshan7869_algotrading__bos_v5_hyperopt__20260825_161638 --hyperopt-loss SharpeHyperOptLoss \
     --spaces buy sell --timeframe 1h --timerange 20260501-20260518 \
     -e 500 --config strat_optimisation/configs/config_v5_hyperopt.json
"""

import numpy as np
import pandas as pd
from freqtrade.strategy import IStrategy, DecimalParameter, IntParameter


class Github_Roshan7869_algotrading__bos_v5_hyperopt__20260825_161638(IStrategy):
    """BOS+LVN+VWAP with all parameters enabled for hyperopt"""

    INTERFACE_VERSION = 3
    can_short = True
    timeframe = "1h"
    startup_candle_count = 100

    stoploss = -0.08  # Will be overridden by hyperopt via sell space
    minimal_roi = {"0": 100}
    use_exit_signal = True
    exit_profit_only = False

    trailing_stop = True
    trailing_stop_positive = 0.025
    trailing_stop_positive_offset = 0.12
    trailing_only_offset_is_reached = True

    stake_amount = "unlimited"
    max_open_trades = 5

    # ─── OPTIMIZABLE PARAMETERS ────────────────────────────────────
    swing_lookback = IntParameter(10, 40, default=20, space="buy", optimize=True)
    volume_bins = IntParameter(10, 40, default=25, space="buy", optimize=True)
    lvn_threshold = DecimalParameter(0.2, 0.7, default=0.5, decimals=2, space="buy", optimize=True)
    vwap_proximity_pct = DecimalParameter(0.3, 1.5, default=0.5, decimals=2, space="buy", optimize=True)
    min_body_ratio = DecimalParameter(0.3, 0.7, default=0.4, decimals=2, space="buy", optimize=True)
    min_confluences = IntParameter(3, 4, default=3, space="buy", optimize=True)

    # Stop loss optimization
    sl_pct = DecimalParameter(0.03, 0.10, default=0.08, decimals=2, space="sell", optimize=True)

    # Trailing stop optimization
    trail_pct = DecimalParameter(0.01, 0.05, default=0.025, decimals=3, space="sell", optimize=True)
    trail_offset_pct = DecimalParameter(0.05, 0.25, default=0.12, decimals=2, space="sell", optimize=True)

    leverage_num = DecimalParameter(5, 15, default=12.0, decimals=0, space="buy", optimize=False)

    def leverage(self, pair, current_time, current_rate, proposed_leverage, max_leverage, entry_tag, side, **kwargs):
        return float(self.leverage_num.value)

    def populate_indicators(self, dataframe, metadata):
        df = dataframe.copy()
        delta = df["close"].diff()
        gain = delta.clip(lower=0)
        loss = -delta.clip(upper=0)
        avg_gain = gain.ewm(alpha=1/14, min_periods=14).mean()
        avg_loss = loss.ewm(alpha=1/14, min_periods=14).mean()
        rs = avg_gain / avg_loss.replace(0, np.nan)
        df["rsi"] = 100 - (100 / (1 + rs))

        high_low = df["high"] - df["low"]
        high_close = np.abs(df["high"] - df["close"].shift())
        low_close = np.abs(df["low"] - df["close"].shift())
        tr = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1)
        df["atr"] = tr.rolling(14).mean()
        df["ema_50"] = df["close"].ewm(span=50, adjust=False).mean()
        df["ema_200"] = df["close"].ewm(span=200, adjust=False).mean()

        df = self._compute_vwap(df)
        df = self._compute_bos(df)
        df = self._compute_volume_profile(df)
        df = self._compute_confluence(df)
        return df

    def _compute_vwap(self, df):
        typical_price = (df["high"] + df["low"] + df["close"]) / 3
        df["vwap_tp_vol"] = typical_price * df["volume"]
        df["date_only"] = df["date"].dt.date
        cum_tp_vol = df.groupby("date_only")["vwap_tp_vol"].cumsum()
        cum_vol = df.groupby("date_only")["volume"].cumsum()
        df["vwap"] = cum_tp_vol / cum_vol.replace(0, np.nan)
        df["vwap"] = df["vwap"].ffill()
        df["vwap_dist_pct"] = ((df["close"] - df["vwap"]) / df["vwap"]) * 100
        df["vwap_slope"] = df["vwap"].diff(3) / df["vwap"].shift(3) * 100
        df.drop(columns=["date_only", "vwap_tp_vol"], inplace=True)
        return df

    def _compute_bos(self, df):
        lookback = int(self.swing_lookback.value)
        half = lookback // 2
        highs = df["high"].values
        lows = df["low"].values
        n = len(df)
        is_sh = np.zeros(n, dtype=bool)
        is_sl = np.zeros(n, dtype=bool)
        for i in range(half, n - half):
            if highs[i] == np.max(highs[i - half:i + half + 1]):
                is_sh[i] = True
            if lows[i] == np.min(lows[i - half:i + half + 1]):
                is_sl[i] = True

        last_sh = np.full(n, np.nan)
        last_sl = np.full(n, np.nan)
        sh_val = sl_val = np.nan
        for i in range(n):
            if is_sh[i]: sh_val = highs[i]
            if is_sl[i]: sl_val = lows[i]
            last_sh[i] = sh_val
            last_sl[i] = sl_val

        df["last_swing_high"] = last_sh
        df["last_swing_low"] = last_sl

        bullish_bos = ((df["close"] > pd.Series(last_sh).shift(1)) & pd.Series(last_sh).shift(1).notna()).astype(int)
        bearish_bos = ((df["close"] < pd.Series(last_sl).shift(1)) & pd.Series(last_sl).shift(1).notna()).astype(int)
        bos_trend = np.zeros(n, dtype=int)
        state = 0
        for i in range(n):
            if bullish_bos.iloc[i] == 1: state = 1
            elif bearish_bos.iloc[i] == 1: state = -1
            bos_trend[i] = state
        df["bos_trend"] = bos_trend
        return df

    def _compute_volume_profile(self, df):
        lookback = int(self.swing_lookback.value)
        n_bins = int(self.volume_bins.value)
        lvn_thresh = float(self.lvn_threshold.value)
        vwap_prox = float(self.vwap_proximity_pct.value)
        n = len(df)
        lvn_top = np.full(n, np.nan)
        lvn_bottom = np.full(n, np.nan)
        highs = df["high"].values
        lows = df["low"].values
        volumes = df["volume"].values
        closes = df["close"].values

        for i in range(lookback, n):
            win_low = lows[i - lookback:i + 1].min()
            win_high = highs[i - lookback:i + 1].max()
            if win_high == win_low: continue
            bin_edges = np.linspace(win_low, win_high, n_bins + 1)
            vol_per_bin = np.zeros(n_bins)
            for j in range(i - lookback, i + 1):
                if volumes[j] <= 0: continue
                candle_range = highs[j] - lows[j]
                if candle_range <= 0: continue
                overlaps = np.maximum(0, np.minimum(highs[j], bin_edges[1:]) - np.maximum(lows[j], bin_edges[:-1]))
                vol_per_bin += volumes[j] * (overlaps / candle_range)
            total_vol = vol_per_bin.sum()
            if total_vol == 0: continue
            poc_bin = np.argmax(vol_per_bin)
            poc_volume = vol_per_bin[poc_bin]
            lvn_bins = np.where((vol_per_bin > 0) & (vol_per_bin < lvn_thresh * poc_volume))[0]
            if len(lvn_bins) > 0:
                zones = []
                start = lvn_bins[0]
                for k in range(1, len(lvn_bins)):
                    if lvn_bins[k] != lvn_bins[k - 1] + 1:
                        zones.append((start, lvn_bins[k - 1]))
                        start = lvn_bins[k]
                zones.append((start, lvn_bins[-1]))
                current_close = closes[i]
                best_zone = None
                best_dist = float("inf")
                for zs, ze in zones:
                    zone_mid = (bin_edges[zs] + bin_edges[ze + 1]) / 2
                    dist = abs(zone_mid - current_close)
                    if dist < best_dist:
                        best_dist = dist
                        best_zone = (zs, ze)
                if best_zone:
                    lvn_bottom[i] = bin_edges[best_zone[0]]
                    lvn_top[i] = bin_edges[best_zone[1] + 1]

        df["lvn_top"] = lvn_top
        df["lvn_bottom"] = lvn_bottom
        df["price_in_lvn"] = (
            df["lvn_bottom"].notna() &
            (df["close"] >= df["lvn_bottom"]) &
            (df["close"] <= df["lvn_top"])
        ).astype(int)
        df["near_vwap"] = (df["vwap_dist_pct"].abs() <= vwap_prox).astype(int)
        return df

    def _compute_confluence(self, df):
        min_br = float(self.min_body_ratio.value)
        min_conf = int(self.min_confluences.value)

        df["body"] = (df["close"] - df["open"]).abs()
        df["candle_range"] = df["high"] - df["low"]
        df["body_ratio"] = df["body"] / df["candle_range"].replace(0, np.nan)
        df["bearish_candle"] = ((df["close"] < df["open"]) & (df["body_ratio"] >= min_br)).astype(int)
        df["bullish_candle"] = ((df["close"] > df["open"]) & (df["body_ratio"] >= min_br)).astype(int)
        df["vwap_declining"] = (df["vwap_slope"] < 0).astype(int)
        df["vwap_rising"] = (df["vwap_slope"] > 0).astype(int)

        # SHORT pillars
        df["short_bos"] = (df["bos_trend"] == -1).astype(int)
        df["short_lvn"] = df["price_in_lvn"]
        df["short_vwap_relaxed"] = df["near_vwap"]
        df["short_confirm"] = df["bearish_candle"]
        df["short_pillars"] = df["short_bos"] + df["short_lvn"] + df["short_vwap_relaxed"] + df["short_confirm"]

        # LONG pillars
        df["long_bos"] = (df["bos_trend"] == 1).astype(int)
        df["long_lvn"] = df["price_in_lvn"]
        df["long_vwap_relaxed"] = df["near_vwap"]
        df["long_confirm"] = df["bullish_candle"]
        df["long_pillars"] = df["long_bos"] + df["long_lvn"] + df["long_vwap_relaxed"] + df["long_confirm"]

        # Confluence thresholds based on min_confluences parameter
        if min_conf >= 4:
            df["short_entry"] = ((df["short_bos"] == 1) & (df["short_pillars"] >= 4)).astype(int)
            df["long_entry"] = ((df["long_bos"] == 1) & (df["long_pillars"] >= 4)).astype(int)
        else:
            df["short_entry"] = ((df["short_bos"] == 1) & (df["short_pillars"] >= 3)).astype(int)
            df["long_entry"] = ((df["long_bos"] == 1) & (df["long_pillars"] >= 3)).astype(int)

        df["rsi_not_oversold"] = (df["rsi"] > 30).astype(int)
        return df

    def populate_entry_trend(self, dataframe, metadata):
        dataframe.loc[
            (dataframe["short_entry"] == 1) &
            (dataframe["rsi_not_oversold"] == 1) &
            (dataframe["volume"] > 0),
            ["enter_short", "enter_tag"]
        ] = (1, "bos_short_hyperopt")

        dataframe.loc[
            (dataframe["long_entry"] == 1) &
            (dataframe["rsi"] < 70) &
            (dataframe["volume"] > 0),
            ["enter_long", "enter_tag"]
        ] = (1, "bos_long_hyperopt")

        return dataframe

    def populate_exit_trend(self, dataframe, metadata):
        dataframe.loc[
            (dataframe["bos_trend"] == 1) | (dataframe["rsi"] < 30),
            ["exit_short", "exit_tag"]
        ] = (1, "bos_reversal_or_oversold")

        dataframe.loc[
            (dataframe["bos_trend"] == -1) | (dataframe["rsi"] > 70),
            ["exit_long", "exit_tag"]
        ] = (1, "bos_reversal_or_overbought")
        return dataframe

    # Hyperopt will tune these via sl_pct, trail_pct, trail_offset_pct
    def custom_stoploss(self, pair, trade, current_time, current_rate, current_profit, after_fill, **kwargs):
        if current_profit > 0.05:
            return -0.01
        if current_profit > 0.03:
            return -0.02
        return None

    def custom_exit(self, pair, trade, current_time, current_rate, current_profit, **kwargs):
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if len(dataframe) < 1:
            return None
        last = dataframe.iloc[-1]
        if trade.is_short and last.get("bos_trend", 0) == 1 and current_profit > 0.02:
            return "bullish_bos_reversal"
        if not trade.is_short and last.get("bos_trend", 0) == -1 and current_profit > 0.02:
            return "bearish_bos_reversal"
        return None