Files
lowkey_backtest/strategies/supertrend_pkg/strategy.py

143 lines
5.1 KiB
Python

"""
Meta Supertrend strategy implementation.
"""
import numpy as np
import pandas as pd
from engine.market import MarketType
from strategies.base import BaseStrategy
from .indicators import SuperTrendIndicator
class MetaSupertrendStrategy(BaseStrategy):
"""
Meta Supertrend Strategy using 3 Supertrend indicators.
Enters long when all 3 Supertrends are bullish.
Enters short when all 3 Supertrends are bearish.
Designed for perpetual futures with leverage and short-selling support.
"""
# Market configuration
default_market_type = MarketType.PERPETUAL
default_leverage = 5
# Risk management parameters
default_sl_stop = 0.02 # 2% stop loss
default_sl_trail = True # Trailing stop enabled
default_exit_on_bearish_flip = False # Rely on SL/TP, not bearish flip
def run(
self,
close: pd.Series,
high: pd.Series = None,
low: pd.Series = None,
period1: int = 10,
multiplier1: float = 3.0,
period2: int = 11,
multiplier2: float = 2.0,
period3: int = 12,
multiplier3: float = 1.0,
exit_on_bearish_flip: bool = None,
enable_short: bool = True,
**kwargs
) -> tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame, pd.DataFrame]:
# 1. Validation & Setup
if exit_on_bearish_flip is None:
exit_on_bearish_flip = self.default_exit_on_bearish_flip
if high is None or low is None:
raise ValueError("MetaSupertrendStrategy requires High and Low prices.")
# 2. Calculate Supertrends
t1, t2, t3 = self._calculate_supertrends(
high, low, close,
period1, multiplier1,
period2, multiplier2,
period3, multiplier3
)
# 3. Meta Signals
bullish, bearish = self._calculate_meta_signals(t1, t2, t3, close)
# 4. Generate Entry/Exit Signals
return self._generate_signals(bullish, bearish, exit_on_bearish_flip, enable_short)
def _calculate_supertrends(
self, high, low, close, p1, m1, p2, m2, p3, m3
):
"""Run the 3 Supertrend indicators."""
# Pass NumPy arrays explicitly to avoid Numba typing errors
h_vals = high.values
l_vals = low.values
c_vals = close.values
def run_st(p, m):
st = SuperTrendIndicator.run(h_vals, l_vals, c_vals, period=p, multiplier=m)
trend = st.trend
if isinstance(trend, pd.DataFrame):
trend.index = close.index
if trend.shape[1] == 1:
trend = trend.iloc[:, 0]
elif isinstance(trend, pd.Series):
trend.index = close.index
return trend
t1 = run_st(p1, m1)
t2 = run_st(p2, m2)
t3 = run_st(p3, m3)
return t1, t2, t3
def _calculate_meta_signals(self, t1, t2, t3, close_series):
"""Combine 3 Supertrends into boolean Bullish/Bearish signals."""
# Use NumPy broadcasting
t1_vals = t1.values if isinstance(t1, pd.DataFrame) else t1.values.reshape(-1, 1)
# Force column vectors for broadcasting if scalar result
t2_vals = t2.values.reshape(-1, 1)
t3_vals = t3.values.reshape(-1, 1)
# Boolean logic on numpy arrays (1 = Bull, -1 = Bear)
bullish_vals = (t1_vals == 1) & (t2_vals == 1) & (t3_vals == 1)
bearish_vals = (t1_vals == -1) & (t2_vals == -1) & (t3_vals == -1)
# Reconstruct Pandas objects
if isinstance(t1, pd.DataFrame):
bullish = pd.DataFrame(bullish_vals, index=t1.index, columns=t1.columns)
bearish = pd.DataFrame(bearish_vals, index=t1.index, columns=t1.columns)
else:
bullish = pd.Series(bullish_vals.flatten(), index=t1.index)
bearish = pd.Series(bearish_vals.flatten(), index=t1.index)
return bullish, bearish
def _generate_signals(
self, bullish, bearish, exit_on_bearish_flip, enable_short
):
"""Generate long/short entry/exit signals based on meta trend."""
# Long Entries: Change from Not Bullish to Bullish
prev_bullish = bullish.shift(1).fillna(False)
long_entries = bullish & (~prev_bullish)
# Long Exits
if exit_on_bearish_flip:
prev_bearish = bearish.shift(1).fillna(False)
long_exits = bearish & (~prev_bearish)
else:
long_exits = BaseStrategy.create_empty_signals(long_entries)
# Short signals
if enable_short:
prev_bearish = bearish.shift(1).fillna(False)
short_entries = bearish & (~prev_bearish)
if exit_on_bearish_flip:
short_exits = bullish & (~prev_bullish)
else:
short_exits = BaseStrategy.create_empty_signals(long_entries)
else:
short_entries = BaseStrategy.create_empty_signals(long_entries)
short_exits = BaseStrategy.create_empty_signals(long_entries)
return long_entries, long_exits, short_entries, short_exits