""" MemeHunterFuturesV5_Flat1x - Versión para prueba de sensibilidad de apalancamiento (PB-66). Fija el apalancamiento en 1.0x plano (sin apalancamiento adicional ni sizing asimétrico) para medir la robustez de la frontera eficiente del motor base sobre la muestra completa. """ from datetime import datetime from typing import Optional import logging from freqtrade.strategy import IStrategy from freqtrade.persistence import Trade from pandas import DataFrame import pandas_ta as ta import numpy as np LOG = logging.getLogger(__name__) class MemeHunterFuturesV5_Flat1x(IStrategy): INTERFACE_VERSION = 3 timeframe = '4h' can_short = True minimal_roi = { "0": 0.602, "1704": 0.141, "2255": 0.047, "7483": 0 } stoploss = -0.10 trailing_stop = True trailing_stop_positive = 0.040 trailing_stop_positive_offset = 0.120 trailing_only_offset_is_reached = True process_only_new_candles = True startup_candle_count: int = 200 order_types = { 'entry': 'limit', 'exit': 'limit', 'stoploss': 'limit', 'stoploss_on_exchange': False, 'stoploss_on_exchange_interval': 60 } order_time_in_force = { 'entry': 'GTC', 'exit': 'GTC' } 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: # PB-66: Apalancamiento plano 1.0x estricto return 1.0 def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe['ema_50'] = ta.ema(dataframe['close'], length=50) dataframe['ema_200'] = ta.ema(dataframe['close'], length=200) dataframe['dist_ema_50'] = (dataframe['close'] - dataframe['ema_50']) / dataframe['ema_50'] dataframe['rsi'] = ta.rsi(dataframe['close'], length=14) macd = ta.macd(dataframe['close'], fast=12, slow=26, signal=9) dataframe['macd'] = macd['MACD_12_26_9'] dataframe['macdsignal'] = macd['MACDs_12_26_9'] dataframe['macdhist'] = macd['MACDh_12_26_9'] dataframe['atr'] = ta.atr(dataframe['high'], dataframe['low'], dataframe['close'], length=14) upper_wick = dataframe['high'] - dataframe[['close', 'open']].max(axis=1) candle_body = (dataframe['close'] - dataframe['open']).abs() candle_range = dataframe['high'] - dataframe['low'] vol_sma20 = ta.sma(dataframe['volume'], length=20) vol_ratio = dataframe['volume'] / vol_sma20.replace(0, np.nan) is_wick_dominant = (upper_wick >= candle_body * 1.8) & (upper_wick >= candle_range * 0.45) is_volume_spike = vol_ratio >= 1.4 is_closing_weak = dataframe['close'] <= (dataframe['low'] + candle_range * 0.35) dataframe['trapped_longs_signal'] = (is_wick_dominant & is_volume_spike & is_closing_weak).astype(int) # Sin filtro de calendario para mantener consistencia con PB-61 dataframe['seasonal_short_allowed'] = True return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[:, 'enter_long'] = 0 dataframe.loc[:, 'enter_short'] = 0 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' 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' 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 exit_short = ( (dataframe['close'] > dataframe['ema_200']) | ((dataframe['macd'] > dataframe['macdsignal']) & (dataframe['rsi'] > 58)) ) dataframe.loc[exit_short, 'exit_short'] = 1 return dataframe