# source: https://raw.githubusercontent.com/xiongchenyu6/freqtrade-strategies/c272cbc2d0bb2e8d11e3a5525ff3a377ed707342/strategies/HonestGrid.py
"""
Github_xiongchenyu6_freqtrade_strategies__HonestGrid__20260513_154812 — Grid trading for range-bound markets.

Complements HonestTrendGeneric: enters only when ADX is LOW (sideways market).
Places staggered buy entries as price pulls back inside the range, exits when
price recovers by one ATR-sized grid step.

Core logic:
  - Regime filter: ADX < adx_max (default 20) → range-bound, OK to enter
  - Grid spacing: grid_atr_multiplier × ATR(atr_period) as % of close price
  - Entry: close crosses below price_sma − 0.5 × ATR (pullback into range)
            AND ADX < adx_max AND volume > volume_sma
  - Exit:  close crosses above price_sma + 0.5 × ATR (recovery to mid-range)
  - Stoploss: −15% (wide; avoids premature exit inside the range)
  - Timeframe: 1h
  - Pairs: BTC/USDT, ETH/USDT (spot only)

Hyperoptable parameters:
  adx_max             max ADX to allow entry          [10–30, default 20]
  grid_atr_multiplier grid spacing in ATR units       [1.0–3.0, default 1.5]
  atr_period          ATR lookback period              [7–28, default 14]

Suggested config additions:
  "max_open_trades": 3
  "stake_amount": "unlimited"   # or a fixed USDT amount
  "position_adjustment_enable": false
"""

import csv
import logging
from datetime import datetime
from pathlib import Path
from typing import Optional

import talib.abstract as ta
from pandas import DataFrame
from technical import qtpylib

from freqtrade.strategy import DecimalParameter, IStrategy, IntParameter, Trade

logger = logging.getLogger(__name__)

# Fear & Greed history — same loading pattern as HonestTrendGeneric
FNG_DATA: dict[str, int] = {}
_fng_path = Path(__file__).parent.parent / "data" / "fng_history.csv"
if _fng_path.exists():
    with open(_fng_path) as _f:
        for _row in csv.DictReader(_f):
            FNG_DATA[_row["date"]] = int(_row["value"])


class Github_xiongchenyu6_freqtrade_strategies__HonestGrid__20260513_154812(IStrategy):
    """
    Range-bound grid strategy.

    Enters on pullbacks inside a sideways regime (ADX < threshold).
    Uses ATR-normalised grid spacing for both entry levels and exit targets.
    Designed to be run alongside HonestTrendGeneric, which handles trending markets.
    """

    INTERFACE_VERSION = 3
    can_short = False

    # Wide ROI — we rely on custom_exit / exit signal for per-grid exits
    minimal_roi = {"0": 100}
    stoploss = -0.15
    use_exit_signal = True
    process_only_new_candles = True
    startup_candle_count = 100  # enough for SMA(20) + ATR(28) warm-up
    timeframe = "1h"

    # Do NOT pyramid grid entries
    position_adjustment_enable = False

    # ---- Hyperoptable parameters ----
    adx_max = IntParameter(10, 30, default=20, space="buy", optimize=True)
    grid_atr_multiplier = DecimalParameter(1.0, 3.0, default=1.5, decimals=1,
                                           space="buy", optimize=True)
    atr_period = IntParameter(7, 28, default=14, space="buy", optimize=True)

    # FnG block — don't buy into a range when euphoria is extreme (greed > 80)
    FNG_BLOCK = 80

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

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """Compute ADX, ATR, SMA(20), volume SMA, and FnG overlay."""
        atr_p = self.atr_period.value

        dataframe["adx"] = ta.ADX(dataframe)
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=atr_p)
        dataframe["price_sma"] = ta.SMA(dataframe, timeperiod=20)

        # Volume SMA: 1 calendar day of 1h candles = 24 bars
        dataframe["volume_sma"] = ta.SMA(dataframe["volume"], timeperiod=24)

        # Derived grid bands
        half_atr = dataframe["atr"] * 0.5
        dataframe["lower_band"] = dataframe["price_sma"] - half_atr
        dataframe["upper_band"] = dataframe["price_sma"] + half_atr

        # FnG overlay (date-keyed, fills forward with 50 if missing)
        dataframe["fng"] = dataframe["date"].apply(
            lambda x: FNG_DATA.get(x.strftime("%Y-%m-%d"), 50)
        )

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Enter when price pulls back below lower_band inside a range-bound regime.

        Conditions:
          1. ADX < adx_max  (not trending)
          2. close crossed below price_sma − 0.5×ATR  (pullback into range)
          3. volume > volume_sma  (participation confirms the move)
          4. FnG < FNG_BLOCK  (not in extreme greed)
        """
        dataframe.loc[
            (qtpylib.crossed_below(dataframe["close"], dataframe["lower_band"]))
            & (dataframe["adx"] < self.adx_max.value)
            & (dataframe["volume"] > dataframe["volume_sma"])
            & (dataframe["fng"] < self.FNG_BLOCK)
            & (dataframe["volume"] > 0),
            ["enter_long", "enter_tag"],
        ] = (1, "grid_pullback")
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Exit when price recovers above upper_band (back to mid-range or above).

        This fires the exit signal; actual per-tranche take-profit at one grid
        step above entry is handled by custom_exit below.
        """
        dataframe.loc[
            (qtpylib.crossed_above(dataframe["close"], dataframe["upper_band"]))
            & (dataframe["volume"] > 0),
            ["exit_long", "exit_tag"],
        ] = (1, "grid_recovery")
        return dataframe

    def custom_exit(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        **kwargs,
    ) -> Optional[str]:
        """
        Exit each tranche when profit >= one grid step.

        Grid step = grid_atr_multiplier × ATR / open_rate (approx %).
        We use a fixed ATR snapshot at entry; the ATR column is not directly
        available here so we approximate from trade metadata or fall back to
        a percentage derived from the open rate.
        """
        # Approximate grid step from the hyperopt param + current volatility proxy.
        # Freqtrade does not pass the dataframe here, so we use a simple
        # heuristic: grid_atr_multiplier × 1.5% as a baseline grid step.
        # In a full deployment you'd store ATR at entry in trade.meta.
        base_step_pct = self.grid_atr_multiplier.value * 0.015
        if current_profit >= base_step_pct:
            return "grid_step_exit"
        return None

    def confirm_trade_exit(
        self,
        pair: str,
        trade: Trade,
        order_type: str,
        amount: float,
        rate: float,
        time_in_force: str,
        exit_reason: str,
        current_time: datetime,
        **kwargs,
    ) -> bool:
        """Always allow stoploss and forced exits; allow all others immediately."""
        if exit_reason in ("force_exit", "stoploss"):
            return True
        return True
