Cycles/cycles/strategies/default_strategy.py

349 lines
14 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, List
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.
The strategy works best on 15-minute timeframes but can be configured for other timeframes.
Parameters:
stop_loss_pct (float): Stop loss percentage (default: 0.03)
timeframe (str): Preferred timeframe for analysis (default: "15min")
Example:
strategy = DefaultStrategy(weight=1.0, params={"stop_loss_pct": 0.05, "timeframe": "15min"})
"""
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 and timeframe
"""
super().__init__("default", weight, params)
def get_timeframes(self) -> List[str]:
"""
Get the timeframes required by the default strategy.
The default strategy works on a single timeframe (typically 15min)
but also needs 1min data for precise stop-loss execution.
Returns:
List[str]: List containing primary timeframe and 1min for stop-loss
"""
primary_timeframe = self.params.get("timeframe", "15min")
# Always include 1min for stop-loss precision, avoid duplicates
timeframes = [primary_timeframe]
if primary_timeframe != "1min":
timeframes.append("1min")
return timeframes
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
"""
try:
import threading
import time
from cycles.Analysis.supertrend import Supertrends
# First, resample the original 1-minute data to required timeframes
self._resample_data(backtester.original_df)
# Get the primary timeframe data for strategy calculations
primary_timeframe = self.get_timeframes()[0]
strategy_data = self.get_data_for_timeframe(primary_timeframe)
if strategy_data is None or len(strategy_data) < 50:
# Not enough data for reliable Supertrend calculation
self.meta_trend = np.zeros(len(strategy_data) if strategy_data is not None else 1)
self.stop_loss_pct = self.params.get("stop_loss_pct", 0.03)
self.primary_timeframe = primary_timeframe
self.initialized = True
print(f"DefaultStrategy: Insufficient data ({len(strategy_data) if strategy_data is not None else 0} points), using fallback")
return
# Limit data size to prevent excessive computation time
# original_length = len(strategy_data)
# if len(strategy_data) > 200:
# strategy_data = strategy_data.tail(200)
# print(f"DefaultStrategy: Limited data from {original_length} to {len(strategy_data)} points for faster computation")
# Use a timeout mechanism for Supertrend calculation
result_container = {}
exception_container = {}
def calculate_supertrend():
try:
# Calculate Supertrend indicators on the primary timeframe
supertrends = Supertrends(strategy_data, verbose=False)
supertrend_results_list = supertrends.calculate_supertrend_indicators()
result_container['supertrend_results'] = supertrend_results_list
except Exception as e:
exception_container['error'] = e
# Run Supertrend calculation in a separate thread with timeout
calc_thread = threading.Thread(target=calculate_supertrend)
calc_thread.daemon = True
calc_thread.start()
# Wait for calculation with timeout
calc_thread.join(timeout=15.0) # 15 second timeout
if calc_thread.is_alive():
# Calculation timed out
print(f"DefaultStrategy: Supertrend calculation timed out, using fallback")
self.meta_trend = np.zeros(len(strategy_data))
self.stop_loss_pct = self.params.get("stop_loss_pct", 0.03)
self.primary_timeframe = primary_timeframe
self.initialized = True
return
if 'error' in exception_container:
# Calculation failed
raise exception_container['error']
if 'supertrend_results' not in result_container:
# No result returned
print(f"DefaultStrategy: No Supertrend results, using fallback")
self.meta_trend = np.zeros(len(strategy_data))
self.stop_loss_pct = self.params.get("stop_loss_pct", 0.03)
self.primary_timeframe = primary_timeframe
self.initialized = True
return
# Process successful results
supertrend_results_list = result_container['supertrend_results']
# 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 data internally instead of relying on backtester.strategies
self.meta_trend = meta_trend
self.stop_loss_pct = self.params.get("stop_loss_pct", 0.03)
self.primary_timeframe = primary_timeframe
# Also store in backtester if it has strategies attribute (for compatibility)
if hasattr(backtester, 'strategies'):
if not isinstance(backtester.strategies, dict):
backtester.strategies = {}
backtester.strategies["meta_trend"] = meta_trend
backtester.strategies["stop_loss_pct"] = self.stop_loss_pct
backtester.strategies["primary_timeframe"] = primary_timeframe
self.initialized = True
print(f"DefaultStrategy: Successfully initialized with {len(meta_trend)} data points")
except Exception as e:
# Handle any other errors gracefully
print(f"DefaultStrategy initialization failed: {e}")
primary_timeframe = self.get_timeframes()[0]
strategy_data = self.get_data_for_timeframe(primary_timeframe)
data_length = len(strategy_data) if strategy_data is not None else 1
# Create a simple fallback
self.meta_trend = np.zeros(data_length)
self.stop_loss_pct = self.params.get("stop_loss_pct", 0.03)
self.primary_timeframe = primary_timeframe
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 primary timeframe 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 bounds
if not hasattr(self, 'meta_trend') or df_index >= len(self.meta_trend):
return StrategySignal("HOLD", 0.0)
# Check for meta-trend entry condition
prev_trend = self.meta_trend[df_index - 1]
curr_trend = self.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 primary timeframe 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 bounds
if not hasattr(self, 'meta_trend') or df_index >= len(self.meta_trend):
return StrategySignal("HOLD", 0.0)
# Check for meta-trend exit signal
prev_trend = self.meta_trend[df_index - 1]
curr_trend = self.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 using 1-minute data for precision
# Note: Stop loss checking requires active trade context which may not be available in StrategyTrader
# For now, skip stop loss checking in signal generation
# 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 primary timeframe dataframe
Returns:
float: Confidence level (0.0 to 1.0)
"""
if not self.initialized:
return 0.0
# Check bounds
if not hasattr(self, 'meta_trend') or df_index >= len(self.meta_trend):
return 0.0
curr_trend = self.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.
Uses 1-minute data for precise stop loss checking regardless of
the primary timeframe used for strategy signals.
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 - self.stop_loss_pct)
# Use 1-minute data for precise stop loss checking
min1_data = self.get_data_for_timeframe("1min")
if min1_data is None:
# Fallback to original_df if 1min timeframe not available
min1_data = backtester.original_df if hasattr(backtester, 'original_df') else backtester.min1_df
min1_index = min1_data.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_data.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