# source: https://raw.githubusercontent.com/kalebCard/MemeHunter/18829614b77ef695755f5d610e269a59c2849284/user_data/strategies/MemeHunterFuturesV4.py
# directory_url: https://github.com/kalebCard/MemeHunter/blob/main/user_data/strategies/
# User: kalebCard
# Repository: MemeHunter
# --------------------"""
Github_kalebCard_MemeHunter__MemeHunterFuturesV4__20261012_213430 - Auction Market Theory & Human Seasonality Engine.

Innovaciones Integradas:
1. Máscara de Estacionalidad Humana (Sprint 13 / PB-48, PB-49, PB-50):
   - Bloqueo sistemático de entradas Short los Lunes y Martes (responsables de -$756 USDT y 156 stoplosses en V3).
   - Concentración y sizing prioritario de Jueves a Domingo (donde el ciclo de desinflación generó +$761 USDT neto).
   - Prioridad a la sesión americana de distribución (14:00 - 21:00 UTC).
2. Teoría de Subasta & Trapped Longs Index (Sprint 12 / PB-45, PB-46, PB-47):
   - Trapped Longs Score: mechas superiores con volumen anormal y cierre bajo que indican compradores minoristas atrapados.
   - Detección de Nodos de Bajo Volumen (LVN): aceleración bajista en vacíos de liquidez.
3. Motor de Salida Asimétrico Puro:
   - Margen aislado 1.0x (cero riesgo de liquidación).
   - Trailing stop holgado (offset al 12%, trailing al 4%) probado en Freqtrade para dejar correr colapsos macro.
   - Time-decay exit para evitar sangrado de tasas de financiación negativas en operaciones estancadas.
