2025-05-28 22:37:53 +08:00

18 KiB
Raw Blame History

Volatility Indicators

Overview

Volatility indicators measure the rate of price change and market uncertainty. IncrementalTrader provides Average True Range (ATR) implementations that help assess market volatility and set appropriate stop-loss levels.

ATRState (Average True Range)

Full ATR implementation that maintains a moving average of True Range values.

Features

  • True Range Calculation: Accounts for gaps between trading sessions
  • Volatility Measurement: Provides absolute volatility measurement
  • Stop-Loss Guidance: Helps set dynamic stop-loss levels
  • Trend Strength: Indicates trend strength through volatility

Mathematical Formula

True Range = max(
    High - Low,
    |High - Previous_Close|,
    |Low - Previous_Close|
)

ATR = Moving_Average(True_Range, period)

Class Definition

from IncrementalTrader.strategies.indicators import ATRState

class ATRState(OHLCIndicatorState):
    def __init__(self, period: int):
        super().__init__(period)
        self.true_ranges = []
        self.tr_sum = 0.0
        self.previous_close = None
    
    def _process_ohlc_data(self, high: float, low: float, close: float):
        # Calculate True Range
        if self.previous_close is not None:
            tr = max(
                high - low,
                abs(high - self.previous_close),
                abs(low - self.previous_close)
            )
        else:
            tr = high - low
        
        # Update True Range moving average
        self.true_ranges.append(tr)
        self.tr_sum += tr
        
        if len(self.true_ranges) > self.period:
            old_tr = self.true_ranges.pop(0)
            self.tr_sum -= old_tr
        
        self.previous_close = close
    
    def get_value(self) -> float:
        if not self.is_ready():
            return 0.0
        return self.tr_sum / len(self.true_ranges)

Usage Examples

Basic ATR Calculation

# Create 14-period ATR
atr_14 = ATRState(period=14)

# OHLC data: (high, low, close)
ohlc_data = [
    (105.0, 102.0, 104.0),
    (106.0, 103.0, 105.5),
    (107.0, 104.0, 106.0),
    (108.0, 105.0, 107.5)
]

for high, low, close in ohlc_data:
    atr_14.update_ohlc(high, low, close)
    if atr_14.is_ready():
        print(f"ATR(14): {atr_14.get_value():.2f}")

Dynamic Stop-Loss with ATR

class ATRStopLoss:
    def __init__(self, atr_period: int = 14, atr_multiplier: float = 2.0):
        self.atr = ATRState(atr_period)
        self.atr_multiplier = atr_multiplier
    
    def update(self, high: float, low: float, close: float):
        self.atr.update_ohlc(high, low, close)
    
    def get_stop_loss(self, entry_price: float, position_type: str) -> float:
        if not self.atr.is_ready():
            return entry_price * 0.95 if position_type == "LONG" else entry_price * 1.05
        
        atr_value = self.atr.get_value()
        
        if position_type == "LONG":
            return entry_price - (atr_value * self.atr_multiplier)
        else:  # SHORT
            return entry_price + (atr_value * self.atr_multiplier)
    
    def get_position_size(self, account_balance: float, risk_percent: float, entry_price: float, position_type: str) -> float:
        """Calculate position size based on ATR risk."""
        if not self.atr.is_ready():
            return 0.0
        
        risk_amount = account_balance * (risk_percent / 100)
        stop_loss = self.get_stop_loss(entry_price, position_type)
        risk_per_share = abs(entry_price - stop_loss)
        
        if risk_per_share == 0:
            return 0.0
        
        return risk_amount / risk_per_share

# Usage
atr_stop = ATRStopLoss(atr_period=14, atr_multiplier=2.0)

for high, low, close in ohlc_stream:
    atr_stop.update(high, low, close)
    
    # Calculate stop loss for a long position
    entry_price = close
    stop_loss = atr_stop.get_stop_loss(entry_price, "LONG")
    position_size = atr_stop.get_position_size(10000, 2.0, entry_price, "LONG")
    
    print(f"Entry: {entry_price:.2f}, Stop: {stop_loss:.2f}, Size: {position_size:.0f}")

Performance Characteristics

  • Time Complexity: O(1) per update
  • Space Complexity: O(period)
  • Memory Usage: ~8 bytes per period + constant overhead

SimpleATRState

Simplified ATR implementation using exponential smoothing instead of simple moving average.

