- Introduced a comprehensive framework for incremental trading strategies, including modules for strategy execution, backtesting, and data processing. - Added key components such as `IncTrader`, `IncBacktester`, and various trading strategies (e.g., `MetaTrendStrategy`, `BBRSStrategy`, `RandomStrategy`) to facilitate real-time trading and backtesting. - Implemented a robust backtesting framework with configuration management, parallel execution, and result analysis capabilities. - Developed an incremental indicators framework to support real-time data processing with constant memory usage. - Enhanced documentation to provide clear usage examples and architecture overview, ensuring maintainability and ease of understanding for future development. - Ensured compatibility with existing strategies and maintained a focus on performance and scalability throughout the implementation.
316 lines
11 KiB
Python
316 lines
11 KiB
Python
"""
|
|
Supertrend Indicator State
|
|
|
|
This module implements incremental Supertrend calculation that maintains constant memory usage
|
|
and provides identical results to traditional batch calculations. Supertrend is used by
|
|
the DefaultStrategy for trend detection.
|
|
"""
|
|
|
|
from typing import Dict, Union, Optional
|
|
from .base import OHLCIndicatorState
|
|
from .atr import ATRState
|
|
|
|
|
|
class SupertrendState(OHLCIndicatorState):
|
|
"""
|
|
Incremental Supertrend calculation state.
|
|
|
|
Supertrend is a trend-following indicator that uses Average True Range (ATR)
|
|
to calculate dynamic support and resistance levels. It provides clear trend
|
|
direction signals: +1 for uptrend, -1 for downtrend.
|
|
|
|
The calculation involves:
|
|
1. Calculate ATR for the given period
|
|
2. Calculate basic upper and lower bands using ATR and multiplier
|
|
3. Calculate final upper and lower bands with trend logic
|
|
4. Determine trend direction based on price vs bands
|
|
|
|
Attributes:
|
|
period (int): ATR period for Supertrend calculation
|
|
multiplier (float): Multiplier for ATR in band calculation
|
|
atr_state (ATRState): ATR calculation state
|
|
previous_close (float): Previous period's close price
|
|
previous_trend (int): Previous trend direction (+1 or -1)
|
|
final_upper_band (float): Current final upper band
|
|
final_lower_band (float): Current final lower band
|
|
|
|
Example:
|
|
supertrend = SupertrendState(period=10, multiplier=3.0)
|
|
|
|
# Add OHLC data incrementally
|
|
ohlc = {'open': 100, 'high': 105, 'low': 98, 'close': 103}
|
|
result = supertrend.update(ohlc)
|
|
trend = result['trend'] # +1 or -1
|
|
supertrend_value = result['supertrend'] # Supertrend line value
|
|
"""
|
|
|
|
def __init__(self, period: int = 10, multiplier: float = 3.0):
|
|
"""
|
|
Initialize Supertrend state.
|
|
|
|
Args:
|
|
period: ATR period for Supertrend calculation (default: 10)
|
|
multiplier: Multiplier for ATR in band calculation (default: 3.0)
|
|
|
|
Raises:
|
|
ValueError: If period is not positive or multiplier is not positive
|
|
"""
|
|
super().__init__(period)
|
|
|
|
if multiplier <= 0:
|
|
raise ValueError(f"Multiplier must be positive, got {multiplier}")
|
|
|
|
self.multiplier = multiplier
|
|
self.atr_state = ATRState(period)
|
|
|
|
# State variables
|
|
self.previous_close = None
|
|
self.previous_trend = None # Don't assume initial trend, let first calculation determine it
|
|
self.final_upper_band = None
|
|
self.final_lower_band = None
|
|
|
|
# Current values
|
|
self.current_trend = None
|
|
self.current_supertrend = None
|
|
|
|
self.is_initialized = True
|
|
|
|
def update(self, ohlc_data: Dict[str, float]) -> Dict[str, float]:
|
|
"""
|
|
Update Supertrend with new OHLC data.
|
|
|
|
Args:
|
|
ohlc_data: Dictionary with 'open', 'high', 'low', 'close' keys
|
|
|
|
Returns:
|
|
Dictionary with 'trend', 'supertrend', 'upper_band', 'lower_band' keys
|
|
|
|
Raises:
|
|
ValueError: If OHLC data is invalid
|
|
TypeError: If ohlc_data is not a dictionary
|
|
"""
|
|
# Validate input
|
|
if not isinstance(ohlc_data, dict):
|
|
raise TypeError(f"ohlc_data must be a dictionary, got {type(ohlc_data)}")
|
|
|
|
self.validate_input(ohlc_data)
|
|
|
|
high = float(ohlc_data['high'])
|
|
low = float(ohlc_data['low'])
|
|
close = float(ohlc_data['close'])
|
|
|
|
# Update ATR
|
|
atr_value = self.atr_state.update(ohlc_data)
|
|
|
|
# Calculate HL2 (typical price)
|
|
hl2 = (high + low) / 2.0
|
|
|
|
# Calculate basic upper and lower bands
|
|
basic_upper_band = hl2 + (self.multiplier * atr_value)
|
|
basic_lower_band = hl2 - (self.multiplier * atr_value)
|
|
|
|
# Calculate final upper band
|
|
if self.final_upper_band is None or basic_upper_band < self.final_upper_band or self.previous_close > self.final_upper_band:
|
|
final_upper_band = basic_upper_band
|
|
else:
|
|
final_upper_band = self.final_upper_band
|
|
|
|
# Calculate final lower band
|
|
if self.final_lower_band is None or basic_lower_band > self.final_lower_band or self.previous_close < self.final_lower_band:
|
|
final_lower_band = basic_lower_band
|
|
else:
|
|
final_lower_band = self.final_lower_band
|
|
|
|
# Determine trend
|
|
if self.previous_close is None:
|
|
# First calculation - match original logic
|
|
# If close <= upper_band, trend is -1 (downtrend), else trend is 1 (uptrend)
|
|
trend = -1 if close <= basic_upper_band else 1
|
|
else:
|
|
# Trend logic for subsequent calculations
|
|
if self.previous_trend == 1 and close <= final_lower_band:
|
|
trend = -1
|
|
elif self.previous_trend == -1 and close >= final_upper_band:
|
|
trend = 1
|
|
else:
|
|
trend = self.previous_trend
|
|
|
|
# Calculate Supertrend value
|
|
if trend == 1:
|
|
supertrend_value = final_lower_band
|
|
else:
|
|
supertrend_value = final_upper_band
|
|
|
|
# Store current state
|
|
self.previous_close = close
|
|
self.previous_trend = trend
|
|
self.final_upper_band = final_upper_band
|
|
self.final_lower_band = final_lower_band
|
|
self.current_trend = trend
|
|
self.current_supertrend = supertrend_value
|
|
self.values_received += 1
|
|
|
|
# Prepare result
|
|
result = {
|
|
'trend': trend,
|
|
'supertrend': supertrend_value,
|
|
'upper_band': final_upper_band,
|
|
'lower_band': final_lower_band,
|
|
'atr': atr_value
|
|
}
|
|
|
|
self._current_values = result
|
|
return result
|
|
|
|
def is_warmed_up(self) -> bool:
|
|
"""
|
|
Check if Supertrend has enough data for reliable values.
|
|
|
|
Returns:
|
|
True if ATR state is warmed up
|
|
"""
|
|
return self.atr_state.is_warmed_up()
|
|
|
|
def reset(self) -> None:
|
|
"""Reset Supertrend state to initial conditions."""
|
|
self.atr_state.reset()
|
|
self.previous_close = None
|
|
self.previous_trend = None
|
|
self.final_upper_band = None
|
|
self.final_lower_band = None
|
|
self.current_trend = None
|
|
self.current_supertrend = None
|
|
self.values_received = 0
|
|
self._current_values = {}
|
|
|
|
def get_current_value(self) -> Optional[Dict[str, float]]:
|
|
"""
|
|
Get current Supertrend values without updating.
|
|
|
|
Returns:
|
|
Dictionary with current Supertrend values, or None if not warmed up
|
|
"""
|
|
if not self.is_warmed_up():
|
|
return None
|
|
return self._current_values.copy() if self._current_values else None
|
|
|
|
def get_current_trend(self) -> int:
|
|
"""
|
|
Get current trend direction.
|
|
|
|
Returns:
|
|
Current trend (+1 for uptrend, -1 for downtrend, 0 if not warmed up)
|
|
"""
|
|
return self.current_trend if self.current_trend is not None else 0
|
|
|
|
def get_current_supertrend_value(self) -> Optional[float]:
|
|
"""
|
|
Get current Supertrend line value.
|
|
|
|
Returns:
|
|
Current Supertrend value, or None if not warmed up
|
|
"""
|
|
return self.current_supertrend
|
|
|
|
def get_state_summary(self) -> dict:
|
|
"""Get detailed state summary for debugging."""
|
|
base_summary = super().get_state_summary()
|
|
base_summary.update({
|
|
'multiplier': self.multiplier,
|
|
'previous_close': self.previous_close,
|
|
'previous_trend': self.previous_trend,
|
|
'current_trend': self.current_trend,
|
|
'current_supertrend': self.current_supertrend,
|
|
'final_upper_band': self.final_upper_band,
|
|
'final_lower_band': self.final_lower_band,
|
|
'atr_state': self.atr_state.get_state_summary()
|
|
})
|
|
return base_summary
|
|
|
|
|
|
class SupertrendCollection:
|
|
"""
|
|
Collection of multiple Supertrend indicators for meta-trend calculation.
|
|
|
|
This class manages multiple Supertrend indicators with different parameters
|
|
and provides meta-trend calculation based on their agreement.
|
|
"""
|
|
|
|
def __init__(self, supertrend_configs: list):
|
|
"""
|
|
Initialize collection of Supertrend indicators.
|
|
|
|
Args:
|
|
supertrend_configs: List of (period, multiplier) tuples
|
|
"""
|
|
self.supertrends = []
|
|
self.configs = supertrend_configs
|
|
|
|
for period, multiplier in supertrend_configs:
|
|
supertrend = SupertrendState(period=period, multiplier=multiplier)
|
|
self.supertrends.append(supertrend)
|
|
|
|
def update(self, ohlc_data: Dict[str, float]) -> Dict[str, Union[int, list]]:
|
|
"""
|
|
Update all Supertrend indicators and calculate meta-trend.
|
|
|
|
Args:
|
|
ohlc_data: OHLC data dictionary
|
|
|
|
Returns:
|
|
Dictionary with 'meta_trend' and 'trends' keys
|
|
"""
|
|
trends = []
|
|
|
|
# Update each Supertrend and collect trends
|
|
for supertrend in self.supertrends:
|
|
result = supertrend.update(ohlc_data)
|
|
trends.append(result['trend'])
|
|
|
|
# Calculate meta-trend
|
|
meta_trend = self.get_current_meta_trend()
|
|
|
|
return {
|
|
'meta_trend': meta_trend,
|
|
'trends': trends
|
|
}
|
|
|
|
def is_warmed_up(self) -> bool:
|
|
"""Check if all Supertrend indicators are warmed up."""
|
|
return all(st.is_warmed_up() for st in self.supertrends)
|
|
|
|
def reset(self) -> None:
|
|
"""Reset all Supertrend indicators."""
|
|
for supertrend in self.supertrends:
|
|
supertrend.reset()
|
|
|
|
def get_current_meta_trend(self) -> int:
|
|
"""
|
|
Calculate current meta-trend from all Supertrend indicators.
|
|
|
|
Meta-trend logic:
|
|
- If all trends agree, return that trend
|
|
- If trends disagree, return 0 (neutral)
|
|
|
|
Returns:
|
|
Meta-trend value (1, -1, or 0)
|
|
"""
|
|
if not self.is_warmed_up():
|
|
return 0
|
|
|
|
trends = [st.get_current_trend() for st in self.supertrends]
|
|
|
|
# Check if all trends agree
|
|
if all(trend == trends[0] for trend in trends):
|
|
return trends[0] # All agree: return the common trend
|
|
else:
|
|
return 0 # Neutral when trends disagree
|
|
|
|
def get_state_summary(self) -> dict:
|
|
"""Get detailed state summary for all Supertrend indicators."""
|
|
return {
|
|
'configs': self.configs,
|
|
'meta_trend': self.get_current_meta_trend(),
|
|
'is_warmed_up': self.is_warmed_up(),
|
|
'supertrends': [st.get_state_summary() for st in self.supertrends]
|
|
} |