""" Incremental MetaTrend Strategy This module implements an incremental version of the DefaultStrategy that processes real-time data efficiently while producing identical meta-trend signals to the original batch-processing implementation. The strategy uses 3 Supertrend indicators with parameters: - Supertrend 1: period=12, multiplier=3.0 - Supertrend 2: period=10, multiplier=1.0 - Supertrend 3: period=11, multiplier=2.0 Meta-trend calculation: - Meta-trend = 1 when all 3 Supertrends agree on uptrend - Meta-trend = -1 when all 3 Supertrends agree on downtrend - Meta-trend = 0 when Supertrends disagree (neutral) Signal generation: - Entry: meta-trend changes from != 1 to == 1 - Exit: meta-trend changes from != -1 to == -1 Stop-loss handling is delegated to the trader layer. """ import pandas as pd import numpy as np from typing import Dict, Optional, List, Any import logging from .base import IncStrategyBase, IncStrategySignal from .indicators.supertrend import SupertrendCollection logger = logging.getLogger(__name__) class MetaTrendStrategy(IncStrategyBase): """ Incremental MetaTrend strategy implementation. This strategy uses multiple Supertrend indicators to determine market direction and generates entry/exit signals based on meta-trend changes. It processes data incrementally for real-time performance while maintaining mathematical equivalence to the original DefaultStrategy. The strategy is designed to work with any timeframe but defaults to the timeframe specified in parameters (or 15min if not specified). Parameters: timeframe (str): Primary timeframe for analysis (default: "15min") buffer_size_multiplier (float): Buffer size multiplier for memory management (default: 2.0) enable_logging (bool): Enable detailed logging (default: False) Example: strategy = MetaTrendStrategy("metatrend", weight=1.0, params={ "timeframe": "15min", "enable_logging": True }) """ def __init__(self, name: str = "metatrend", weight: float = 1.0, params: Optional[Dict] = None): """ Initialize the incremental MetaTrend strategy. Args: name: Strategy name/identifier weight: Strategy weight for combination (default: 1.0) params: Strategy parameters - timeframe: Primary timeframe for analysis (default: "15min") - enable_logging: Enable detailed logging (default: False) - supertrend_periods: List of periods for Supertrend indicators (default: [12, 10, 11]) - supertrend_multipliers: List of multipliers for Supertrend indicators (default: [3.0, 1.0, 2.0]) - min_trend_agreement: Minimum fraction of indicators that must agree (default: 1.0, meaning all) """ super().__init__(name, weight, params) # Strategy configuration - now handled by base class timeframe aggregation self.primary_timeframe = self.params.get("timeframe", "15min") self.enable_logging = self.params.get("enable_logging", False) # Configure logging level if self.enable_logging: logger.setLevel(logging.DEBUG) # Get configurable Supertrend parameters from params or use defaults default_periods = [12, 10, 11] default_multipliers = [3.0, 1.0, 2.0] supertrend_periods = self.params.get("supertrend_periods", default_periods) supertrend_multipliers = self.params.get("supertrend_multipliers", default_multipliers) # Validate parameters if len(supertrend_periods) != len(supertrend_multipliers): raise ValueError(f"supertrend_periods ({len(supertrend_periods)}) and " f"supertrend_multipliers ({len(supertrend_multipliers)}) must have same length") if len(supertrend_periods) < 1: raise ValueError("At least one Supertrend indicator is required") # Initialize Supertrend collection with configurable parameters self.supertrend_configs = list(zip(supertrend_periods, supertrend_multipliers)) # Store agreement threshold self.min_trend_agreement = self.params.get("min_trend_agreement", 1.0) if not 0.0 <= self.min_trend_agreement <= 1.0: raise ValueError("min_trend_agreement must be between 0.0 and 1.0") self.supertrend_collection = SupertrendCollection(self.supertrend_configs) # Meta-trend state self.current_meta_trend = 0 self.previous_meta_trend = 0 self._meta_trend_history = [] # For debugging/analysis # Signal generation state self._last_entry_signal = None self._last_exit_signal = None self._signal_count = {"entry": 0, "exit": 0} # Performance tracking self._update_count = 0 self._last_update_time = None logger.info(f"MetaTrendStrategy initialized: timeframe={self.primary_timeframe}, " f"aggregation_enabled={self._timeframe_aggregator is not None}") logger.info(f"Supertrend configs: {self.supertrend_configs}, " f"min_agreement={self.min_trend_agreement}") if self.enable_logging: logger.info(f"Using new timeframe utilities with mathematically correct aggregation") logger.info(f"Bar timestamps use 'end' mode to prevent future data leakage") if self._timeframe_aggregator: stats = self.get_timeframe_aggregator_stats() logger.debug(f"Timeframe aggregator stats: {stats}") def get_minimum_buffer_size(self) -> Dict[str, int]: """ Return minimum data points needed for reliable Supertrend calculations. With the new base class timeframe aggregation, we only need to specify the minimum buffer size for our primary timeframe. The base class handles minute-level data aggregation automatically. Returns: Dict[str, int]: {timeframe: min_points} mapping """ # Find the largest period among all Supertrend configurations max_period = max(config[0] for config in self.supertrend_configs) # Add buffer for ATR warmup (ATR typically needs ~2x period for stability) min_buffer_size = max_period * 2 + 10 # Extra 10 points for safety # With new base class, we only specify our primary timeframe # The base class handles minute-level aggregation automatically return {self.primary_timeframe: min_buffer_size} def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None: """ Process a single new data point incrementally. This method updates the Supertrend indicators and recalculates the meta-trend based on the new data point. Args: new_data_point: OHLCV data point {open, high, low, close, volume} timestamp: Timestamp of the data point """ try: self._update_count += 1 self._last_update_time = timestamp if self.enable_logging: logger.debug(f"Processing data point {self._update_count} at {timestamp}") logger.debug(f"OHLC: O={new_data_point.get('open', 0):.2f}, " f"H={new_data_point.get('high', 0):.2f}, " f"L={new_data_point.get('low', 0):.2f}, " f"C={new_data_point.get('close', 0):.2f}") # Store previous meta-trend for change detection self.previous_meta_trend = self.current_meta_trend # Update Supertrend collection with new data supertrend_results = self.supertrend_collection.update(new_data_point) # Calculate new meta-trend self.current_meta_trend = self._calculate_meta_trend(supertrend_results) # Store meta-trend history for analysis self._meta_trend_history.append({ 'timestamp': timestamp, 'meta_trend': self.current_meta_trend, 'individual_trends': supertrend_results['trends'].copy(), 'update_count': self._update_count }) # Limit history size to prevent memory growth if len(self._meta_trend_history) > 1000: self._meta_trend_history = self._meta_trend_history[-500:] # Keep last 500 # Log meta-trend changes if self.enable_logging and self.current_meta_trend != self.previous_meta_trend: logger.info(f"Meta-trend changed: {self.previous_meta_trend} -> {self.current_meta_trend} " f"at {timestamp} (update #{self._update_count})") logger.debug(f"Individual trends: {supertrend_results['trends']}") # Update warmup status if not self._is_warmed_up and self.supertrend_collection.is_warmed_up(): self._is_warmed_up = True logger.info(f"Strategy warmed up after {self._update_count} data points") except Exception as e: logger.error(f"Error in calculate_on_data: {e}") raise def supports_incremental_calculation(self) -> bool: """ Whether strategy supports incremental calculation. Returns: bool: True (this strategy is fully incremental) """ return True def get_entry_signal(self) -> IncStrategySignal: """ 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. Returns: IncStrategySignal: Entry signal if trend aligns, hold signal otherwise """ if not self.is_warmed_up: return IncStrategySignal.HOLD() # Check for meta-trend entry condition if self._check_entry_condition(): self._signal_count["entry"] += 1 self._last_entry_signal = { 'timestamp': self._last_update_time, 'meta_trend': self.current_meta_trend, 'previous_meta_trend': self.previous_meta_trend, 'update_count': self._update_count } if self.enable_logging: logger.info(f"ENTRY SIGNAL generated at {self._last_update_time} " f"(signal #{self._signal_count['entry']})") return IncStrategySignal.BUY(confidence=1.0, metadata={ "meta_trend": self.current_meta_trend, "previous_meta_trend": self.previous_meta_trend, "signal_count": self._signal_count["entry"] }) return IncStrategySignal.HOLD() def get_exit_signal(self) -> IncStrategySignal: """ Generate exit signal based on meta-trend reversal. Exit occurs when meta-trend changes from != -1 to == -1, indicating trend reversal to downward direction. Returns: IncStrategySignal: Exit signal if trend reverses, hold signal otherwise """ if not self.is_warmed_up: return IncStrategySignal.HOLD() # Check for meta-trend exit condition if self._check_exit_condition(): self._signal_count["exit"] += 1 self._last_exit_signal = { 'timestamp': self._last_update_time, 'meta_trend': self.current_meta_trend, 'previous_meta_trend': self.previous_meta_trend, 'update_count': self._update_count } if self.enable_logging: logger.info(f"EXIT SIGNAL generated at {self._last_update_time} " f"(signal #{self._signal_count['exit']})") return IncStrategySignal.SELL(confidence=1.0, metadata={ "type": "META_TREND_EXIT", "meta_trend": self.current_meta_trend, "previous_meta_trend": self.previous_meta_trend, "signal_count": self._signal_count["exit"] }) return IncStrategySignal.HOLD() def get_confidence(self) -> float: """ Get strategy confidence based on meta-trend strength. Higher confidence when meta-trend is strongly directional, lower confidence during neutral periods. Returns: float: Confidence level (0.0 to 1.0) """ if not self.is_warmed_up: return 0.0 # High confidence for strong directional signals if self.current_meta_trend == 1 or self.current_meta_trend == -1: return 1.0 # Lower confidence for neutral trend return 0.3 def _calculate_meta_trend(self, supertrend_results: Dict) -> int: """ Calculate meta-trend from SupertrendCollection results. Meta-trend logic (enhanced with configurable agreement threshold): - Uses min_trend_agreement to determine consensus requirement - If agreement threshold is met for a direction, meta-trend = that direction - If no consensus, meta-trend = 0 (neutral) Args: supertrend_results: Results from SupertrendCollection.update() Returns: int: Meta-trend value (1, -1, or 0) """ trends = supertrend_results['trends'] total_indicators = len(trends) if total_indicators == 0: return 0 # Count votes for each direction uptrend_votes = sum(1 for trend in trends if trend == 1) downtrend_votes = sum(1 for trend in trends if trend == -1) # Calculate agreement percentages uptrend_agreement = uptrend_votes / total_indicators downtrend_agreement = downtrend_votes / total_indicators # Check if agreement threshold is met if uptrend_agreement >= self.min_trend_agreement: return 1 elif downtrend_agreement >= self.min_trend_agreement: return -1 else: return 0 # No consensus def _check_entry_condition(self) -> bool: """ Check if meta-trend entry condition is met. Entry condition: meta-trend changes from != 1 to == 1 Returns: bool: True if entry condition is met """ return (self.previous_meta_trend != 1 and self.current_meta_trend == 1) def _check_exit_condition(self) -> bool: """ Check if meta-trend exit condition is met. Exit condition: meta-trend changes from != 1 to == -1 (Modified to match original strategy behavior) Returns: bool: True if exit condition is met """ return (self.previous_meta_trend != 1 and self.current_meta_trend == -1) def get_current_state_summary(self) -> Dict[str, Any]: """ Get detailed state summary for debugging and monitoring. Returns: Dict with current strategy state information """ base_summary = super().get_current_state_summary() # Add MetaTrend-specific state base_summary.update({ 'primary_timeframe': self.primary_timeframe, 'current_meta_trend': self.current_meta_trend, 'previous_meta_trend': self.previous_meta_trend, 'supertrend_collection_warmed_up': self.supertrend_collection.is_warmed_up(), 'supertrend_configs': self.supertrend_configs, 'signal_counts': self._signal_count.copy(), 'update_count': self._update_count, 'last_update_time': str(self._last_update_time) if self._last_update_time else None, 'meta_trend_history_length': len(self._meta_trend_history), 'last_entry_signal': self._last_entry_signal, 'last_exit_signal': self._last_exit_signal }) # Add Supertrend collection state if hasattr(self.supertrend_collection, 'get_state_summary'): base_summary['supertrend_collection_state'] = self.supertrend_collection.get_state_summary() return base_summary def reset_calculation_state(self) -> None: """Reset internal calculation state for reinitialization.""" super().reset_calculation_state() # Reset Supertrend collection self.supertrend_collection.reset() # Reset meta-trend state self.current_meta_trend = 0 self.previous_meta_trend = 0 self._meta_trend_history.clear() # Reset signal state self._last_entry_signal = None self._last_exit_signal = None self._signal_count = {"entry": 0, "exit": 0} # Reset performance tracking self._update_count = 0 self._last_update_time = None logger.info("MetaTrendStrategy state reset") def get_meta_trend_history(self, limit: Optional[int] = None) -> List[Dict]: """ Get meta-trend history for analysis. Args: limit: Maximum number of recent entries to return Returns: List of meta-trend history entries """ if limit is None: return self._meta_trend_history.copy() else: return self._meta_trend_history[-limit:] if limit > 0 else [] def get_current_meta_trend(self) -> int: """ Get current meta-trend value. Returns: int: Current meta-trend (1, -1, or 0) """ return self.current_meta_trend def get_individual_supertrend_states(self) -> List[Dict]: """ Get current state of individual Supertrend indicators. Returns: List of Supertrend state summaries """ if hasattr(self.supertrend_collection, 'get_state_summary'): collection_state = self.supertrend_collection.get_state_summary() return collection_state.get('supertrends', []) return [] # Compatibility alias for easier imports IncMetaTrendStrategy = MetaTrendStrategy