364 lines
11 KiB
Markdown
364 lines
11 KiB
Markdown
|
|
# Base Indicator Classes
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
All indicators in IncrementalTrader are built on a foundation of base classes that provide common functionality for incremental computation. These base classes ensure consistent behavior, memory efficiency, and real-time capability across all indicators.
|
||
|
|
|
||
|
|
## Available indicators
|
||
|
|
|
||
|
|
- [Moving Averages](moving_averages.md)
|
||
|
|
- [Volatility](volatility.md) - ATR
|
||
|
|
- [Trend](trend.md) - Supertrend
|
||
|
|
- [Oscillators](oscillators.md) - RSI
|
||
|
|
- [Bollinger Bands](bollinger_bands.md) - Bollinger Bands
|
||
|
|
|
||
|
|
## IndicatorState
|
||
|
|
|
||
|
|
The foundation class for all indicators in the framework.
|
||
|
|
|
||
|
|
### Features
|
||
|
|
- **Incremental Computation**: O(1) time complexity per update
|
||
|
|
- **Constant Memory**: O(1) space complexity regardless of data history
|
||
|
|
- **State Management**: Maintains internal state efficiently
|
||
|
|
- **Ready State Tracking**: Indicates when indicator has sufficient data
|
||
|
|
|
||
|
|
### Class Definition
|
||
|
|
|
||
|
|
```python
|
||
|
|
from IncrementalTrader.strategies.indicators import IndicatorState
|
||
|
|
|
||
|
|
class IndicatorState:
|
||
|
|
def __init__(self, period: int):
|
||
|
|
self.period = period
|
||
|
|
self.data_count = 0
|
||
|
|
|
||
|
|
def update(self, value: float):
|
||
|
|
"""Update indicator with new value."""
|
||
|
|
raise NotImplementedError("Subclasses must implement update method")
|
||
|
|
|
||
|
|
def get_value(self) -> float:
|
||
|
|
"""Get current indicator value."""
|
||
|
|
raise NotImplementedError("Subclasses must implement get_value method")
|
||
|
|
|
||
|
|
def is_ready(self) -> bool:
|
||
|
|
"""Check if indicator has enough data."""
|
||
|
|
return self.data_count >= self.period
|
||
|
|
|
||
|
|
def reset(self):
|
||
|
|
"""Reset indicator state."""
|
||
|
|
self.data_count = 0
|
||
|
|
```
|
||
|
|
|
||
|
|
### Methods
|
||
|
|
|
||
|
|
| Method | Description | Returns |
|
||
|
|
|--------|-------------|---------|
|
||
|
|
| `update(value: float)` | Update indicator with new value | None |
|
||
|
|
| `get_value() -> float` | Get current indicator value | float |
|
||
|
|
| `is_ready() -> bool` | Check if indicator has enough data | bool |
|
||
|
|
| `reset()` | Reset indicator state | None |
|
||
|
|
|
||
|
|
### Usage Example
|
||
|
|
|
||
|
|
```python
|
||
|
|
class MyCustomIndicator(IndicatorState):
|
||
|
|
def __init__(self, period: int):
|
||
|
|
super().__init__(period)
|
||
|
|
self.sum = 0.0
|
||
|
|
self.values = []
|
||
|
|
|
||
|
|
def update(self, value: float):
|
||
|
|
self.values.append(value)
|
||
|
|
self.sum += value
|
||
|
|
|
||
|
|
if len(self.values) > self.period:
|
||
|
|
old_value = self.values.pop(0)
|
||
|
|
self.sum -= old_value
|
||
|
|
|
||
|
|
self.data_count += 1
|
||
|
|
|
||
|
|
def get_value(self) -> float:
|
||
|
|
if not self.is_ready():
|
||
|
|
return 0.0
|
||
|
|
return self.sum / min(len(self.values), self.period)
|
||
|
|
|
||
|
|
# Usage
|
||
|
|
indicator = MyCustomIndicator(period=10)
|
||
|
|
for price in [100, 101, 99, 102, 98]:
|
||
|
|
indicator.update(price)
|
||
|
|
if indicator.is_ready():
|
||
|
|
print(f"Value: {indicator.get_value():.2f}")
|
||
|
|
```
|
||
|
|
|
||
|
|
## SimpleIndicatorState
|
||
|
|
|
||
|
|
For indicators that only need the current value and don't require a period.
|
||
|
|
|
||
|
|
### Features
|
||
|
|
- **Immediate Ready**: Always ready after first update
|
||
|
|
- **No Period Requirement**: Doesn't need historical data
|
||
|
|
- **Minimal State**: Stores only current value
|
||
|
|
|
||
|
|
### Class Definition
|
||
|
|
|
||
|
|
```python
|
||
|
|
class SimpleIndicatorState(IndicatorState):
|
||
|
|
def __init__(self):
|
||
|
|
super().__init__(period=1)
|
||
|
|
self.current_value = 0.0
|
||
|
|
|
||
|
|
def update(self, value: float):
|
||
|
|
self.current_value = value
|
||
|
|
self.data_count = 1 # Always ready
|
||
|
|
|
||
|
|
def get_value(self) -> float:
|
||
|
|
return self.current_value
|
||
|
|
```
|
||
|
|
|
||
|
|
### Usage Example
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Simple price tracker
|
||
|
|
price_tracker = SimpleIndicatorState()
|
||
|
|
|
||
|
|
for price in [100, 101, 99, 102]:
|
||
|
|
price_tracker.update(price)
|
||
|
|
print(f"Current price: {price_tracker.get_value():.2f}")
|
||
|
|
```
|
||
|
|
|
||
|
|
## OHLCIndicatorState
|
||
|
|
|
||
|
|
For indicators that require OHLC (Open, High, Low, Close) data instead of just a single price value.
|
||
|
|
|
||
|
|
### Features
|
||
|
|
- **OHLC Data Support**: Handles high, low, close data
|
||
|
|
- **Flexible Updates**: Can update with individual OHLC components
|
||
|
|
- **Typical Price Calculation**: Built-in typical price (HLC/3) calculation
|
||
|
|
|
||
|
|
### Class Definition
|
||
|
|
|
||
|
|
```python
|
||
|
|
class OHLCIndicatorState(IndicatorState):
|
||
|
|
def __init__(self, period: int):
|
||
|
|
super().__init__(period)
|
||
|
|
self.current_high = 0.0
|
||
|
|
self.current_low = 0.0
|
||
|
|
self.current_close = 0.0
|
||
|
|
|
||
|
|
def update_ohlc(self, high: float, low: float, close: float):
|
||
|
|
"""Update with OHLC data."""
|
||
|
|
self.current_high = high
|
||
|
|
self.current_low = low
|
||
|
|
self.current_close = close
|
||
|
|
self._process_ohlc_data(high, low, close)
|
||
|
|
self.data_count += 1
|
||
|
|
|
||
|
|
def _process_ohlc_data(self, high: float, low: float, close: float):
|
||
|
|
"""Process OHLC data - to be implemented by subclasses."""
|
||
|
|
raise NotImplementedError("Subclasses must implement _process_ohlc_data")
|
||
|
|
|
||
|
|
def get_typical_price(self) -> float:
|
||
|
|
"""Calculate typical price (HLC/3)."""
|
||
|
|
return (self.current_high + self.current_low + self.current_close) / 3.0
|
||
|
|
|
||
|
|
def get_true_range(self, prev_close: float = None) -> float:
|
||
|
|
"""Calculate True Range."""
|
||
|
|
if prev_close is None:
|
||
|
|
return self.current_high - self.current_low
|
||
|
|
|
||
|
|
return max(
|
||
|
|
self.current_high - self.current_low,
|
||
|
|
abs(self.current_high - prev_close),
|
||
|
|
abs(self.current_low - prev_close)
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Methods
|
||
|
|
|
||
|
|
| Method | Description | Returns |
|
||
|
|
|--------|-------------|---------|
|
||
|
|
| `update_ohlc(high, low, close)` | Update with OHLC data | None |
|
||
|
|
| `get_typical_price()` | Get typical price (HLC/3) | float |
|
||
|
|
| `get_true_range(prev_close)` | Calculate True Range | float |
|
||
|
|
|
||
|
|
### Usage Example
|
||
|
|
|
||
|
|
```python
|
||
|
|
class MyOHLCIndicator(OHLCIndicatorState):
|
||
|
|
def __init__(self, period: int):
|
||
|
|
super().__init__(period)
|
||
|
|
self.hl_sum = 0.0
|
||
|
|
self.count = 0
|
||
|
|
|
||
|
|
def _process_ohlc_data(self, high: float, low: float, close: float):
|
||
|
|
self.hl_sum += (high - low)
|
||
|
|
self.count += 1
|
||
|
|
|
||
|
|
def get_value(self) -> float:
|
||
|
|
if self.count == 0:
|
||
|
|
return 0.0
|
||
|
|
return self.hl_sum / self.count
|
||
|
|
|
||
|
|
# Usage
|
||
|
|
ohlc_indicator = MyOHLCIndicator(period=10)
|
||
|
|
ohlc_data = [(105, 95, 100), (108, 98, 102), (110, 100, 105)]
|
||
|
|
|
||
|
|
for high, low, close in ohlc_data:
|
||
|
|
ohlc_indicator.update_ohlc(high, low, close)
|
||
|
|
if ohlc_indicator.is_ready():
|
||
|
|
print(f"Average Range: {ohlc_indicator.get_value():.2f}")
|
||
|
|
print(f"Typical Price: {ohlc_indicator.get_typical_price():.2f}")
|
||
|
|
```
|
||
|
|
|
||
|
|
## Best Practices
|
||
|
|
|
||
|
|
### 1. Always Check Ready State
|
||
|
|
```python
|
||
|
|
indicator = MovingAverageState(period=20)
|
||
|
|
|
||
|
|
for price in price_data:
|
||
|
|
indicator.update(price)
|
||
|
|
|
||
|
|
# Always check if ready before using value
|
||
|
|
if indicator.is_ready():
|
||
|
|
value = indicator.get_value()
|
||
|
|
# Use the value...
|
||
|
|
```
|
||
|
|
|
||
|
|
### 2. Initialize Once, Reuse Many Times
|
||
|
|
```python
|
||
|
|
# Good: Initialize once
|
||
|
|
sma = MovingAverageState(period=20)
|
||
|
|
|
||
|
|
# Process many data points
|
||
|
|
for price in large_dataset:
|
||
|
|
sma.update(price)
|
||
|
|
if sma.is_ready():
|
||
|
|
process_signal(sma.get_value())
|
||
|
|
|
||
|
|
# Bad: Don't recreate indicators
|
||
|
|
for price in large_dataset:
|
||
|
|
sma = MovingAverageState(period=20) # Wasteful!
|
||
|
|
sma.update(price)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 3. Handle Edge Cases
|
||
|
|
```python
|
||
|
|
def safe_indicator_update(indicator, value):
|
||
|
|
"""Safely update indicator with error handling."""
|
||
|
|
try:
|
||
|
|
if value is not None and not math.isnan(value):
|
||
|
|
indicator.update(value)
|
||
|
|
return True
|
||
|
|
except Exception as e:
|
||
|
|
logger.error(f"Error updating indicator: {e}")
|
||
|
|
return False
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. Batch Updates for Multiple Indicators
|
||
|
|
```python
|
||
|
|
# Update all indicators together
|
||
|
|
indicators = [sma_20, ema_12, rsi_14]
|
||
|
|
|
||
|
|
for price in price_stream:
|
||
|
|
# Update all indicators
|
||
|
|
for indicator in indicators:
|
||
|
|
indicator.update(price)
|
||
|
|
|
||
|
|
# Check if all are ready
|
||
|
|
if all(ind.is_ready() for ind in indicators):
|
||
|
|
# Use all indicator values
|
||
|
|
values = [ind.get_value() for ind in indicators]
|
||
|
|
process_signals(values)
|
||
|
|
```
|
||
|
|
|
||
|
|
## Performance Characteristics
|
||
|
|
|
||
|
|
### Memory Usage
|
||
|
|
- **IndicatorState**: O(period) memory usage
|
||
|
|
- **SimpleIndicatorState**: O(1) memory usage
|
||
|
|
- **OHLCIndicatorState**: O(period) memory usage
|
||
|
|
|
||
|
|
### Processing Speed
|
||
|
|
- **Update Time**: O(1) per data point for all base classes
|
||
|
|
- **Value Retrieval**: O(1) for getting current value
|
||
|
|
- **Ready Check**: O(1) for checking ready state
|
||
|
|
|
||
|
|
### Scalability
|
||
|
|
```python
|
||
|
|
# Memory usage remains constant regardless of data volume
|
||
|
|
indicator = MovingAverageState(period=20)
|
||
|
|
|
||
|
|
# Process 1 million data points - memory usage stays O(20)
|
||
|
|
for i in range(1_000_000):
|
||
|
|
indicator.update(i)
|
||
|
|
if indicator.is_ready():
|
||
|
|
value = indicator.get_value() # Always O(1)
|
||
|
|
```
|
||
|
|
|
||
|
|
## Error Handling
|
||
|
|
|
||
|
|
### Common Patterns
|
||
|
|
```python
|
||
|
|
class RobustIndicator(IndicatorState):
|
||
|
|
def update(self, value: float):
|
||
|
|
try:
|
||
|
|
# Validate input
|
||
|
|
if value is None or math.isnan(value) or math.isinf(value):
|
||
|
|
self.logger.warning(f"Invalid value: {value}")
|
||
|
|
return
|
||
|
|
|
||
|
|
# Process value
|
||
|
|
self._process_value(value)
|
||
|
|
self.data_count += 1
|
||
|
|
|
||
|
|
except Exception as e:
|
||
|
|
self.logger.error(f"Error in indicator update: {e}")
|
||
|
|
|
||
|
|
def get_value(self) -> float:
|
||
|
|
try:
|
||
|
|
if not self.is_ready():
|
||
|
|
return 0.0
|
||
|
|
return self._calculate_value()
|
||
|
|
except Exception as e:
|
||
|
|
self.logger.error(f"Error calculating indicator value: {e}")
|
||
|
|
return 0.0
|
||
|
|
```
|
||
|
|
|
||
|
|
## Integration with Strategies
|
||
|
|
|
||
|
|
### Strategy Usage Pattern
|
||
|
|
```python
|
||
|
|
class MyStrategy(IncStrategyBase):
|
||
|
|
def __init__(self, name: str, params: dict = None):
|
||
|
|
super().__init__(name, params)
|
||
|
|
|
||
|
|
# Initialize indicators
|
||
|
|
self.sma = MovingAverageState(period=20)
|
||
|
|
self.rsi = RSIState(period=14)
|
||
|
|
self.atr = ATRState(period=14)
|
||
|
|
|
||
|
|
def _process_aggregated_data(self, timestamp: int, ohlcv: tuple) -> IncStrategySignal:
|
||
|
|
open_price, high, low, close, volume = ohlcv
|
||
|
|
|
||
|
|
# Update all indicators
|
||
|
|
self.sma.update(close)
|
||
|
|
self.rsi.update(close)
|
||
|
|
self.atr.update_ohlc(high, low, close)
|
||
|
|
|
||
|
|
# Check if all indicators are ready
|
||
|
|
if not all([self.sma.is_ready(), self.rsi.is_ready(), self.atr.is_ready()]):
|
||
|
|
return IncStrategySignal.HOLD()
|
||
|
|
|
||
|
|
# Use indicator values for signal generation
|
||
|
|
sma_value = self.sma.get_value()
|
||
|
|
rsi_value = self.rsi.get_value()
|
||
|
|
atr_value = self.atr.get_value()
|
||
|
|
|
||
|
|
# Generate signals based on indicator values
|
||
|
|
return self._generate_signal(close, sma_value, rsi_value, atr_value)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
*The base indicator classes provide a solid foundation for building efficient, real-time indicators that maintain constant memory usage and processing time regardless of data history length.*
|