Cycles/cycles/strategies/default_strategy.py
Vasily.onl 235098c045 Add strategy management system with multiple trading strategies
- Introduced a new strategies module containing the StrategyManager class to orchestrate multiple trading strategies.
- Implemented StrategyBase and StrategySignal as foundational components for strategy development.
- Added DefaultStrategy for meta-trend analysis and BBRSStrategy for Bollinger Bands + RSI trading.
- Enhanced documentation to provide clear usage examples and configuration guidelines for the new system.
- Established a modular architecture to support future strategy additions and improvements.
2025-05-23 16:41:08 +08:00

219 lines
8.2 KiB
Python

"""
Default Meta-Trend Strategy
This module implements the default trading strategy based on meta-trend analysis
using multiple Supertrend indicators. The strategy enters when trends align
and exits on trend reversal or stop loss.
The meta-trend is calculated by comparing three Supertrend indicators:
- Entry: When meta-trend changes from != 1 to == 1
- Exit: When meta-trend changes to -1 or stop loss is triggered
"""
import numpy as np
from typing import Tuple, Optional
from .base import StrategyBase, StrategySignal
class DefaultStrategy(StrategyBase):
"""
Default meta-trend strategy implementation.
This strategy uses multiple Supertrend indicators to determine market direction.
It generates entry signals when all three Supertrend indicators align in an
upward direction, and exit signals when they reverse or stop loss is triggered.
Parameters:
stop_loss_pct (float): Stop loss percentage (default: 0.03)
Example:
strategy = DefaultStrategy(weight=1.0, params={"stop_loss_pct": 0.05})
"""
def __init__(self, weight: float = 1.0, params: Optional[dict] = None):
"""
Initialize the default strategy.
Args:
weight: Strategy weight for combination (default: 1.0)
params: Strategy parameters including stop_loss_pct
"""
super().__init__("default", weight, params)
def initialize(self, backtester) -> None:
"""
Initialize meta trend calculation using Supertrend indicators.
Calculates the meta-trend by comparing three Supertrend indicators.
When all three agree on direction, meta-trend follows that direction.
Otherwise, meta-trend is neutral (0).
Args:
backtester: Backtest instance with OHLCV data
"""
from cycles.Analysis.supertrend import Supertrends
# Calculate Supertrend indicators
supertrends = Supertrends(backtester.df, verbose=False)
supertrend_results_list = supertrends.calculate_supertrend_indicators()
# Extract trend arrays from each Supertrend
trends = [st['results']['trend'] for st in supertrend_results_list]
trends_arr = np.stack(trends, axis=1)
# Calculate meta-trend: all three must agree for direction signal
meta_trend = np.where(
(trends_arr[:,0] == trends_arr[:,1]) & (trends_arr[:,1] == trends_arr[:,2]),
trends_arr[:,0],
0 # Neutral when trends don't agree
)
# Store in backtester for access during trading
backtester.strategies["meta_trend"] = meta_trend
backtester.strategies["stop_loss_pct"] = self.params.get("stop_loss_pct", 0.03)
self.initialized = True
def get_entry_signal(self, backtester, df_index: int) -> StrategySignal:
"""
Generate entry signal based on meta-trend direction change.
Entry occurs when meta-trend changes from != 1 to == 1, indicating
all Supertrend indicators now agree on upward direction.
Args:
backtester: Backtest instance with current state
df_index: Current index in the dataframe
Returns:
StrategySignal: Entry signal if trend aligns, hold signal otherwise
"""
if not self.initialized:
return StrategySignal("HOLD", 0.0)
if df_index < 1:
return StrategySignal("HOLD", 0.0)
# Check for meta-trend entry condition
prev_trend = backtester.strategies["meta_trend"][df_index - 1]
curr_trend = backtester.strategies["meta_trend"][df_index]
if prev_trend != 1 and curr_trend == 1:
# Strong confidence when all indicators align for entry
return StrategySignal("ENTRY", confidence=1.0)
return StrategySignal("HOLD", confidence=0.0)
def get_exit_signal(self, backtester, df_index: int) -> StrategySignal:
"""
Generate exit signal based on meta-trend reversal or stop loss.
Exit occurs when:
1. Meta-trend changes to -1 (trend reversal)
2. Stop loss is triggered based on price movement
Args:
backtester: Backtest instance with current state
df_index: Current index in the dataframe
Returns:
StrategySignal: Exit signal with type and price, or hold signal
"""
if not self.initialized:
return StrategySignal("HOLD", 0.0)
if df_index < 1:
return StrategySignal("HOLD", 0.0)
# Check for meta-trend exit signal
prev_trend = backtester.strategies["meta_trend"][df_index - 1]
curr_trend = backtester.strategies["meta_trend"][df_index]
if prev_trend != 1 and curr_trend == -1:
return StrategySignal("EXIT", confidence=1.0,
metadata={"type": "META_TREND_EXIT_SIGNAL"})
# Check for stop loss
stop_loss_result, sell_price = self._check_stop_loss(backtester)
if stop_loss_result:
return StrategySignal("EXIT", confidence=1.0, price=sell_price,
metadata={"type": "STOP_LOSS"})
return StrategySignal("HOLD", confidence=0.0)
def get_confidence(self, backtester, df_index: int) -> float:
"""
Get strategy confidence based on meta-trend strength.
Higher confidence when meta-trend is strongly directional,
lower confidence during neutral periods.
Args:
backtester: Backtest instance with current state
df_index: Current index in the dataframe
Returns:
float: Confidence level (0.0 to 1.0)
"""
if not self.initialized or df_index >= len(backtester.strategies["meta_trend"]):
return 0.0
curr_trend = backtester.strategies["meta_trend"][df_index]
# High confidence for strong directional signals
if curr_trend == 1 or curr_trend == -1:
return 1.0
# Low confidence for neutral trend
return 0.3
def _check_stop_loss(self, backtester) -> Tuple[bool, Optional[float]]:
"""
Check if stop loss is triggered based on price movement.
Calculates stop loss price and checks if any candle since entry
has triggered the stop loss condition.
Args:
backtester: Backtest instance with current trade state
Returns:
Tuple[bool, Optional[float]]: (stop_loss_triggered, sell_price)
"""
# Calculate stop loss price
stop_price = backtester.entry_price * (1 - backtester.strategies["stop_loss_pct"])
# Get minute-level data for precise stop loss checking
min1_df = backtester.original_df if hasattr(backtester, 'original_df') else backtester.min1_df
min1_index = min1_df.index
# Find data range from entry to current time
start_candidates = min1_index[min1_index >= backtester.entry_time]
if len(start_candidates) == 0:
return False, None
backtester.current_trade_min1_start_idx = start_candidates[0]
end_candidates = min1_index[min1_index <= backtester.current_date]
if len(end_candidates) == 0:
return False, None
backtester.current_min1_end_idx = end_candidates[-1]
# Check if any candle in the range triggered stop loss
min1_slice = min1_df.loc[backtester.current_trade_min1_start_idx:backtester.current_min1_end_idx]
if (min1_slice['low'] <= stop_price).any():
# Find the first candle that triggered stop loss
stop_candle = min1_slice[min1_slice['low'] <= stop_price].iloc[0]
# Use open price if it gapped below stop, otherwise use stop price
if stop_candle['open'] < stop_price:
sell_price = stop_candle['open']
else:
sell_price = stop_price
return True, sell_price
return False, None