# source: https://raw.githubusercontent.com/ariswibono/horizon/9687043680eef6325d7e63c82efa8a626b505d14/user_data/strategies/Horizon.py
# directory_url: https://github.com/ariswibono/horizon/blob/main/user_data/strategies/
# User: ariswibono
# Repository: horizon
# --------------------import talib.abstract as ta
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
from freqtrade.enums import RunMode
from pandas import DataFrame
from datetime import datetime


class Github_ariswibono_horizon__Horizon__20260729_123755(IStrategy):
    """
    Github_ariswibono_horizon__Horizon__20260729_123755 Strategy

    A quantitative trend-following strategy designed for trading on 4-hour timeframe.
    It combines Linear Regression Slope for momentum detection with Exponential Moving
    Average (EMA) as a primary trend direction filter and Volume EMA for confirmation.
    """

    # Default ROI table fallback (overridden by Github_ariswibono_horizon__Horizon__20260729_123755.json in production/backtest)
    minimal_roi = {
        "0": 0.20,
        "240": 0.12,
        "720": 0.08,
        "1440": 0.04
    }

    # Default stoploss fallback (-3.4%)
    stoploss = -0.034

    # Primary timeframe
    timeframe = "4h"

    # Strategy capabilities
    can_short = True
    startup_candle_count = 200

    def leverage(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_leverage: float,
        max_leverage: float,
        entry_tag: str,
        side: str,
        **kwargs,
    ) -> float:
        """
        Custom leverage calculation callback.
        Reads leverage configuration dynamically from strategy_settings in config file.
        """
        cfg_lev = (
            self.config.get("strategy_settings", {})
            .get("Github_ariswibono_horizon__Horizon__20260729_123755", {})
            .get("leverage", 2.0)
        )
        try:
            lev = float(cfg_lev)
        except Exception:
            lev = 2.0

        if max_leverage and lev > max_leverage:
            return float(max_leverage)
        return lev

    def confirm_trade_entry(
        self,
        pair: str,
        order_type: str,
        amount: float,
        rate: float,
        time_in_force: str,
        current_time: datetime,
        entry_tag: str,
        side: str,
        **kwargs,
    ) -> bool:
        """
        Confirm trade entry callback.
        Optional account value cap check via strategy_settings.Github_ariswibono_horizon__Horizon__20260729_123755.max_account_value.
        """
        cfg = self.config.get("strategy_settings", {}).get("Github_ariswibono_horizon__Horizon__20260729_123755", {})
        max_val = cfg.get("max_account_value")
        if max_val is not None:
            try:
                tot = self.wallets.get_total_stake_amount()
                if tot >= float(max_val):
                    if self.config.get("runmode") != RunMode.HYPEROPT:
                        print(f"[Github_ariswibono_horizon__Horizon__20260729_123755] Skip entry {pair}: account value {tot} >= max {max_val}")
                    return False
            except Exception:
                pass
        return True

    # Strategy Parameters (Hyperopt-able boundaries)
    slope_period = IntParameter(10, 50, default=24, space="buy")
    ema_trend_period = IntParameter(20, 100, default=50, space="buy")
    buy_slope_min = DecimalParameter(0.0001, 0.002, default=0.0005, space="buy")
    short_slope_max = DecimalParameter(-0.002, -0.0001, default=-0.0005, space="sell")
    exit_long_slope = DecimalParameter(-0.001, 0.001, default=-0.0005, space="sell")
    exit_short_slope = DecimalParameter(-0.001, 0.001, default=0.0005, space="buy")

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Populate Technical Analysis indicators for the given dataframe.
        """
        # Linear Regression Slope for momentum strength
        dataframe["slope"] = ta.LINEARREG_SLOPE(
            dataframe["close"], timeperiod=self.slope_period.value
        )
        # Exponential Moving Average for main trend filter
        dataframe["ema_trend"] = ta.EMA(
            dataframe["close"], timeperiod=self.ema_trend_period.value
        )
        # 20-period Volume EMA for volume confirmation
        dataframe["vol_ema"] = ta.EMA(dataframe["volume"], timeperiod=20)

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Populate entry (buy/short) signals.
        Uses shifted candles (.shift(1)) to prevent lookahead bias / repaint.
        """
        # Long Entry Conditions:
        # 1. Linear slope is strongly positive (> buy_slope_min)
        # 2. Close price is above the primary EMA trend line (Uptrend)
        # 3. Volume is above 20-period Volume EMA (Volume confirmation)
        dataframe.loc[
            (
                (dataframe["slope"].shift(1) > float(self.buy_slope_min.value))
                & (dataframe["close"].shift(1) > dataframe["ema_trend"].shift(1))
                & (dataframe["volume"].shift(1) > dataframe["vol_ema"].shift(1))
                & (dataframe["volume"].shift(1) > 0)
            ),
            "enter_long",
        ] = 1

        # Short Entry Conditions:
        # 1. Linear slope is strongly negative (< short_slope_max)
        # 2. Close price is below the primary EMA trend line (Downtrend)
        # 3. Volume is above 20-period Volume EMA (Volume confirmation)
        dataframe.loc[
            (
                (dataframe["slope"].shift(1) < float(self.short_slope_max.value))
                & (dataframe["close"].shift(1) < dataframe["ema_trend"].shift(1))
                & (dataframe["volume"].shift(1) > dataframe["vol_ema"].shift(1))
                & (dataframe["volume"].shift(1) > 0)
            ),
            "enter_short",
        ] = 1

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Populate exit (sell) signals based on slope trend reversal.
        """
        # Exit Long: Slope reverses downwards below threshold
        dataframe.loc[
            ((dataframe["slope"].shift(1) < float(self.exit_long_slope.value))),
            "exit_long",
        ] = 1

        # Exit Short: Slope reverses upwards above threshold
        dataframe.loc[
            ((dataframe["slope"].shift(1) > float(self.exit_short_slope.value))),
            "exit_short",
        ] = 1

        return dataframe
