# source: https://raw.githubusercontent.com/VanceChu/Crypto-Hackers-NUTS/2e3976789ad8bc2aa3b63acb221cc444f2472e47/final_strategies/sources/MarchBreaker30m.py
"""
================================================================================
Github_VanceChu_Crypto_Hackers_NUTS__MarchBreaker30m__20260327_180934 — Competition Strategy
================================================================================
Target   : >20% return in a 10-day spot-only long competition window
Start    : 2026-03-21
Platform : Roostoo / freqtrade (IStrategy v3)
Timeframe: 30 min

--------------------------------------------------------------------------------
STRATEGY OVERVIEW
--------------------------------------------------------------------------------
Github_VanceChu_Crypto_Hackers_NUTS__MarchBreaker30m__20260327_180934 is a momentum-breakout strategy designed for a 10-day long-only
competition on spot markets.  The core idea is simple:

  1. Wait for a coin to break out of its prior 6-hour high with meaningful volume
  2. Confirm the move with RSI momentum and trend alignment (price > MA20)
  3. Require BTC to be in a bullish regime (BTC > MA50) before entering any trade
  4. Dynamically select only the top-6 coins by 48-hour momentum at each bar,
     so capital always flows to current market leaders rather than laggards
  5. Exit on a 4-hour low break, a -5% hard stop, or an 8% trailing stop from peak
  6. Close ALL positions and halt when portfolio equity reaches +20% — locking
     in the competition target

--------------------------------------------------------------------------------
ENTRY CONDITIONS  (all must be true simultaneously)
--------------------------------------------------------------------------------
  A. close > highest_high(12)            — 6h breakout (shift-1, no look-ahead)
  B. close > MA(20)                      — above medium-term trend
  C. volume > 2.0 × vol_MA(20)          — real volume surge (10h average)
  D. 48 < RSI(7) < 82                    — momentum entering, not exhausted
  E. |Δclose| < 20%                      — anti-chase filter (no parabolic bars)
  F. BTC_close > BTC_MA(50)             — macro bullish regime
  G. coin is in Top-6 by 48h momentum   — dynamic rotation: only leaders enter

Execution: signal fires at candle close → buy at NEXT candle open

--------------------------------------------------------------------------------
EXIT CONDITIONS  (first triggered wins)
--------------------------------------------------------------------------------
  1. close < lowest_low(8)              — 4h low break (trend failure)
  2. current_price < entry × 0.95       — hard stop -5%
  3. current_price < peak × 0.92        — trailing stop -8% from high
  4. ROI: +20% immediate / +15% @30h / +10% @60h / +5% @120h
  5. Portfolio equity ≥ 120% of start   — close ALL positions, halt trading

--------------------------------------------------------------------------------
UNIVERSE  (20 coins, rotating selection at runtime)
--------------------------------------------------------------------------------
  AI / Infra  : TAO, NEAR, FET, ENA, SEI
  High-β L1   : SOL, SUI, APT, ARB, AVAX
  Meme        : PEPE, BONK, DOGE, WIF, FLOKI, SHIB

  All 20 coins are kept in the universe.  The Top-6 by 48h momentum filter
  decides which ones are ELIGIBLE for new entries at each 30-min bar.
  Existing open positions are never force-closed by the rotation filter.

--------------------------------------------------------------------------------
RISK PARAMETERS
--------------------------------------------------------------------------------
  Max open positions : 5  (equal capital allocation per slot)
  Hard stop loss     : -5%  per trade
  Trailing stop      : -8%  from trade peak
  Portfolio hard stop: -8%  from portfolio peak  (only active after +8% gain)
  Portfolio target TP: +20% → close all, halt

--------------------------------------------------------------------------------
BACKTEST RESULTS  (standalone engine, Binance 30m data, fee=0.1%, slip=0.05%)
--------------------------------------------------------------------------------
  Window          Return    MaxDD   Sharpe  WinRate  Trades   BTC      Notes
  Jan  1-11      +20.43% *  2.96%   18.94    91.7%     12   +3.22%   Target hit day 4
  Mar  1-11       -2.06%    6.42%   -1.39    35.7%     28   +4.91%   Choppy recovery
  Mar  8-18       +3.63%    4.75%    3.09    50.0%     30   +9.55%   Steady grind
  Mar 11-21       +4.45%    3.96%    6.31    63.2%     19   +5.97%   Pre-comp window

  * Portfolio TP triggered on day 4 → equity locked at $1,204,339

  Top contributors (Mar 11-21):
    FET  : +$41,421  (2 trades, 100% win rate)
    TAO  : +$11,539  (3 trades,  67% win rate)
    RENDER: +$8,615  (2 trades,  50% win rate)

  Key risk (Mar 1-11): BONK/NEAR gave false breakouts in choppy early-March
  Mitigated by: rotation filter (top-6 by 48h momentum) limits exposure

--------------------------------------------------------------------------------
NOTES FOR QUANT DEVELOPER
--------------------------------------------------------------------------------
  * BTC informative pair must be included in pair_whitelist (already done in config)
  * The portfolio target-TP (+20%) is implemented via freqtrade ROI at 0 minutes
    and custom_stoploss.  The "close all at once" behavior requires an external
    coordinator or a custom_exit wrapper if exact simultaneous close is required.
  * Dynamic rotation is NOT implementable cross-pair in a standard freqtrade
    IStrategy. The production version should either:
      (a) Use the standalone engine (run_march_competition.py) with Binance WS
      (b) Approximate with a per-coin 48h momentum indicator (see below)
  * Standalone backtest engine: hackathon_roostoo/backtest/run_march_competition.py

================================================================================
"""

