#src/strategies/donchian_breakout.py """ Estrategia de breakout de rango Donchian. """ from __future__ import annotations import pandas as pd from ..core.strategy import Strategy, Signal, calculate_donchian_channels, cross_above, cross_below class DonchianBreakout(Strategy): """ Breakout de máximos/mínimos de N barras. Señales: - BUY: close rompe por encima del máximo Donchian previo - SELL: close rompe por debajo del mínimo Donchian previo """ strategy_id = "donchian_breakout" strategy_family = "breakout" display_name = "Donchian Breakout" description = "Breakout de rango basado en máximos y mínimos rolling." def __init__(self, donchian_window: int = 20, exit_window: int = 10): params = { "donchian_window": donchian_window, "exit_window": exit_window, } super().__init__(name="Donchian Breakout", params=params) self.donchian_window = int(donchian_window) self.exit_window = int(exit_window) @classmethod def default_parameters(cls) -> dict: return { "donchian_window": 20, "exit_window": 10, } @classmethod def strategy_metadata(cls) -> dict: return { "strategy_id": cls.strategy_id, "name": cls.display_name, "family": cls.strategy_family, "direction": "long_short", "description": cls.description, } @classmethod def strategy_definition(cls) -> dict: return { "meta": cls.strategy_metadata(), "defaults": cls.default_parameters(), "parameters_schema": cls.parameters_schema(), } @classmethod def parameters_schema(cls) -> dict: return { "donchian_window": { "type": "int", "min": 2, "max": 300, "default": 20, }, "exit_window": { "type": "int", "min": 2, "max": 300, "default": 10, }, } def init_indicators(self, data: pd.DataFrame) -> pd.DataFrame: high = data["high"] low = data["low"] close = data["close"] data["donchian_high"], data["donchian_low"] = calculate_donchian_channels( high, low, period=self.donchian_window, shift=1, ) data["exit_high"], data["exit_low"] = calculate_donchian_channels( high, low, period=self.exit_window, shift=1, ) data["breakout_up"] = cross_above(close, data["donchian_high"]) data["breakout_down"] = cross_below(close, data["donchian_low"]) data["lose_exit_low"] = cross_below(close, data["exit_low"]) data["lose_exit_high"] = cross_above(close, data["exit_high"]) return data def generate_signal(self, idx: int) -> Signal: if self.data is None: raise ValueError("Data no establecida") if idx < 1: return Signal.HOLD row = self.data.iloc[idx] needed = ["donchian_high", "donchian_low", "exit_high", "exit_low"] if any(pd.isna(row[c]) for c in needed): return Signal.HOLD if bool(row["breakout_up"]) and self.current_position <= 0: return Signal.BUY if bool(row["breakout_down"]) and self.current_position >= 0: return Signal.SELL if self.current_position > 0 and bool(row["lose_exit_low"]): return Signal.SELL if self.current_position < 0 and bool(row["lose_exit_high"]): return Signal.BUY return Signal.HOLD