""" 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