import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy

try:
    import talib.abstract as ta
    HAS_TALIB = True
except ImportError:
    HAS_TALIB = False


class Github_VanceChu_Crypto_Hackers_NUTS__MarchBreaker30m__20260327_180934(IStrategy):
    """
    6h Breakout + 4h Low Exit + 8% Trailing Stop + 20% Portfolio TP
    BTC > MA50 macro filter + 48h per-coin momentum filter.
    """

    INTERFACE_VERSION: int = 3
    can_short: bool = False
    timeframe = "30m"

    # ── ROI / Stop ───────────────────────────────────────────────────────────
    minimal_roi = {
        "0":   0.20,   # lock +20% immediately (competition target)
        "60":  0.15,   # +15% after 30h
        "120": 0.10,   # +10% after 60h
        "240": 0.05,   # + 5% after 120h (safety net)
    }

    stoploss = -0.05            # hard stop -5%
    use_custom_stoploss = True  # enables 8% trailing via custom_stoploss
    trailing_stop = False       # handled in custom_stoploss

    startup_candle_count: int = 100   # warmup for MA + momentum indicators
    process_only_new_candles = True
    use_exit_signal = True
    exit_profit_only = False
    ignore_roi_if_entry_signal = False

    # ── Strategy constants ───────────────────────────────────────────────────
    BREAKOUT_WINDOW   = 12    # 6h breakout lookback (12 × 30m)
    EXIT_LOW_WINDOW   = 8     # 4h exit low lookback  (8 × 30m)
    VOL_AVG_WINDOW    = 20    # 10h volume baseline
    VOL_SURGE_THRESH  = 2.0   # minimum volume multiplier
    RSI_PERIOD        = 7
    RSI_THRESHOLD     = 48    # minimum RSI for entry
    RSI_MAX           = 82    # maximum RSI (overbought guard)
    MA_PERIOD         = 20
    MAX_BAR_MOVE      = 0.20  # max single-candle return (anti-chase)
    TRAILING_STOP_PCT = 0.08  # 8% trailing stop from peak
    BTC_MA_PERIOD     = 50    # BTC MA50 (~25h)
    MOMENTUM_WINDOW   = 96    # 48h momentum lookback (96 × 30m)
    MOMENTUM_MIN      = 0.00  # coin must be up > 0% over 48h to enter

    plot_config = {
        "main_plot": {
            "ma20":         {"color": "#1E90FF"},
            "highest_high": {"color": "#32CD32", "type": "line"},
            "lowest_low":   {"color": "#FF6347", "type": "line"},
        },
        "subplots": {
            "RSI(7)": {"rsi7": {"color": "#9370DB"}},
            "Vol x":  {"vol_ratio": {"color": "#4682B4", "type": "bar"}},
            "Mom48h": {"momentum_48h": {"color": "#FF8C00", "type": "line"}},
        },
    }

    # ── Informative pairs ────────────────────────────────────────────────────
    def informative_pairs(self):
        return [("BTC/USD", "30m")]

    # ── Indicators ───────────────────────────────────────────────────────────
    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:

        # --- Trend & momentum indicators ------------------------------------
        if HAS_TALIB:
            dataframe["ma20"] = ta.SMA(dataframe, timeperiod=self.MA_PERIOD)
            dataframe["rsi7"] = ta.RSI(dataframe, timeperiod=self.RSI_PERIOD)
        else:
            dataframe["ma20"] = dataframe["close"].rolling(self.MA_PERIOD).mean()
            delta    = dataframe["close"].diff()
            gain     = delta.clip(lower=0)
            loss     = -delta.clip(upper=0)
            alpha    = 1.0 / self.RSI_PERIOD
            avg_gain = gain.ewm(alpha=alpha, min_periods=self.RSI_PERIOD,
                                adjust=False).mean()
            avg_loss = loss.ewm(alpha=alpha, min_periods=self.RSI_PERIOD,
                                adjust=False).mean()
            rs = avg_gain / avg_loss
            dataframe["rsi7"] = 100 - (100 / (1 + rs))

        # --- Breakout channel -----------------------------------------------
        # shift(1) prevents look-ahead bias: today's high cannot break today's high
        dataframe["highest_high"] = (
            dataframe["high"].rolling(self.BREAKOUT_WINDOW).max().shift(1)
        )
        dataframe["lowest_low"] = (
            dataframe["low"].rolling(self.EXIT_LOW_WINDOW).min().shift(1)
        )

        # --- Volume ratio ---------------------------------------------------
        dataframe["vol_avg"]   = dataframe["volume"].rolling(self.VOL_AVG_WINDOW).mean()
        dataframe["vol_ratio"] = dataframe["volume"] / dataframe["vol_avg"]

        # --- Anti-chase: single-candle return magnitude ---------------------
        dataframe["bar_return_abs"] = dataframe["close"].pct_change().abs()

        # --- 48h per-coin momentum (rotation proxy) -------------------------
        # Full dynamic rotation (cross-pair ranking) requires the standalone engine.
        # Here we use a per-coin 48h return as an approximation: coin must be
        # trending UP over the last 48h to be eligible for entry.
        dataframe["momentum_48h"] = (
            dataframe["close"] / dataframe["close"].shift(self.MOMENTUM_WINDOW) - 1
        )

        # --- BTC macro regime filter ----------------------------------------
        inf_pair  = "BTC/USD"
        informative = self.dp.get_pair_dataframe(pair=inf_pair, timeframe="30m")
        if not informative.empty:
            informative["btc_ma50"] = (
                informative["close"].rolling(self.BTC_MA_PERIOD).mean()
            )
            informative["btc_bullish"] = (
                informative["close"] > informative["btc_ma50"]
            )
            informative = informative[["date", "btc_bullish"]].copy()
            dataframe = pd.merge_asof(
                dataframe.sort_values("date"),
                informative.sort_values("date"),
                on="date",
                direction="backward",
            )
            if "btc_bullish" not in dataframe.columns:
                dataframe["btc_bullish"] = True
        else:
            dataframe["btc_bullish"] = True

        return dataframe

    # ── Entry signal ─────────────────────────────────────────────────────────
    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        All conditions must be true.  Trade executes at NEXT candle open.

          A) close > highest_high(12)      6h breakout confirmed
          B) close > MA20                  above medium-term trend
          C) vol_ratio > 2.0               genuine volume surge
          D) 48 < RSI(7) < 82             momentum active, not exhausted
          E) |Δclose| < 20%               no parabolic chasing
          F) BTC > MA50                   macro bullish
          G) momentum_48h > 0%            coin trending up last 48h (rotation proxy)
        """
        btc_ok = dataframe.get(
            "btc_bullish",
            pd.Series(True, index=dataframe.index)
        )

        conditions = (
            (dataframe["close"] > dataframe["highest_high"])           # A
            & (dataframe["close"] > dataframe["ma20"])                 # B
            & (dataframe["vol_ratio"] > self.VOL_SURGE_THRESH)         # C
            & (dataframe["rsi7"] > self.RSI_THRESHOLD)                 # D
            & (dataframe["rsi7"] < self.RSI_MAX)                       # D
            & (dataframe["bar_return_abs"] < self.MAX_BAR_MOVE)        # E
            & btc_ok                                                    # F
            & (dataframe["momentum_48h"] > self.MOMENTUM_MIN)          # G
            & dataframe["highest_high"].notna()
            & dataframe["ma20"].notna()
            & dataframe["rsi7"].notna()
            & dataframe["momentum_48h"].notna()
            & (dataframe["volume"] > 0)
        )
        dataframe.loc[conditions, "enter_long"] = 1
        return dataframe

    # ── Exit signal ──────────────────────────────────────────────────────────
    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        """
        Exit when close breaks below the 4h low (trend failure signal).
        Hard stop / trailing stop / ROI are handled by stoploss + minimal_roi.
        """
        dataframe.loc[
            (dataframe["close"] < dataframe["lowest_low"])
            & dataframe["lowest_low"].notna()
            & (dataframe["volume"] > 0),
            "exit_long",
        ] = 1
        return dataframe

    # ── Custom stoploss: 8% trailing from peak ───────────────────────────────
    def custom_stoploss(
        self,
        pair: str,
        trade,
        current_time,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs,
    ) -> float:
        """
        Trailing stop: stop = max(peak × 0.92, entry × 0.95)
        Never exceeds the -5% hard floor.
        """
        if hasattr(trade, "max_rate") and trade.max_rate > 0:
            max_gain       = trade.max_rate / trade.open_rate - 1
            trail_from_open = (1 + max_gain) * (1 - self.TRAILING_STOP_PCT) - 1
            return max(trail_from_open, self.stoploss)
        return self.stoploss