Features

  • O(1) Memory: Constant memory usage regardless of period
  • Exponential Smoothing: Uses Wilder's smoothing method
  • Faster Computation: No need to maintain historical True Range values
  • Traditional ATR: Follows Wilder's original ATR calculation

Mathematical Formula

True Range = max(
    High - Low,
    |High - Previous_Close|,
    |Low - Previous_Close|
)

ATR = (Previous_ATR × (period - 1) + True_Range) / period

Class Definition

class SimpleATRState(OHLCIndicatorState):
    def __init__(self, period: int):
        super().__init__(period)
        self.atr_value = 0.0
        self.previous_close = None
        self.is_first_value = True
    
    def _process_ohlc_data(self, high: float, low: float, close: float):
        # Calculate True Range
        if self.previous_close is not None:
            tr = max(
                high - low,
                abs(high - self.previous_close),
                abs(low - self.previous_close)
            )
        else:
            tr = high - low
        
        # Update ATR using Wilder's smoothing
        if self.is_first_value:
            self.atr_value = tr
            self.is_first_value = False
        else:
            self.atr_value = ((self.atr_value * (self.period - 1)) + tr) / self.period
        
        self.previous_close = close
    
    def get_value(self) -> float:
        return self.atr_value

Usage Examples

Memory-Efficient ATR

# Create memory-efficient ATR
simple_atr = SimpleATRState(period=14)

# Process large amounts of data with constant memory
for i, (high, low, close) in enumerate(large_ohlc_dataset):
    simple_atr.update_ohlc(high, low, close)
    
    if i % 1000 == 0:  # Print every 1000 updates
        print(f"ATR after {i} updates: {simple_atr.get_value():.4f}")

Volatility Breakout Strategy

class VolatilityBreakout:
    def __init__(self, atr_period: int = 14, breakout_multiplier: float = 1.5):
        self.atr = SimpleATRState(atr_period)
        self.breakout_multiplier = breakout_multiplier
        self.previous_close = None
    
    def update(self, high: float, low: float, close: float):
        self.atr.update_ohlc(high, low, close)
        self.previous_close = close
    
    def get_breakout_levels(self, current_close: float) -> tuple:
        """Get upper and lower breakout levels."""
        if not self.atr.is_ready() or self.previous_close is None:
            return current_close * 1.01, current_close * 0.99
        
        atr_value = self.atr.get_value()
        breakout_distance = atr_value * self.breakout_multiplier
        
        upper_breakout = self.previous_close + breakout_distance
        lower_breakout = self.previous_close - breakout_distance
        
        return upper_breakout, lower_breakout
    
    def check_breakout(self, current_high: float, current_low: float, current_close: float) -> str:
        """Check if current price breaks out of volatility range."""
        upper_level, lower_level = self.get_breakout_levels(current_close)
        
        if current_high > upper_level:
            return "BULLISH_BREAKOUT"
        elif current_low < lower_level:
            return "BEARISH_BREAKOUT"
        
        return "NO_BREAKOUT"

# Usage
breakout_detector = VolatilityBreakout(atr_period=14, breakout_multiplier=1.5)

for high, low, close in ohlc_data:
    breakout_detector.update(high, low, close)
    breakout_signal = breakout_detector.check_breakout(high, low, close)
    
    if breakout_signal != "NO_BREAKOUT":
        print(f"Breakout detected: {breakout_signal} at {close:.2f}")

Performance Characteristics

  • Time Complexity: O(1) per update
  • Space Complexity: O(1)
  • Memory Usage: ~32 bytes (constant)

Comparison: ATRState vs SimpleATRState

