467 lines
18 KiB
Python
467 lines
18 KiB
Python
"""
|
|
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 |