# source: https://raw.githubusercontent.com/saifbelaarbi/trader-google-c/97f35ead8ee505df554ce720791a1417bf93539e/ftbot/strategies/ClaudeDayTrader.py
"""
Github_saifbelaarbi_trader_google_c__ClaudeDayTrader__20260625_021644 — V-next, synthesized from the 6-way fast-day-trading bake-off.

Bake-off result (15m candidates, 2024-06 → 2026-06, same data + 0.06% taker fee):
  ClaudeDonchian15m  PF 0.95   (4293 trades)  ← best; breakout edge, killed only by fees
  ClaudeRSI2         PF 0.73   (10331 trades) — fee death (over-trading)
  ClaudeVWAP         PF 0.69   (8255 trades)  — fee death (over-trading)
  ClaudeMomoScalp    PF 0.22   (2033 trades)  — broken edge
  ClaudeORB          ~0 trades — n/a
  ClaudeSqueeze      bug (not scored)

Lesson confirmed for the 3rd time: 15m trading loses to ~0.12% round-trip fees.
The ONLY edge that works in this repo is the Donchian breakout — at 4h it returns
+38.7% (PF 1.14, ClaudeBreakout). ClaudeDonchian15m showed that same breakout edge
is real even at 15m (PF 0.95) and fails ONLY on trade frequency / fee drag.

Synthesis: run the proven breakout edge at 1h. Fast enough to be intraday — entries
break a 20-bar (≈20h) channel and exit on a 10-bar (≈10h) reversal, so holds are
hours, not the 4h version's multi-day swings — but ~4× fewer trades than 15m, so the
gross breakout edge clears fees. 4h EMA200 macro filter (only trade with the higher-
timeframe trend); 2×ATR disaster stop; Donchian channel trails winners; a volatility
floor skips low-ATR chop where fees dominate.

This is the same machinery as ClaudeBreakout, moved from 4h → 1h with a 4h trend
filter — a deliberate timeframe step between the dead 15m candidates and the winning
4h swing, targeting the "fast money that still clears fees" sweet spot.
"""

from datetime import datetime
from typing import Optional

import talib.abstract as ta
from freqtrade.persistence import Trade
from freqtrade.strategy import IStrategy, informative, stoploss_from_open
from pandas import DataFrame


class Github_saifbelaarbi_trader_google_c__ClaudeDayTrader__20260625_021644(IStrategy):
    INTERFACE_VERSION = 3

    timeframe = "1h"
    can_short = True

    # Hard failsafe only — the real stop is ATR-based in custom_stoploss.
    stoploss = -0.20
    use_custom_stoploss = True

    minimal_roi = {"0": 100}

    process_only_new_candles = True
    startup_candle_count = 850  # 4h EMA200 needs ~200×4h = 800×1h of warm-up

    entry_window = 20  # ~20h channel — the breakout trigger
    exit_window = 10  # ~10h channel — the trailing exit (winners run intraday)
    atr_floor = 0.004  # skip chop where the move can't clear fees

    max_stake_usdt = 40.0
    wallet_fraction = 0.20

    @property
    def protections(self):
        return [
            {"method": "CooldownPeriod", "stop_duration_candles": 2},
            {
                "method": "StoplossGuard",
                "lookback_period_candles": 168,  # 1 week of 1h candles
                "trade_limit": 4,
                "stop_duration_candles": 24,
                "only_per_pair": False,
            },
            {
                "method": "MaxDrawdown",
                "lookback_period_candles": 720,  # 30 days of 1h candles
                "trade_limit": 6,
                "stop_duration_candles": 48,
                "max_allowed_drawdown": 0.15,
            },
        ]

    @informative("4h")
    def populate_indicators_4h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
        return dataframe

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
        dataframe["atr_pct"] = dataframe["atr"] / dataframe["close"]
        # Donchian channels, shifted so the current candle can break the prior window.
        dataframe["dc_entry_high"] = dataframe["high"].rolling(self.entry_window).max().shift(1)
        dataframe["dc_entry_low"] = dataframe["low"].rolling(self.entry_window).min().shift(1)
        dataframe["dc_exit_high"] = dataframe["high"].rolling(self.exit_window).max().shift(1)
        dataframe["dc_exit_low"] = dataframe["low"].rolling(self.exit_window).min().shift(1)
        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        long_mask = (
            (dataframe["close"] > dataframe["dc_entry_high"])
            & (dataframe["close"] > dataframe["ema200_4h"])
            & (dataframe["atr_pct"] > self.atr_floor)
            & (dataframe["volume"] > 0)
        )
        short_mask = (
            (dataframe["close"] < dataframe["dc_entry_low"])
            & (dataframe["close"] < dataframe["ema200_4h"])
            & (dataframe["atr_pct"] > self.atr_floor)
            & (dataframe["volume"] > 0)
        )
        dataframe.loc[long_mask, "enter_long"] = 1
        dataframe.loc[long_mask, "enter_tag"] = "breakout_long"
        dataframe.loc[short_mask, "enter_short"] = 1
        dataframe.loc[short_mask, "enter_tag"] = "breakout_short"
        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # Turtle exit: opposite 10-bar extreme — trails winners through the move.
        dataframe.loc[dataframe["close"] < dataframe["dc_exit_low"], "exit_long"] = 1
        dataframe.loc[dataframe["close"] > dataframe["dc_exit_high"], "exit_short"] = 1
        return dataframe

    def custom_stake_amount(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_stake: float,
        min_stake: Optional[float],
        max_stake: float,
        leverage: float,
        entry_tag: Optional[str],
        side: str,
        **kwargs,
    ) -> float:
        total_equity = self.wallets.get_total_stake_amount() if self.wallets else 200.0
        stake = min(self.max_stake_usdt, total_equity * self.wallet_fraction)
        if min_stake:
            stake = max(stake, min_stake)
        return min(stake, max_stake)

    def leverage(
        self,
        pair: str,
        current_time: datetime,
        current_rate: float,
        proposed_leverage: float,
        max_leverage: float,
        entry_tag: Optional[str],
        side: str,
        **kwargs,
    ) -> float:
        return 1.0

    def custom_stoploss(
        self,
        pair: str,
        trade: Trade,
        current_time: datetime,
        current_rate: float,
        current_profit: float,
        after_fill: bool,
        **kwargs,
    ) -> Optional[float]:
        atr_pct = self._last_candle_value(pair, "atr_pct") or 0.02
        atr_pct = max(atr_pct, 0.005)
        # Fixed 2×ATR disaster stop; the Donchian exit manages winners.
        return stoploss_from_open(
            -2 * atr_pct, current_profit, is_short=trade.is_short, leverage=trade.leverage
        )

    def _last_candle_value(self, pair: str, column: str):
        dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
        if dataframe is None or dataframe.empty or column not in dataframe.columns:
            return None
        value = dataframe[column].iloc[-1]
        try:
            return float(value)
        except (TypeError, ValueError):
            return None
