131 lines
3.7 KiB
Python
131 lines
3.7 KiB
Python
#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
|