Aspect ATRState SimpleATRState
Memory Usage O(period) O(1)
Calculation Method Simple Moving Average Exponential Smoothing
Accuracy Higher (true SMA) Good (Wilder's method)
Responsiveness Moderate Slightly more responsive
Historical Compatibility Modern Traditional (Wilder's)

When to Use ATRState

  • Precise Calculations: When you need exact simple moving average of True Range
  • Backtesting: For historical analysis where memory isn't constrained
  • Research: When studying exact ATR behavior
  • Small Periods: When period is small (< 20) and memory isn't an issue

When to Use SimpleATRState

  • Memory Efficiency: When processing large amounts of data
  • Real-time Systems: For high-frequency trading applications
  • Traditional Analysis: When following Wilder's original methodology
  • Large Periods: When using large ATR periods (> 50)

Advanced Usage Patterns

Multi-Timeframe ATR Analysis

class MultiTimeframeATR:
    def __init__(self):
        self.atr_short = SimpleATRState(period=7)   # Short-term volatility
        self.atr_medium = SimpleATRState(period=14) # Medium-term volatility
        self.atr_long = SimpleATRState(period=28)   # Long-term volatility
    
    def update(self, high: float, low: float, close: float):
        self.atr_short.update_ohlc(high, low, close)
        self.atr_medium.update_ohlc(high, low, close)
        self.atr_long.update_ohlc(high, low, close)
    
    def get_volatility_regime(self) -> str:
        """Determine current volatility regime."""
        if not all([self.atr_short.is_ready(), self.atr_medium.is_ready(), self.atr_long.is_ready()]):
            return "UNKNOWN"
        
        short_atr = self.atr_short.get_value()
        medium_atr = self.atr_medium.get_value()
        long_atr = self.atr_long.get_value()
        
        # Compare short-term to long-term volatility
        volatility_ratio = short_atr / long_atr if long_atr > 0 else 1.0
        
        if volatility_ratio > 1.5:
            return "HIGH_VOLATILITY"
        elif volatility_ratio < 0.7:
            return "LOW_VOLATILITY"
        else:
            return "NORMAL_VOLATILITY"
    
    def get_adaptive_stop_multiplier(self) -> float:
        """Get adaptive stop-loss multiplier based on volatility regime."""
        regime = self.get_volatility_regime()
        
        if regime == "HIGH_VOLATILITY":
            return 2.5  # Wider stops in high volatility
        elif regime == "LOW_VOLATILITY":
            return 1.5  # Tighter stops in low volatility
        else:
            return 2.0  # Standard stops in normal volatility

# Usage
multi_atr = MultiTimeframeATR()

for high, low, close in ohlc_data:
    multi_atr.update(high, low, close)
    
    regime = multi_atr.get_volatility_regime()
    stop_multiplier = multi_atr.get_adaptive_stop_multiplier()
    
    print(f"Volatility Regime: {regime}, Stop Multiplier: {stop_multiplier:.1f}")

ATR-Based Position Sizing

class ATRPositionSizer:
    def __init__(self, atr_period: int = 14):
        self.atr = SimpleATRState(atr_period)
        self.price_history = []
    
    def update(self, high: float, low: float, close: float):
        self.atr.update_ohlc(high, low, close)
        self.price_history.append(close)
        
        # Keep only recent price history
        if len(self.price_history) > 100:
            self.price_history.pop(0)
    
    def calculate_position_size(self, account_balance: float, risk_percent: float, 
                              entry_price: float, stop_loss_atr_multiplier: float = 2.0) -> dict:
        """Calculate position size based on ATR risk management."""
        
        if not self.atr.is_ready():
            return {"position_size": 0, "risk_amount": 0, "stop_loss": entry_price * 0.95}
        
        atr_value = self.atr.get_value()
        risk_amount = account_balance * (risk_percent / 100)
        
        # Calculate stop loss based on ATR
        stop_loss = entry_price - (atr_value * stop_loss_atr_multiplier)
        risk_per_share = entry_price - stop_loss
        
        # Calculate position size
        if risk_per_share > 0:
            position_size = risk_amount / risk_per_share
        else:
            position_size = 0
        
        return {
            "position_size": position_size,
            "risk_amount": risk_amount,
            "stop_loss": stop_loss,
            "atr_value": atr_value,
            "risk_per_share": risk_per_share
        }
    
    def get_volatility_percentile(self) -> float:
        """Get current ATR percentile compared to recent history."""
        if not self.atr.is_ready() or len(self.price_history) < 20:
            return 50.0  # Default to median
        
        current_atr = self.atr.get_value()
        
        # Calculate ATR for recent periods
        recent_atrs = []
        for i in range(len(self.price_history) - 14):
            if i + 14 < len(self.price_history):
                # Simplified ATR calculation for comparison
                price_range = max(self.price_history[i:i+14]) - min(self.price_history[i:i+14])
                recent_atrs.append(price_range)
        
        if not recent_atrs:
            return 50.0
        
        # Calculate percentile
        sorted_atrs = sorted(recent_atrs)
        position = sum(1 for atr in sorted_atrs if atr <= current_atr)
        percentile = (position / len(sorted_atrs)) * 100
        
        return percentile

# Usage
position_sizer = ATRPositionSizer(atr_period=14)

for high, low, close in ohlc_data:
    position_sizer.update(high, low, close)
    
    # Calculate position for a potential trade
    trade_info = position_sizer.calculate_position_size(
        account_balance=10000,
        risk_percent=2.0,
        entry_price=close,
        stop_loss_atr_multiplier=2.0
    )
    
    volatility_percentile = position_sizer.get_volatility_percentile()
    
    print(f"Price: {close:.2f}, Position Size: {trade_info['position_size']:.0f}, "
          f"ATR Percentile: {volatility_percentile:.1f}%")

Integration with Strategies

ATR-Enhanced Strategy Example

class ATRTrendStrategy(IncStrategyBase):
    def __init__(self, name: str, params: dict = None):
        super().__init__(name, params)
        
        # Initialize indicators
        self.atr = SimpleATRState(self.params.get('atr_period', 14))
        self.sma = MovingAverageState(self.params.get('sma_period', 20))
        
        # ATR parameters
        self.atr_stop_multiplier = self.params.get('atr_stop_multiplier', 2.0)
        self.atr_entry_multiplier = self.params.get('atr_entry_multiplier', 0.5)
    
    def _process_aggregated_data(self, timestamp: int, ohlcv: tuple) -> IncStrategySignal:
        open_price, high, low, close, volume = ohlcv
        
        # Update indicators
        self.atr.update_ohlc(high, low, close)
        self.sma.update(close)
        
        # Wait for indicators to be ready
        if not all([self.atr.is_ready(), self.sma.is_ready()]):
            return IncStrategySignal.HOLD()
        
        atr_value = self.atr.get_value()
        sma_value = self.sma.get_value()
        
        # Calculate dynamic entry threshold based on ATR
        entry_threshold = atr_value * self.atr_entry_multiplier
        
        # Generate signals based on trend and volatility
        if close > sma_value + entry_threshold:
            # Strong uptrend with sufficient volatility
            confidence = min(0.9, (close - sma_value) / atr_value * 0.1)
            
            # Calculate stop loss
            stop_loss = close - (atr_value * self.atr_stop_multiplier)
            
            return IncStrategySignal.BUY(
                confidence=confidence,
                metadata={
                    'atr_value': atr_value,
                    'sma_value': sma_value,
                    'stop_loss': stop_loss,
                    'entry_threshold': entry_threshold
                }
            )
        
        elif close < sma_value - entry_threshold:
            # Strong downtrend with sufficient volatility
            confidence = min(0.9, (sma_value - close) / atr_value * 0.1)
            
            # Calculate stop loss
            stop_loss = close + (atr_value * self.atr_stop_multiplier)
            
            return IncStrategySignal.SELL(
                confidence=confidence,
                metadata={
                    'atr_value': atr_value,
                    'sma_value': sma_value,
                    'stop_loss': stop_loss,
                    'entry_threshold': entry_threshold
                }
            )
        
        return IncStrategySignal.HOLD()

Performance Optimization Tips

1. Choose the Right ATR Implementation

# For memory-constrained environments
atr = SimpleATRState(period=14)  # O(1) memory

# For precise calculations
atr = ATRState(period=14)  # O(period) memory

2. Batch Processing for Multiple ATRs

def update_multiple_atrs(atrs: list, high: float, low: float, close: float):
    """Efficiently update multiple ATR indicators."""
    for atr in atrs:
        atr.update_ohlc(high, low, close)
    
    return [atr.get_value() for atr in atrs if atr.is_ready()]

3. Cache ATR Values for Complex Calculations

class CachedATR:
    def __init__(self, period: int):
        self.atr = SimpleATRState(period)
        self._cached_value = 0.0
        self._cache_valid = False
    
    def update_ohlc(self, high: float, low: float, close: float):
        self.atr.update_ohlc(high, low, close)
        self._cache_valid = False
    
    def get_value(self) -> float:
        if not self._cache_valid:
            self._cached_value = self.atr.get_value()
            self._cache_valid = True
        return self._cached_value

ATR indicators are essential for risk management and volatility analysis. Use ATRState for precise calculations or SimpleATRState for memory efficiency in high-frequency applications.