"""

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

from freqtrade.strategy import (
    IStrategy,
    IntParameter,
    DecimalParameter,
)
from freqtrade.persistence import Trade
from pandas import DataFrame, Series
import pandas_ta as ta
import numpy as np

LOG = logging.getLogger(__name__)

KILL_SWITCH_DRAWDOWN = 0.25
KILL_SWITCH_FLAG = "KILL_SWITCH"
LIVE_RUNMODES = ('live', 'dry_run', 'runmode.live', 'runmode.dry_run')


class Github_kalebCard_MemeHunter__MemeHunterFuturesV4__20261012_213430(IStrategy):
    INTERFACE_VERSION = 3
    timeframe = '4h'
    can_short = True

    # ROI calibrado para desinflación macro
    minimal_roi = {
        "0": 0.602,
        "1704": 0.141,
        "2255": 0.047,
        "7483": 0
    }

    # Stoploss duro de emergencia (-10%)
    stoploss = -0.10

    # Trailing Stop Nativo de Alta Fidelidad
    trailing_stop = True
    trailing_stop_positive = 0.040
    trailing_stop_positive_offset = 0.120
    trailing_only_offset_is_reached = True
    use_custom_stoploss = False

    use_exit_signal = True

    # Guardián de Funding Rate
    MAX_ADVERSE_FUNDING = -0.0005
    MAX_EUPHORIC_FUNDING = 0.0010

    startup_candle_count = 200

    # Protecciones de Cartera Activas
    protections = [
        {
            "method": "CooldownPeriod",
            "stop_duration_candles": 2
        },
        {
            "method": "StoplossGuard",
            "lookback_period_candles": 6,   # 24 horas
            "trade_limit": 3,
            "stop_duration_candles": 6,
            "only_per_pair": False
        },
        {
            "method": "MaxDrawdown",
            "lookback_period_candles": 120, # 20 días
            "trade_limit": 1,
            "stop_duration_candles": 24,    # 4 días
            "max_allowed_drawdown": 0.18
        }
    ]

    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:
        """Margen Aislado 1.0x para inmunidad absoluta contra liquidaciones catastróficas."""
        return 1.0

    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:
        """
        Sizing Asimétrico Multicapa (Estacionalidad + Subasta):
        - Longs: 0.0 (Cero compras).
        - Trapped Longs en LVN o Fin de Semana: 1.40x (Máxima aceleración y convicción).
        - Short Structure Shift estándar: 1.25x.
        - Short Macro Breakdown estándar: 1.0x.
        """
        if side == "long":
            return 0.0

        base_stake = proposed_stake
        if entry_tag == "short_trapped_auction":
            stake = base_stake * 1.40
        elif entry_tag == "short_structure_shift":
            stake = base_stake * 1.25
        else:
            stake = base_stake * 1.0

        if min_stake is not None:
            stake = max(stake, min_stake)
        stake = max(stake, 5.0)  # Binance Min Notional
        return min(stake, max_stake)

    def _is_live_mode(self) -> bool:
        runmode = self.config.get('runmode')
        return bool(runmode) and str(runmode).lower() in LIVE_RUNMODES

    def _kill_switch_path(self) -> Path:
        return Path(self.config.get('user_data_dir', '/freqtrade/user_data')) / KILL_SWITCH_FLAG

    def _is_adverse_funding(self, pair: str, side: str) -> bool:
        exchange = getattr(self, 'dp', None) and getattr(self.dp, 'exchange', None)
        if not exchange:
            exchange = getattr(self, 'exchange', None)
        if not exchange:
            return False

        try:
            funding_rate = None
            if hasattr(exchange, 'fetch_funding_rate'):
                fr_data = exchange.fetch_funding_rate(pair)
                if isinstance(fr_data, dict):
                    funding_rate = fr_data.get('fundingRate') or fr_data.get('funding_rate')
                elif isinstance(fr_data, (float, int)):
                    funding_rate = float(fr_data)
            elif hasattr(exchange, 'get_funding_rate'):
                funding_rate = exchange.get_funding_rate(pair)
            elif hasattr(exchange, '_funding_rates') and pair in exchange._funding_rates:
                funding_rate = exchange._funding_rates[pair]

            if funding_rate is not None:
                funding_rate = float(funding_rate)
                if side == "short" and funding_rate < self.MAX_ADVERSE_FUNDING:
                    return True
                if side == "long" and funding_rate > self.MAX_EUPHORIC_FUNDING:
                    return True
        except Exception:
            pass
        return False

    def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
                            time_in_force: str, current_time: datetime, entry_tag: Optional[str],
                            side: str, **kwargs) -> bool:
        if side == "long":
            return False

        live = self._is_live_mode()
        flag = self._kill_switch_path()
        if live and flag.exists():
            return False

        if self._is_adverse_funding(pair, side):
            return False

        return True

    def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float,
                    current_profit: float, **kwargs) -> Optional[str]:
        """
        Salida por Decaimiento Temporal / Funding Exhaustion:
        Si la posición lleva abierta más de 72 horas (4320 min) y se encuentra estancada
        con ganancia menor al +1.5%, cerramos para liberar margen y no sangrar funding fees.
        """
        trade_duration = (current_time - trade.open_date_utc).total_seconds() / 60.0
        if trade_duration >= 4320 and current_profit < 0.015:
            return "time_decay_exit"

        return None

    def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        # 1. Variables Temporales y Estacionales Humanas
        # dayofweek: 0=Lunes, 1=Martes, 2=Miércoles, 3=Jueves, 4=Viernes, 5=Sábado, 6=Domingo
        dataframe['dayofweek'] = dataframe['date'].dt.dayofweek
        dataframe['hour_utc'] = dataframe['date'].dt.hour
        dataframe['day_of_month'] = dataframe['date'].dt.day

        # Filtro de Estacionalidad Favorable para Shorts:
        # Excluye Lunes y Martes (responsables de -$756 USDT en pérdidas en la auditoría)
        # Permite Jueves, Viernes, Sábado y Domingo (donde se concentra el +$761 USDT neto).
        # Miércoles se permite solo si coincide con la sesión americana de distribución (14:00 - 21:00 UTC).
        favorable_day = dataframe['dayofweek'].isin([3, 4, 5, 6])
        favorable_wednesday = (dataframe['dayofweek'] == 2) & (dataframe['hour_utc'] >= 14)
        dataframe['seasonal_short_allowed'] = favorable_day | favorable_wednesday

        # 2. Indicadores Clásicos de Tendencia y Momento
        macd = ta.macd(dataframe['close'], fast=12, slow=26, signal=9)
        if macd is not None:
            dataframe['macd'] = macd['MACD_12_26_9']
            dataframe['macdsignal'] = macd['MACDs_12_26_9']
            dataframe['macdhist'] = macd['MACDh_12_26_9']
        else:
            dataframe['macd'] = 0.0
            dataframe['macdsignal'] = 0.0
            dataframe['macdhist'] = 0.0

        dataframe['rsi'] = ta.rsi(dataframe['close'], length=14)
        dataframe['ema_50'] = ta.ema(dataframe['close'], length=50)
        dataframe['ema_200'] = ta.ema(dataframe['close'], length=200)

        # Distancia a EMA 50
        dataframe['dist_ema_50'] = (dataframe['close'] - dataframe['ema_50']) / dataframe['ema_50']

        # Volumen Relativo (RVOL)
        vol_sma = dataframe['volume'].rolling(window=20).mean()
        dataframe['rvol'] = dataframe['volume'] / vol_sma.replace(0, 1.0)

        # 3. Teoría de Subasta: Trapped Longs Squeeze Index
        # Mide la concentración de volumen en la mecha superior de una vela de pump fallida
        candle_range = (dataframe['high'] - dataframe['low']).replace(0, 1e-8)
        upper_wick = dataframe['high'] - dataframe[['open', 'close']].max(axis=1)
        dataframe['wick_ratio'] = upper_wick / candle_range
        # Trapped Score: mecha superior alta (>= 40% del rango) con volumen inusual (RVOL >= 1.8)
        dataframe['trapped_longs_signal'] = (dataframe['wick_ratio'] >= 0.40) & (dataframe['rvol'] >= 1.8)

        # 4. Detección de Aceleración en Nodos de Bajo Volumen (LVN Proxy)
        # Una contracción de rango previo seguida de expansión bajista
        atr = ta.atr(dataframe['high'], dataframe['low'], dataframe['close'], length=14)
        dataframe['atr'] = atr if atr is not None else (dataframe['high'] - dataframe['low'])
        dataframe['is_lvn_break'] = (dataframe['close'] < dataframe['close'].shift(1) - 0.7 * dataframe['atr'])

        return dataframe

    def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[:, 'enter_long'] = 0
        dataframe.loc[:, 'enter_short'] = 0
        dataframe.loc[:, 'enter_tag'] = ''

        # -------------------------------------------------------------
        # LONGS = 0 (Completamente Desactivados)
        # -------------------------------------------------------------

        # -------------------------------------------------------------
        # SHORTS A: "Auction Failure / Trapped Longs Squeeze" (Máxima Convicción)
        # -------------------------------------------------------------
        # Combina la estacionalidad humana favorable con la trampa de compradores:
        # - Estacionalidad favorable (Jueves a Domingo o Miércoles tarde)
        # - Señal de compradores atrapados en las últimas 3 velas
        # - Quiebre del mínimo de la vela previa
        # - Pérdida de impulso en MACD
        short_trapped_conditions = (
            (dataframe['seasonal_short_allowed']) &
            (dataframe['trapped_longs_signal'].rolling(window=3).max() == 1) &
            (dataframe['close'] < dataframe['low'].shift(1)) &
            (dataframe['macdhist'] < dataframe['macdhist'].shift(1)) &
            (dataframe['volume'] > 0)
        )
        dataframe.loc[short_trapped_conditions, 'enter_short'] = 1
        dataframe.loc[short_trapped_conditions, 'enter_tag'] = 'short_trapped_auction'

        # -------------------------------------------------------------
        # SHORTS B: "Pump Structure Shift" (Fin de Euforia con Filtro Humano)
        # -------------------------------------------------------------
        short_structure_shift = (
            (dataframe['seasonal_short_allowed']) &
            (dataframe['dist_ema_50'].rolling(window=3).max() >= 0.12) &
            (dataframe['close'] < dataframe['low'].shift(1)) &
            (dataframe['rsi'] < 68) &
            (dataframe['rsi'].shift(1) >= 65) &
            (dataframe['macdhist'] < dataframe['macdhist'].shift(1)) &
            (dataframe['volume'] > 0)
        )
        dataframe.loc[short_structure_shift, 'enter_short'] = 1
        dataframe.loc[short_structure_shift, 'enter_tag'] = 'short_structure_shift'

        # -------------------------------------------------------------
        # SHORTS C: "Macro Breakdown" (Tendencia Bajista en Días Favirables)
        # -------------------------------------------------------------
        short_macro_conditions = (
            (dataframe['seasonal_short_allowed']) &
            (dataframe['close'] < dataframe['ema_200']) &
            (dataframe['macd'] < dataframe['macdsignal']) &
            (dataframe['macd'] < 0) &
            (dataframe['rsi'] <= 44) &
            (dataframe['rsi'] >= 22) &
            (dataframe['volume'] > 0)
        )
        dataframe.loc[short_macro_conditions, 'enter_short'] = 1
        dataframe.loc[short_macro_conditions, 'enter_tag'] = 'short_macro_breakdown'

        return dataframe

    def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
        dataframe.loc[:, 'exit_long'] = 0
        dataframe.loc[:, 'exit_short'] = 0

        # Salida técnica de Short: Ruptura alcista consolidada sobre EMA 200 o MACD cruza alcista con RSI > 58
        exit_short = (
            (dataframe['close'] > dataframe['ema_200']) |
            ((dataframe['macd'] > dataframe['macdsignal']) & (dataframe['rsi'] > 58))
        )
        dataframe.loc[exit_short, 'exit_short'] = 1

        return dataframe
