Implement Incremental Trading Framework
- 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.
This commit is contained in:
59
IncrementalTrader/strategies/__init__.py
Normal file
59
IncrementalTrader/strategies/__init__.py
Normal file
@@ -0,0 +1,59 @@
|
||||
"""
|
||||
Incremental Trading Strategies Framework
|
||||
|
||||
This module provides the strategy framework and implementations for incremental trading.
|
||||
All strategies inherit from IncStrategyBase and support real-time data processing
|
||||
with constant memory usage.
|
||||
|
||||
Available Components:
|
||||
- Base Framework: IncStrategyBase, IncStrategySignal, TimeframeAggregator
|
||||
- Strategies: MetaTrendStrategy, RandomStrategy, BBRSStrategy
|
||||
- Indicators: Complete indicator framework in .indicators submodule
|
||||
|
||||
Example:
|
||||
from IncrementalTrader.strategies import MetaTrendStrategy, IncStrategySignal
|
||||
|
||||
# Create strategy
|
||||
strategy = MetaTrendStrategy("metatrend", params={"timeframe": "15min"})
|
||||
|
||||
# Process data
|
||||
strategy.process_data_point(timestamp, ohlcv_data)
|
||||
|
||||
# Get signals
|
||||
entry_signal = strategy.get_entry_signal()
|
||||
if entry_signal.action == "BUY":
|
||||
print(f"Entry signal with confidence: {entry_signal.confidence}")
|
||||
"""
|
||||
|
||||
# Base strategy framework (already migrated)
|
||||
from .base import (
|
||||
IncStrategyBase,
|
||||
IncStrategySignal,
|
||||
TimeframeAggregator,
|
||||
)
|
||||
|
||||
# Migrated strategies
|
||||
from .metatrend import MetaTrendStrategy, IncMetaTrendStrategy
|
||||
from .random import RandomStrategy, IncRandomStrategy
|
||||
from .bbrs import BBRSStrategy, IncBBRSStrategy
|
||||
|
||||
# Indicators submodule
|
||||
from . import indicators
|
||||
|
||||
__all__ = [
|
||||
# Base framework
|
||||
"IncStrategyBase",
|
||||
"IncStrategySignal",
|
||||
"TimeframeAggregator",
|
||||
|
||||
# Available strategies
|
||||
"MetaTrendStrategy",
|
||||
"IncMetaTrendStrategy", # Compatibility alias
|
||||
"RandomStrategy",
|
||||
"IncRandomStrategy", # Compatibility alias
|
||||
"BBRSStrategy",
|
||||
"IncBBRSStrategy", # Compatibility alias
|
||||
|
||||
# Indicators submodule
|
||||
"indicators",
|
||||
]
|
||||
637
IncrementalTrader/strategies/base.py
Normal file
637
IncrementalTrader/strategies/base.py
Normal file
@@ -0,0 +1,637 @@
|
||||
"""
|
||||
Base classes for the incremental strategy system.
|
||||
|
||||
This module contains the fundamental building blocks for all incremental trading strategies:
|
||||
- IncStrategySignal: Represents trading signals with confidence and metadata
|
||||
- IncStrategyBase: Abstract base class that all incremental strategies must inherit from
|
||||
- TimeframeAggregator: Built-in timeframe aggregation for minute-level data processing
|
||||
|
||||
The incremental approach allows strategies to:
|
||||
- Process new data points without full recalculation
|
||||
- Maintain bounded memory usage regardless of data history length
|
||||
- Provide real-time performance with minimal latency
|
||||
- Support both initialization and incremental modes
|
||||
- Accept minute-level data and internally aggregate to any timeframe
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Optional, List, Union, Any
|
||||
from collections import deque
|
||||
import logging
|
||||
import time
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IncStrategySignal:
|
||||
"""
|
||||
Represents a trading signal from an incremental strategy.
|
||||
|
||||
A signal encapsulates the strategy's recommendation along with confidence
|
||||
level, optional price target, and additional metadata.
|
||||
|
||||
Attributes:
|
||||
signal_type (str): Type of signal - "ENTRY", "EXIT", or "HOLD"
|
||||
confidence (float): Confidence level from 0.0 to 1.0
|
||||
price (Optional[float]): Optional specific price for the signal
|
||||
metadata (Dict): Additional signal data and context
|
||||
|
||||
Example:
|
||||
# Entry signal with high confidence
|
||||
signal = IncStrategySignal("ENTRY", confidence=0.8)
|
||||
|
||||
# Exit signal with stop loss price
|
||||
signal = IncStrategySignal("EXIT", confidence=1.0, price=50000,
|
||||
metadata={"type": "STOP_LOSS"})
|
||||
"""
|
||||
|
||||
def __init__(self, signal_type: str, confidence: float = 1.0,
|
||||
price: Optional[float] = None, metadata: Optional[Dict] = None):
|
||||
"""
|
||||
Initialize a strategy signal.
|
||||
|
||||
Args:
|
||||
signal_type: Type of signal ("ENTRY", "EXIT", "HOLD")
|
||||
confidence: Confidence level (0.0 to 1.0)
|
||||
price: Optional specific price for the signal
|
||||
metadata: Additional signal data and context
|
||||
"""
|
||||
self.signal_type = signal_type
|
||||
self.confidence = max(0.0, min(1.0, confidence)) # Clamp to [0,1]
|
||||
self.price = price
|
||||
self.metadata = metadata or {}
|
||||
|
||||
@classmethod
|
||||
def BUY(cls, confidence: float = 1.0, price: Optional[float] = None, **metadata):
|
||||
"""Create a BUY signal."""
|
||||
return cls("ENTRY", confidence, price, metadata)
|
||||
|
||||
@classmethod
|
||||
def SELL(cls, confidence: float = 1.0, price: Optional[float] = None, **metadata):
|
||||
"""Create a SELL signal."""
|
||||
return cls("EXIT", confidence, price, metadata)
|
||||
|
||||
@classmethod
|
||||
def HOLD(cls, confidence: float = 0.0, **metadata):
|
||||
"""Create a HOLD signal."""
|
||||
return cls("HOLD", confidence, None, metadata)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of the signal."""
|
||||
return (f"IncStrategySignal(type={self.signal_type}, "
|
||||
f"confidence={self.confidence:.2f}, "
|
||||
f"price={self.price}, metadata={self.metadata})")
|
||||
|
||||
|
||||
class TimeframeAggregator:
|
||||
"""
|
||||
Handles real-time aggregation of minute data to higher timeframes.
|
||||
|
||||
This class accumulates minute-level OHLCV data and produces complete
|
||||
bars when a timeframe period is completed. Integrated into IncStrategyBase
|
||||
to provide consistent minute-level data processing across all strategies.
|
||||
"""
|
||||
|
||||
def __init__(self, timeframe_minutes: int = 15):
|
||||
"""
|
||||
Initialize timeframe aggregator.
|
||||
|
||||
Args:
|
||||
timeframe_minutes: Target timeframe in minutes (e.g., 60 for 1h, 15 for 15min)
|
||||
"""
|
||||
self.timeframe_minutes = timeframe_minutes
|
||||
self.current_bar = None
|
||||
self.current_bar_start = None
|
||||
self.last_completed_bar = None
|
||||
|
||||
def update(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Optional[Dict[str, float]]:
|
||||
"""
|
||||
Update with new minute data and return completed bar if timeframe is complete.
|
||||
|
||||
Args:
|
||||
timestamp: Timestamp of the data
|
||||
ohlcv_data: OHLCV data dictionary
|
||||
|
||||
Returns:
|
||||
Completed OHLCV bar if timeframe period ended, None otherwise
|
||||
"""
|
||||
# Calculate which timeframe bar this timestamp belongs to
|
||||
bar_start = self._get_bar_start_time(timestamp)
|
||||
|
||||
# Check if we're starting a new bar
|
||||
if self.current_bar_start != bar_start:
|
||||
# Save the completed bar (if any)
|
||||
completed_bar = self.current_bar.copy() if self.current_bar is not None else None
|
||||
|
||||
# Start new bar
|
||||
self.current_bar_start = bar_start
|
||||
self.current_bar = {
|
||||
'timestamp': bar_start,
|
||||
'open': ohlcv_data['close'], # Use current close as open for new bar
|
||||
'high': ohlcv_data['close'],
|
||||
'low': ohlcv_data['close'],
|
||||
'close': ohlcv_data['close'],
|
||||
'volume': ohlcv_data['volume']
|
||||
}
|
||||
|
||||
# Return the completed bar (if any)
|
||||
if completed_bar is not None:
|
||||
self.last_completed_bar = completed_bar
|
||||
return completed_bar
|
||||
else:
|
||||
# Update current bar with new data
|
||||
if self.current_bar is not None:
|
||||
self.current_bar['high'] = max(self.current_bar['high'], ohlcv_data['high'])
|
||||
self.current_bar['low'] = min(self.current_bar['low'], ohlcv_data['low'])
|
||||
self.current_bar['close'] = ohlcv_data['close']
|
||||
self.current_bar['volume'] += ohlcv_data['volume']
|
||||
|
||||
return None # No completed bar yet
|
||||
|
||||
def _get_bar_start_time(self, timestamp: pd.Timestamp) -> pd.Timestamp:
|
||||
"""Calculate the start time of the timeframe bar for given timestamp.
|
||||
|
||||
This method aligns with pandas resampling to ensure consistency
|
||||
with the original strategy's bar boundaries.
|
||||
"""
|
||||
# Use pandas-style resampling alignment
|
||||
# This ensures bars align to standard boundaries (e.g., 00:00, 00:15, 00:30, 00:45)
|
||||
freq_str = f'{self.timeframe_minutes}min'
|
||||
|
||||
try:
|
||||
# Create a temporary series with the timestamp and resample to get the bar start
|
||||
temp_series = pd.Series([1], index=[timestamp])
|
||||
resampled = temp_series.resample(freq_str)
|
||||
|
||||
# Get the first group's name (which is the bar start time)
|
||||
for bar_start, _ in resampled:
|
||||
return bar_start
|
||||
except Exception:
|
||||
# Fallback to original method if resampling fails
|
||||
pass
|
||||
|
||||
# Fallback method
|
||||
minutes_since_midnight = timestamp.hour * 60 + timestamp.minute
|
||||
bar_minutes = (minutes_since_midnight // self.timeframe_minutes) * self.timeframe_minutes
|
||||
|
||||
return timestamp.replace(
|
||||
hour=bar_minutes // 60,
|
||||
minute=bar_minutes % 60,
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
|
||||
def get_current_bar(self) -> Optional[Dict[str, float]]:
|
||||
"""Get the current incomplete bar (for debugging)."""
|
||||
return self.current_bar.copy() if self.current_bar is not None else None
|
||||
|
||||
def reset(self):
|
||||
"""Reset aggregator state."""
|
||||
self.current_bar = None
|
||||
self.current_bar_start = None
|
||||
self.last_completed_bar = None
|
||||
|
||||
|
||||
class IncStrategyBase(ABC):
|
||||
"""
|
||||
Abstract base class for all incremental trading strategies.
|
||||
|
||||
This class defines the interface that all incremental strategies must implement:
|
||||
- get_minimum_buffer_size(): Specify minimum data requirements
|
||||
- process_data_point(): Process new data points incrementally
|
||||
- supports_incremental_calculation(): Whether strategy supports incremental mode
|
||||
- get_entry_signal(): Generate entry signals
|
||||
- get_exit_signal(): Generate exit signals
|
||||
|
||||
The incremental approach allows strategies to:
|
||||
- Process new data points without full recalculation
|
||||
- Maintain bounded memory usage regardless of data history length
|
||||
- Provide real-time performance with minimal latency
|
||||
- Support both initialization and incremental modes
|
||||
- Accept minute-level data and internally aggregate to any timeframe
|
||||
|
||||
New Features:
|
||||
- Built-in TimeframeAggregator for minute-level data processing
|
||||
- update_minute_data() method for real-time trading systems
|
||||
- Automatic timeframe detection and aggregation
|
||||
- Backward compatibility with existing update() methods
|
||||
|
||||
Attributes:
|
||||
name (str): Strategy name
|
||||
weight (float): Strategy weight for combination
|
||||
params (Dict): Strategy parameters
|
||||
calculation_mode (str): Current mode ('initialization' or 'incremental')
|
||||
is_warmed_up (bool): Whether strategy has sufficient data for reliable signals
|
||||
timeframe_buffers (Dict): Rolling buffers for different timeframes
|
||||
indicator_states (Dict): Internal indicator calculation states
|
||||
timeframe_aggregator (TimeframeAggregator): Built-in aggregator for minute data
|
||||
|
||||
Example:
|
||||
class MyIncStrategy(IncStrategyBase):
|
||||
def get_minimum_buffer_size(self):
|
||||
return {"15min": 50} # Strategy works on 15min timeframe
|
||||
|
||||
def process_data_point(self, timestamp, ohlcv_data):
|
||||
# Process new data incrementally
|
||||
self._update_indicators(ohlcv_data)
|
||||
return self.get_current_signal()
|
||||
|
||||
def get_entry_signal(self):
|
||||
# Generate signal based on current state
|
||||
if self._should_enter():
|
||||
return IncStrategySignal.BUY(confidence=0.8)
|
||||
return IncStrategySignal.HOLD()
|
||||
|
||||
# Usage with minute-level data:
|
||||
strategy = MyIncStrategy(params={"timeframe_minutes": 15})
|
||||
for minute_data in live_stream:
|
||||
signal = strategy.process_data_point(minute_data['timestamp'], minute_data)
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, weight: float = 1.0, params: Optional[Dict] = None):
|
||||
"""
|
||||
Initialize the incremental strategy base.
|
||||
|
||||
Args:
|
||||
name: Strategy name/identifier
|
||||
weight: Strategy weight for combination (default: 1.0)
|
||||
params: Strategy-specific parameters
|
||||
"""
|
||||
self.name = name
|
||||
self.weight = weight
|
||||
self.params = params or {}
|
||||
|
||||
# Calculation state
|
||||
self._calculation_mode = "initialization"
|
||||
self._is_warmed_up = False
|
||||
self._data_points_received = 0
|
||||
|
||||
# Data management
|
||||
self._timeframe_buffers = {}
|
||||
self._timeframe_last_update = {}
|
||||
self._indicator_states = {}
|
||||
self._last_signals = {}
|
||||
self._signal_history = deque(maxlen=100) # Keep last 100 signals
|
||||
|
||||
# Performance tracking
|
||||
self._performance_metrics = {
|
||||
'update_times': deque(maxlen=1000),
|
||||
'signal_generation_times': deque(maxlen=1000),
|
||||
'state_validation_failures': 0,
|
||||
'data_gaps_handled': 0,
|
||||
'minute_data_points_processed': 0,
|
||||
'timeframe_bars_completed': 0
|
||||
}
|
||||
|
||||
# Configuration
|
||||
self._buffer_size_multiplier = 1.5 # Extra buffer for safety
|
||||
self._state_validation_enabled = True
|
||||
self._max_acceptable_gap = pd.Timedelta(minutes=5)
|
||||
|
||||
# Timeframe aggregation
|
||||
self._primary_timeframe_minutes = self._extract_timeframe_minutes()
|
||||
self._timeframe_aggregator = None
|
||||
if self._primary_timeframe_minutes > 1:
|
||||
self._timeframe_aggregator = TimeframeAggregator(self._primary_timeframe_minutes)
|
||||
|
||||
logger.info(f"Initialized incremental strategy: {self.name}")
|
||||
|
||||
def _extract_timeframe_minutes(self) -> int:
|
||||
"""Extract timeframe in minutes from strategy parameters."""
|
||||
timeframe = self.params.get("timeframe", "1min")
|
||||
|
||||
if isinstance(timeframe, str):
|
||||
if timeframe.endswith("min"):
|
||||
return int(timeframe[:-3])
|
||||
elif timeframe.endswith("h"):
|
||||
return int(timeframe[:-1]) * 60
|
||||
elif timeframe.endswith("d"):
|
||||
return int(timeframe[:-1]) * 24 * 60
|
||||
elif isinstance(timeframe, int):
|
||||
return timeframe
|
||||
|
||||
# Default to 1 minute
|
||||
return 1
|
||||
|
||||
def process_data_point(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Optional[IncStrategySignal]:
|
||||
"""
|
||||
Process a new data point and return signal if generated.
|
||||
|
||||
This is the main entry point for incremental processing. It handles
|
||||
timeframe aggregation, buffer updates, and signal generation.
|
||||
|
||||
Args:
|
||||
timestamp: Timestamp of the data point
|
||||
ohlcv_data: OHLCV data dictionary
|
||||
|
||||
Returns:
|
||||
IncStrategySignal if a signal is generated, None otherwise
|
||||
"""
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Update performance metrics
|
||||
self._performance_metrics['minute_data_points_processed'] += 1
|
||||
self._data_points_received += 1
|
||||
|
||||
# Handle timeframe aggregation if needed
|
||||
if self._timeframe_aggregator is not None:
|
||||
completed_bar = self._timeframe_aggregator.update(timestamp, ohlcv_data)
|
||||
if completed_bar is not None:
|
||||
# Process the completed timeframe bar
|
||||
self._performance_metrics['timeframe_bars_completed'] += 1
|
||||
return self._process_timeframe_bar(completed_bar['timestamp'], completed_bar)
|
||||
else:
|
||||
# No complete bar yet, return None
|
||||
return None
|
||||
else:
|
||||
# Process minute data directly
|
||||
return self._process_timeframe_bar(timestamp, ohlcv_data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing data point in {self.name}: {e}")
|
||||
return None
|
||||
finally:
|
||||
# Track processing time
|
||||
processing_time = time.time() - start_time
|
||||
self._performance_metrics['update_times'].append(processing_time)
|
||||
|
||||
def _process_timeframe_bar(self, timestamp: pd.Timestamp, ohlcv_data: Dict[str, float]) -> Optional[IncStrategySignal]:
|
||||
"""Process a complete timeframe bar and generate signals."""
|
||||
# Update timeframe buffers
|
||||
self._update_timeframe_buffers(ohlcv_data, timestamp)
|
||||
|
||||
# Call strategy-specific calculation
|
||||
self.calculate_on_data(ohlcv_data, timestamp)
|
||||
|
||||
# Check if strategy is warmed up
|
||||
if not self._is_warmed_up:
|
||||
self._check_warmup_status()
|
||||
|
||||
# Generate signal if warmed up
|
||||
if self._is_warmed_up:
|
||||
signal_start = time.time()
|
||||
signal = self.get_current_signal()
|
||||
signal_time = time.time() - signal_start
|
||||
self._performance_metrics['signal_generation_times'].append(signal_time)
|
||||
|
||||
# Store signal in history
|
||||
if signal and signal.signal_type != "HOLD":
|
||||
self._signal_history.append({
|
||||
'timestamp': timestamp,
|
||||
'signal': signal,
|
||||
'strategy_state': self.get_current_state_summary()
|
||||
})
|
||||
|
||||
return signal
|
||||
|
||||
return None
|
||||
|
||||
def _check_warmup_status(self):
|
||||
"""Check if strategy has enough data to be considered warmed up."""
|
||||
min_buffer_sizes = self.get_minimum_buffer_size()
|
||||
|
||||
for timeframe, min_size in min_buffer_sizes.items():
|
||||
buffer = self._timeframe_buffers.get(timeframe, deque())
|
||||
if len(buffer) < min_size:
|
||||
return # Not enough data yet
|
||||
|
||||
# All buffers have sufficient data
|
||||
self._is_warmed_up = True
|
||||
self._calculation_mode = "incremental"
|
||||
logger.info(f"Strategy {self.name} is now warmed up after {self._data_points_received} data points")
|
||||
|
||||
def get_current_signal(self) -> IncStrategySignal:
|
||||
"""Get the current signal based on strategy state."""
|
||||
# Try entry signal first
|
||||
entry_signal = self.get_entry_signal()
|
||||
if entry_signal and entry_signal.signal_type != "HOLD":
|
||||
return entry_signal
|
||||
|
||||
# Check exit signal
|
||||
exit_signal = self.get_exit_signal()
|
||||
if exit_signal and exit_signal.signal_type != "HOLD":
|
||||
return exit_signal
|
||||
|
||||
# Default to hold
|
||||
return IncStrategySignal.HOLD()
|
||||
|
||||
def get_current_incomplete_bar(self) -> Optional[Dict[str, float]]:
|
||||
"""Get current incomplete timeframe bar (for debugging)."""
|
||||
if self._timeframe_aggregator is not None:
|
||||
return self._timeframe_aggregator.get_current_bar()
|
||||
return None
|
||||
|
||||
# Properties
|
||||
@property
|
||||
def calculation_mode(self) -> str:
|
||||
"""Get current calculation mode."""
|
||||
return self._calculation_mode
|
||||
|
||||
@property
|
||||
def is_warmed_up(self) -> bool:
|
||||
"""Check if strategy is warmed up."""
|
||||
return self._is_warmed_up
|
||||
|
||||
# Abstract methods that must be implemented by strategies
|
||||
@abstractmethod
|
||||
def get_minimum_buffer_size(self) -> Dict[str, int]:
|
||||
"""
|
||||
Get minimum buffer sizes for each timeframe.
|
||||
|
||||
This method specifies how much historical data the strategy needs
|
||||
for each timeframe to generate reliable signals.
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: Mapping of timeframe to minimum buffer size
|
||||
|
||||
Example:
|
||||
return {"15min": 50, "1h": 24} # 50 15min bars, 24 1h bars
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
|
||||
"""
|
||||
Process new data point and update internal indicators.
|
||||
|
||||
This method is called for each new timeframe bar and should update
|
||||
all internal indicators and strategy state incrementally.
|
||||
|
||||
Args:
|
||||
new_data_point: New OHLCV data point
|
||||
timestamp: Timestamp of the data point
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def supports_incremental_calculation(self) -> bool:
|
||||
"""
|
||||
Check if strategy supports incremental calculation.
|
||||
|
||||
Returns:
|
||||
bool: True if strategy can process data incrementally
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_entry_signal(self) -> IncStrategySignal:
|
||||
"""
|
||||
Generate entry signal based on current strategy state.
|
||||
|
||||
This method should use the current internal state to determine
|
||||
whether an entry signal should be generated.
|
||||
|
||||
Returns:
|
||||
IncStrategySignal: Entry signal with confidence level
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_exit_signal(self) -> IncStrategySignal:
|
||||
"""
|
||||
Generate exit signal based on current strategy state.
|
||||
|
||||
This method should use the current internal state to determine
|
||||
whether an exit signal should be generated.
|
||||
|
||||
Returns:
|
||||
IncStrategySignal: Exit signal with confidence level
|
||||
"""
|
||||
pass
|
||||
|
||||
# Utility methods
|
||||
def get_confidence(self) -> float:
|
||||
"""
|
||||
Get strategy confidence for the current market state.
|
||||
|
||||
Default implementation returns 1.0. Strategies can override
|
||||
this to provide dynamic confidence based on market conditions.
|
||||
|
||||
Returns:
|
||||
float: Confidence level (0.0 to 1.0)
|
||||
"""
|
||||
return 1.0
|
||||
|
||||
def reset_calculation_state(self) -> None:
|
||||
"""Reset internal calculation state for reinitialization."""
|
||||
self._calculation_mode = "initialization"
|
||||
self._is_warmed_up = False
|
||||
self._data_points_received = 0
|
||||
self._timeframe_buffers.clear()
|
||||
self._timeframe_last_update.clear()
|
||||
self._indicator_states.clear()
|
||||
self._last_signals.clear()
|
||||
self._signal_history.clear()
|
||||
|
||||
# Reset timeframe aggregator
|
||||
if self._timeframe_aggregator is not None:
|
||||
self._timeframe_aggregator.reset()
|
||||
|
||||
# Reset performance metrics
|
||||
for key in self._performance_metrics:
|
||||
if isinstance(self._performance_metrics[key], deque):
|
||||
self._performance_metrics[key].clear()
|
||||
else:
|
||||
self._performance_metrics[key] = 0
|
||||
|
||||
def get_current_state_summary(self) -> Dict[str, Any]:
|
||||
"""Get summary of current calculation state for debugging."""
|
||||
return {
|
||||
'strategy_name': self.name,
|
||||
'calculation_mode': self._calculation_mode,
|
||||
'is_warmed_up': self._is_warmed_up,
|
||||
'data_points_received': self._data_points_received,
|
||||
'timeframes': list(self._timeframe_buffers.keys()),
|
||||
'buffer_sizes': {tf: len(buf) for tf, buf in self._timeframe_buffers.items()},
|
||||
'indicator_states': {name: state.get_state_summary() if hasattr(state, 'get_state_summary') else str(state)
|
||||
for name, state in self._indicator_states.items()},
|
||||
'last_signals': self._last_signals,
|
||||
'timeframe_aggregator': {
|
||||
'enabled': self._timeframe_aggregator is not None,
|
||||
'primary_timeframe_minutes': self._primary_timeframe_minutes,
|
||||
'current_incomplete_bar': self.get_current_incomplete_bar()
|
||||
},
|
||||
'performance_metrics': {
|
||||
'avg_update_time': sum(self._performance_metrics['update_times']) / len(self._performance_metrics['update_times'])
|
||||
if self._performance_metrics['update_times'] else 0,
|
||||
'avg_signal_time': sum(self._performance_metrics['signal_generation_times']) / len(self._performance_metrics['signal_generation_times'])
|
||||
if self._performance_metrics['signal_generation_times'] else 0,
|
||||
'validation_failures': self._performance_metrics['state_validation_failures'],
|
||||
'data_gaps_handled': self._performance_metrics['data_gaps_handled'],
|
||||
'minute_data_points_processed': self._performance_metrics['minute_data_points_processed'],
|
||||
'timeframe_bars_completed': self._performance_metrics['timeframe_bars_completed']
|
||||
}
|
||||
}
|
||||
|
||||
def _update_timeframe_buffers(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
|
||||
"""Update all timeframe buffers with new data point."""
|
||||
# Get minimum buffer sizes
|
||||
min_buffer_sizes = self.get_minimum_buffer_size()
|
||||
|
||||
for timeframe in min_buffer_sizes.keys():
|
||||
# Calculate actual buffer size with multiplier
|
||||
min_size = min_buffer_sizes[timeframe]
|
||||
actual_buffer_size = int(min_size * self._buffer_size_multiplier)
|
||||
|
||||
# Initialize buffer if needed
|
||||
if timeframe not in self._timeframe_buffers:
|
||||
self._timeframe_buffers[timeframe] = deque(maxlen=actual_buffer_size)
|
||||
self._timeframe_last_update[timeframe] = None
|
||||
|
||||
# Add data point to buffer
|
||||
data_point = new_data_point.copy()
|
||||
data_point['timestamp'] = timestamp
|
||||
self._timeframe_buffers[timeframe].append(data_point)
|
||||
self._timeframe_last_update[timeframe] = timestamp
|
||||
|
||||
def _get_timeframe_buffer(self, timeframe: str) -> pd.DataFrame:
|
||||
"""Get current buffer for specific timeframe as DataFrame."""
|
||||
if timeframe not in self._timeframe_buffers:
|
||||
return pd.DataFrame()
|
||||
|
||||
buffer_data = list(self._timeframe_buffers[timeframe])
|
||||
if not buffer_data:
|
||||
return pd.DataFrame()
|
||||
|
||||
df = pd.DataFrame(buffer_data)
|
||||
if 'timestamp' in df.columns:
|
||||
df = df.set_index('timestamp')
|
||||
|
||||
return df
|
||||
|
||||
def handle_data_gap(self, gap_duration: pd.Timedelta) -> None:
|
||||
"""Handle gaps in data stream."""
|
||||
self._performance_metrics['data_gaps_handled'] += 1
|
||||
|
||||
if gap_duration > self._max_acceptable_gap:
|
||||
logger.warning(f"Data gap {gap_duration} exceeds maximum acceptable gap {self._max_acceptable_gap}")
|
||||
self._trigger_reinitialization()
|
||||
else:
|
||||
logger.info(f"Handling acceptable data gap: {gap_duration}")
|
||||
# For small gaps, continue with current state
|
||||
|
||||
def _trigger_reinitialization(self) -> None:
|
||||
"""Trigger strategy reinitialization due to data gap or corruption."""
|
||||
logger.info(f"Triggering reinitialization for strategy {self.name}")
|
||||
self.reset_calculation_state()
|
||||
|
||||
# Compatibility methods for original strategy interface
|
||||
def get_timeframes(self) -> List[str]:
|
||||
"""Get required timeframes (compatibility method)."""
|
||||
return list(self.get_minimum_buffer_size().keys())
|
||||
|
||||
def initialize(self, backtester) -> None:
|
||||
"""Initialize strategy (compatibility method)."""
|
||||
# This method provides compatibility with the original strategy interface
|
||||
# The actual initialization happens through the incremental interface
|
||||
self.initialized = True
|
||||
logger.info(f"Incremental strategy {self.name} initialized in compatibility mode")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of the strategy."""
|
||||
return (f"{self.__class__.__name__}(name={self.name}, "
|
||||
f"weight={self.weight}, mode={self._calculation_mode}, "
|
||||
f"warmed_up={self._is_warmed_up}, "
|
||||
f"data_points={self._data_points_received})")
|
||||
510
IncrementalTrader/strategies/bbrs.py
Normal file
510
IncrementalTrader/strategies/bbrs.py
Normal file
@@ -0,0 +1,510 @@
|
||||
"""
|
||||
Incremental BBRS Strategy (Bollinger Bands + RSI Strategy)
|
||||
|
||||
This module implements an incremental version of the Bollinger Bands + RSI Strategy (BBRS)
|
||||
for real-time data processing. It maintains constant memory usage and provides
|
||||
identical results to the batch implementation after the warm-up period.
|
||||
|
||||
Key Features:
|
||||
- Accepts minute-level data input for real-time compatibility
|
||||
- Internal timeframe aggregation (1min, 5min, 15min, 1h, etc.)
|
||||
- Incremental Bollinger Bands calculation
|
||||
- Incremental RSI calculation with Wilder's smoothing
|
||||
- Market regime detection (trending vs sideways)
|
||||
- Real-time signal generation
|
||||
- Constant memory usage
|
||||
"""
|
||||
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import Dict, Optional, List, Any, Tuple, Union
|
||||
import logging
|
||||
from collections import deque
|
||||
|
||||
from .base import IncStrategyBase, IncStrategySignal
|
||||
from .indicators.bollinger_bands import BollingerBandsState
|
||||
from .indicators.rsi import RSIState
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BBRSStrategy(IncStrategyBase):
|
||||
"""
|
||||
Incremental BBRS (Bollinger Bands + RSI) strategy implementation.
|
||||
|
||||
This strategy combines Bollinger Bands and RSI indicators to detect market
|
||||
conditions and generate trading signals. It adapts its behavior based on
|
||||
market regime detection (trending vs sideways markets).
|
||||
|
||||
The strategy uses different Bollinger Band multipliers and RSI thresholds
|
||||
for different market regimes:
|
||||
- Trending markets: Breakout strategy with higher BB multiplier
|
||||
- Sideways markets: Mean reversion strategy with lower BB multiplier
|
||||
|
||||
Parameters:
|
||||
timeframe (str): Primary timeframe for analysis (default: "1h")
|
||||
bb_period (int): Bollinger Bands period (default: 20)
|
||||
rsi_period (int): RSI period (default: 14)
|
||||
bb_width_threshold (float): BB width threshold for regime detection (default: 0.05)
|
||||
trending_bb_multiplier (float): BB multiplier for trending markets (default: 2.5)
|
||||
sideways_bb_multiplier (float): BB multiplier for sideways markets (default: 1.8)
|
||||
trending_rsi_thresholds (list): RSI thresholds for trending markets (default: [30, 70])
|
||||
sideways_rsi_thresholds (list): RSI thresholds for sideways markets (default: [40, 60])
|
||||
squeeze_strategy (bool): Enable squeeze strategy (default: True)
|
||||
enable_logging (bool): Enable detailed logging (default: False)
|
||||
|
||||
Example:
|
||||
strategy = BBRSStrategy("bbrs", weight=1.0, params={
|
||||
"timeframe": "1h",
|
||||
"bb_period": 20,
|
||||
"rsi_period": 14,
|
||||
"bb_width_threshold": 0.05,
|
||||
"trending_bb_multiplier": 2.5,
|
||||
"sideways_bb_multiplier": 1.8,
|
||||
"trending_rsi_thresholds": [30, 70],
|
||||
"sideways_rsi_thresholds": [40, 60],
|
||||
"squeeze_strategy": True
|
||||
})
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "bbrs", weight: float = 1.0, params: Optional[Dict] = None):
|
||||
"""Initialize the incremental BBRS strategy."""
|
||||
super().__init__(name, weight, params)
|
||||
|
||||
# Strategy configuration
|
||||
self.primary_timeframe = self.params.get("timeframe", "1h")
|
||||
self.bb_period = self.params.get("bb_period", 20)
|
||||
self.rsi_period = self.params.get("rsi_period", 14)
|
||||
self.bb_width_threshold = self.params.get("bb_width_threshold", 0.05)
|
||||
|
||||
# Market regime specific parameters
|
||||
self.trending_bb_multiplier = self.params.get("trending_bb_multiplier", 2.5)
|
||||
self.sideways_bb_multiplier = self.params.get("sideways_bb_multiplier", 1.8)
|
||||
self.trending_rsi_thresholds = tuple(self.params.get("trending_rsi_thresholds", [30, 70]))
|
||||
self.sideways_rsi_thresholds = tuple(self.params.get("sideways_rsi_thresholds", [40, 60]))
|
||||
|
||||
self.squeeze_strategy = self.params.get("squeeze_strategy", True)
|
||||
self.enable_logging = self.params.get("enable_logging", False)
|
||||
|
||||
# Configure logging level
|
||||
if self.enable_logging:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
# Initialize indicators with different multipliers for regime detection
|
||||
self.bb_trending = BollingerBandsState(self.bb_period, self.trending_bb_multiplier)
|
||||
self.bb_sideways = BollingerBandsState(self.bb_period, self.sideways_bb_multiplier)
|
||||
self.bb_reference = BollingerBandsState(self.bb_period, 2.0) # For regime detection
|
||||
self.rsi = RSIState(self.rsi_period)
|
||||
|
||||
# Volume tracking for volume analysis
|
||||
self.volume_history = deque(maxlen=20) # 20-period volume MA
|
||||
self.volume_sum = 0.0
|
||||
self.volume_ma = None
|
||||
|
||||
# Strategy state
|
||||
self.current_price = None
|
||||
self.current_volume = None
|
||||
self.current_market_regime = "trending" # Default to trending
|
||||
self.last_bb_result = None
|
||||
self.last_rsi_value = None
|
||||
|
||||
# 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"BBRSStrategy initialized: timeframe={self.primary_timeframe}, "
|
||||
f"bb_period={self.bb_period}, rsi_period={self.rsi_period}, "
|
||||
f"aggregation_enabled={self._timeframe_aggregator is not None}")
|
||||
|
||||
def get_minimum_buffer_size(self) -> Dict[str, int]:
|
||||
"""
|
||||
Return minimum data points needed for reliable BBRS calculations.
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: {timeframe: min_points} mapping
|
||||
"""
|
||||
# Need enough data for BB, RSI, and volume MA
|
||||
min_buffer_size = max(self.bb_period, self.rsi_period, 20) * 2 + 10
|
||||
|
||||
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.
|
||||
|
||||
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}")
|
||||
|
||||
close_price = float(new_data_point['close'])
|
||||
volume = float(new_data_point['volume'])
|
||||
|
||||
# Update indicators
|
||||
bb_trending_result = self.bb_trending.update(close_price)
|
||||
bb_sideways_result = self.bb_sideways.update(close_price)
|
||||
bb_reference_result = self.bb_reference.update(close_price)
|
||||
rsi_value = self.rsi.update(close_price)
|
||||
|
||||
# Update volume tracking
|
||||
self._update_volume_tracking(volume)
|
||||
|
||||
# Determine market regime
|
||||
self.current_market_regime = self._determine_market_regime(bb_reference_result)
|
||||
|
||||
# Select appropriate BB values based on regime
|
||||
if self.current_market_regime == "sideways":
|
||||
self.last_bb_result = bb_sideways_result
|
||||
else: # trending
|
||||
self.last_bb_result = bb_trending_result
|
||||
|
||||
# Store current state
|
||||
self.current_price = close_price
|
||||
self.current_volume = volume
|
||||
self.last_rsi_value = rsi_value
|
||||
self._data_points_received += 1
|
||||
|
||||
# Update warm-up status
|
||||
if not self._is_warmed_up and self.is_warmed_up():
|
||||
self._is_warmed_up = True
|
||||
logger.info(f"BBRSStrategy warmed up after {self._update_count} data points")
|
||||
|
||||
if self.enable_logging and self._update_count % 10 == 0:
|
||||
logger.debug(f"BBRS state: price=${close_price:.2f}, "
|
||||
f"regime={self.current_market_regime}, "
|
||||
f"rsi={rsi_value:.1f}, "
|
||||
f"bb_width={bb_reference_result.get('bandwidth', 0):.4f}")
|
||||
|
||||
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 BBRS strategy logic.
|
||||
|
||||
Returns:
|
||||
IncStrategySignal: Entry signal if conditions are met, hold signal otherwise
|
||||
"""
|
||||
if not self.is_warmed_up():
|
||||
return IncStrategySignal.HOLD()
|
||||
|
||||
# Check for entry condition
|
||||
if self._check_entry_condition():
|
||||
self._signal_count["entry"] += 1
|
||||
self._last_entry_signal = {
|
||||
'timestamp': self._last_update_time,
|
||||
'price': self.current_price,
|
||||
'market_regime': self.current_market_regime,
|
||||
'rsi': self.last_rsi_value,
|
||||
'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={
|
||||
"market_regime": self.current_market_regime,
|
||||
"rsi": self.last_rsi_value,
|
||||
"bb_position": self._get_bb_position(),
|
||||
"signal_count": self._signal_count["entry"]
|
||||
})
|
||||
|
||||
return IncStrategySignal.HOLD()
|
||||
|
||||
def get_exit_signal(self) -> IncStrategySignal:
|
||||
"""
|
||||
Generate exit signal based on BBRS strategy logic.
|
||||
|
||||
Returns:
|
||||
IncStrategySignal: Exit signal if conditions are met, hold signal otherwise
|
||||
"""
|
||||
if not self.is_warmed_up():
|
||||
return IncStrategySignal.HOLD()
|
||||
|
||||
# Check for exit condition
|
||||
if self._check_exit_condition():
|
||||
self._signal_count["exit"] += 1
|
||||
self._last_exit_signal = {
|
||||
'timestamp': self._last_update_time,
|
||||
'price': self.current_price,
|
||||
'market_regime': self.current_market_regime,
|
||||
'rsi': self.last_rsi_value,
|
||||
'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={
|
||||
"market_regime": self.current_market_regime,
|
||||
"rsi": self.last_rsi_value,
|
||||
"bb_position": self._get_bb_position(),
|
||||
"signal_count": self._signal_count["exit"]
|
||||
})
|
||||
|
||||
return IncStrategySignal.HOLD()
|
||||
|
||||
def get_confidence(self) -> float:
|
||||
"""
|
||||
Get strategy confidence based on signal strength.
|
||||
|
||||
Returns:
|
||||
float: Confidence level (0.0 to 1.0)
|
||||
"""
|
||||
if not self.is_warmed_up():
|
||||
return 0.0
|
||||
|
||||
# Higher confidence when signals are clear
|
||||
if self._check_entry_condition() or self._check_exit_condition():
|
||||
return 1.0
|
||||
|
||||
# Medium confidence during normal operation
|
||||
return 0.5
|
||||
|
||||
def _update_volume_tracking(self, volume: float) -> None:
|
||||
"""Update volume moving average tracking."""
|
||||
# Update rolling sum
|
||||
if len(self.volume_history) == 20: # maxlen reached
|
||||
self.volume_sum -= self.volume_history[0]
|
||||
|
||||
self.volume_history.append(volume)
|
||||
self.volume_sum += volume
|
||||
|
||||
# Calculate moving average
|
||||
if len(self.volume_history) > 0:
|
||||
self.volume_ma = self.volume_sum / len(self.volume_history)
|
||||
else:
|
||||
self.volume_ma = volume
|
||||
|
||||
def _determine_market_regime(self, bb_reference: Dict[str, float]) -> str:
|
||||
"""
|
||||
Determine market regime based on Bollinger Band width.
|
||||
|
||||
Args:
|
||||
bb_reference: Reference BB result for regime detection
|
||||
|
||||
Returns:
|
||||
"sideways" or "trending"
|
||||
"""
|
||||
if not self.bb_reference.is_warmed_up():
|
||||
return "trending" # Default to trending during warm-up
|
||||
|
||||
bb_width = bb_reference['bandwidth']
|
||||
|
||||
if bb_width < self.bb_width_threshold:
|
||||
return "sideways"
|
||||
else:
|
||||
return "trending"
|
||||
|
||||
def _check_volume_spike(self) -> bool:
|
||||
"""Check if current volume represents a spike (≥1.5× average)."""
|
||||
if self.volume_ma is None or self.volume_ma == 0 or self.current_volume is None:
|
||||
return False
|
||||
|
||||
return self.current_volume >= 1.5 * self.volume_ma
|
||||
|
||||
def _get_bb_position(self) -> str:
|
||||
"""Get current price position relative to Bollinger Bands."""
|
||||
if not self.last_bb_result or self.current_price is None:
|
||||
return 'unknown'
|
||||
|
||||
upper_band = self.last_bb_result['upper_band']
|
||||
lower_band = self.last_bb_result['lower_band']
|
||||
|
||||
if self.current_price > upper_band:
|
||||
return 'above_upper'
|
||||
elif self.current_price < lower_band:
|
||||
return 'below_lower'
|
||||
else:
|
||||
return 'between_bands'
|
||||
|
||||
def _check_entry_condition(self) -> bool:
|
||||
"""
|
||||
Check if entry condition is met based on market regime.
|
||||
|
||||
Returns:
|
||||
bool: True if entry condition is met
|
||||
"""
|
||||
if not self.is_warmed_up() or self.last_bb_result is None:
|
||||
return False
|
||||
|
||||
if np.isnan(self.last_rsi_value):
|
||||
return False
|
||||
|
||||
upper_band = self.last_bb_result['upper_band']
|
||||
lower_band = self.last_bb_result['lower_band']
|
||||
|
||||
if self.current_market_regime == "sideways":
|
||||
# Sideways market (Mean Reversion)
|
||||
rsi_low, rsi_high = self.sideways_rsi_thresholds
|
||||
buy_condition = (self.current_price <= lower_band) and (self.last_rsi_value <= rsi_low)
|
||||
|
||||
if self.squeeze_strategy:
|
||||
# Add volume contraction filter for sideways markets
|
||||
volume_contraction = self.current_volume < 0.7 * (self.volume_ma or self.current_volume)
|
||||
buy_condition = buy_condition and volume_contraction
|
||||
|
||||
return buy_condition
|
||||
|
||||
else: # trending
|
||||
# Trending market (Breakout Mode)
|
||||
volume_spike = self._check_volume_spike()
|
||||
buy_condition = (self.current_price < lower_band) and (self.last_rsi_value < 50) and volume_spike
|
||||
|
||||
return buy_condition
|
||||
|
||||
def _check_exit_condition(self) -> bool:
|
||||
"""
|
||||
Check if exit condition is met based on market regime.
|
||||
|
||||
Returns:
|
||||
bool: True if exit condition is met
|
||||
"""
|
||||
if not self.is_warmed_up() or self.last_bb_result is None:
|
||||
return False
|
||||
|
||||
if np.isnan(self.last_rsi_value):
|
||||
return False
|
||||
|
||||
upper_band = self.last_bb_result['upper_band']
|
||||
lower_band = self.last_bb_result['lower_band']
|
||||
|
||||
if self.current_market_regime == "sideways":
|
||||
# Sideways market (Mean Reversion)
|
||||
rsi_low, rsi_high = self.sideways_rsi_thresholds
|
||||
sell_condition = (self.current_price >= upper_band) and (self.last_rsi_value >= rsi_high)
|
||||
|
||||
if self.squeeze_strategy:
|
||||
# Add volume contraction filter for sideways markets
|
||||
volume_contraction = self.current_volume < 0.7 * (self.volume_ma or self.current_volume)
|
||||
sell_condition = sell_condition and volume_contraction
|
||||
|
||||
return sell_condition
|
||||
|
||||
else: # trending
|
||||
# Trending market (Breakout Mode)
|
||||
volume_spike = self._check_volume_spike()
|
||||
sell_condition = (self.current_price > upper_band) and (self.last_rsi_value > 50) and volume_spike
|
||||
|
||||
return sell_condition
|
||||
|
||||
def is_warmed_up(self) -> bool:
|
||||
"""
|
||||
Check if strategy is warmed up and ready for reliable signals.
|
||||
|
||||
Returns:
|
||||
True if all indicators are warmed up
|
||||
"""
|
||||
return (self.bb_trending.is_warmed_up() and
|
||||
self.bb_sideways.is_warmed_up() and
|
||||
self.bb_reference.is_warmed_up() and
|
||||
self.rsi.is_warmed_up() and
|
||||
len(self.volume_history) >= 20)
|
||||
|
||||
def reset_calculation_state(self) -> None:
|
||||
"""Reset internal calculation state for reinitialization."""
|
||||
super().reset_calculation_state()
|
||||
|
||||
# Reset indicators
|
||||
self.bb_trending.reset()
|
||||
self.bb_sideways.reset()
|
||||
self.bb_reference.reset()
|
||||
self.rsi.reset()
|
||||
|
||||
# Reset volume tracking
|
||||
self.volume_history.clear()
|
||||
self.volume_sum = 0.0
|
||||
self.volume_ma = None
|
||||
|
||||
# Reset strategy state
|
||||
self.current_price = None
|
||||
self.current_volume = None
|
||||
self.current_market_regime = "trending"
|
||||
self.last_bb_result = None
|
||||
self.last_rsi_value = None
|
||||
|
||||
# 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("BBRSStrategy state reset")
|
||||
|
||||
def get_current_state_summary(self) -> Dict[str, Any]:
|
||||
"""Get detailed state summary for debugging and monitoring."""
|
||||
base_summary = super().get_current_state_summary()
|
||||
|
||||
# Add BBRS-specific state
|
||||
base_summary.update({
|
||||
'primary_timeframe': self.primary_timeframe,
|
||||
'current_price': self.current_price,
|
||||
'current_volume': self.current_volume,
|
||||
'volume_ma': self.volume_ma,
|
||||
'current_market_regime': self.current_market_regime,
|
||||
'last_rsi_value': self.last_rsi_value,
|
||||
'bb_position': self._get_bb_position(),
|
||||
'volume_spike': self._check_volume_spike(),
|
||||
'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,
|
||||
'last_entry_signal': self._last_entry_signal,
|
||||
'last_exit_signal': self._last_exit_signal,
|
||||
'indicators_warmed_up': {
|
||||
'bb_trending': self.bb_trending.is_warmed_up(),
|
||||
'bb_sideways': self.bb_sideways.is_warmed_up(),
|
||||
'bb_reference': self.bb_reference.is_warmed_up(),
|
||||
'rsi': self.rsi.is_warmed_up(),
|
||||
'volume_tracking': len(self.volume_history) >= 20
|
||||
},
|
||||
'config': {
|
||||
'bb_period': self.bb_period,
|
||||
'rsi_period': self.rsi_period,
|
||||
'bb_width_threshold': self.bb_width_threshold,
|
||||
'trending_bb_multiplier': self.trending_bb_multiplier,
|
||||
'sideways_bb_multiplier': self.sideways_bb_multiplier,
|
||||
'trending_rsi_thresholds': self.trending_rsi_thresholds,
|
||||
'sideways_rsi_thresholds': self.sideways_rsi_thresholds,
|
||||
'squeeze_strategy': self.squeeze_strategy
|
||||
}
|
||||
})
|
||||
|
||||
return base_summary
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of the strategy."""
|
||||
return (f"BBRSStrategy(timeframe={self.primary_timeframe}, "
|
||||
f"bb_period={self.bb_period}, rsi_period={self.rsi_period}, "
|
||||
f"regime={self.current_market_regime}, "
|
||||
f"warmed_up={self.is_warmed_up()}, "
|
||||
f"updates={self._update_count})")
|
||||
|
||||
|
||||
# Compatibility alias for easier imports
|
||||
IncBBRSStrategy = BBRSStrategy
|
||||
91
IncrementalTrader/strategies/indicators/__init__.py
Normal file
91
IncrementalTrader/strategies/indicators/__init__.py
Normal file
@@ -0,0 +1,91 @@
|
||||
"""
|
||||
Incremental Indicators Framework
|
||||
|
||||
This module provides incremental indicator implementations for real-time trading strategies.
|
||||
All indicators maintain constant memory usage and provide identical results to traditional
|
||||
batch calculations.
|
||||
|
||||
Available Indicators:
|
||||
- Base classes: IndicatorState, SimpleIndicatorState, OHLCIndicatorState
|
||||
- Moving Averages: MovingAverageState, ExponentialMovingAverageState
|
||||
- Volatility: ATRState, SimpleATRState
|
||||
- Trend: SupertrendState, SupertrendCollection
|
||||
- Bollinger Bands: BollingerBandsState, BollingerBandsOHLCState
|
||||
- RSI: RSIState, SimpleRSIState
|
||||
|
||||
Example:
|
||||
from IncrementalTrader.strategies.indicators import SupertrendState, ATRState
|
||||
|
||||
# Create indicators
|
||||
atr = ATRState(period=14)
|
||||
supertrend = SupertrendState(period=10, multiplier=3.0)
|
||||
|
||||
# Update with OHLC data
|
||||
ohlc = {'open': 100, 'high': 105, 'low': 98, 'close': 103}
|
||||
atr_value = atr.update(ohlc)
|
||||
st_result = supertrend.update(ohlc)
|
||||
"""
|
||||
|
||||
# Base indicator classes
|
||||
from .base import (
|
||||
IndicatorState,
|
||||
SimpleIndicatorState,
|
||||
OHLCIndicatorState,
|
||||
)
|
||||
|
||||
# Moving average indicators
|
||||
from .moving_average import (
|
||||
MovingAverageState,
|
||||
ExponentialMovingAverageState,
|
||||
)
|
||||
|
||||
# Volatility indicators
|
||||
from .atr import (
|
||||
ATRState,
|
||||
SimpleATRState,
|
||||
)
|
||||
|
||||
# Trend indicators
|
||||
from .supertrend import (
|
||||
SupertrendState,
|
||||
SupertrendCollection,
|
||||
)
|
||||
|
||||
# Bollinger Bands indicators
|
||||
from .bollinger_bands import (
|
||||
BollingerBandsState,
|
||||
BollingerBandsOHLCState,
|
||||
)
|
||||
|
||||
# RSI indicators
|
||||
from .rsi import (
|
||||
RSIState,
|
||||
SimpleRSIState,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# Base classes
|
||||
"IndicatorState",
|
||||
"SimpleIndicatorState",
|
||||
"OHLCIndicatorState",
|
||||
|
||||
# Moving averages
|
||||
"MovingAverageState",
|
||||
"ExponentialMovingAverageState",
|
||||
|
||||
# Volatility indicators
|
||||
"ATRState",
|
||||
"SimpleATRState",
|
||||
|
||||
# Trend indicators
|
||||
"SupertrendState",
|
||||
"SupertrendCollection",
|
||||
|
||||
# Bollinger Bands
|
||||
"BollingerBandsState",
|
||||
"BollingerBandsOHLCState",
|
||||
|
||||
# RSI indicators
|
||||
"RSIState",
|
||||
"SimpleRSIState",
|
||||
]
|
||||
254
IncrementalTrader/strategies/indicators/atr.py
Normal file
254
IncrementalTrader/strategies/indicators/atr.py
Normal file
@@ -0,0 +1,254 @@
|
||||
"""
|
||||
Average True Range (ATR) Indicator State
|
||||
|
||||
This module implements incremental ATR calculation that maintains constant memory usage
|
||||
and provides identical results to traditional batch calculations. ATR is used by
|
||||
Supertrend and other volatility-based indicators.
|
||||
"""
|
||||
|
||||
from typing import Dict, Union, Optional
|
||||
from .base import OHLCIndicatorState
|
||||
from .moving_average import ExponentialMovingAverageState
|
||||
|
||||
|
||||
class ATRState(OHLCIndicatorState):
|
||||
"""
|
||||
Incremental Average True Range calculation state.
|
||||
|
||||
ATR measures market volatility by calculating the average of true ranges over
|
||||
a specified period. True Range is the maximum of:
|
||||
1. Current High - Current Low
|
||||
2. |Current High - Previous Close|
|
||||
3. |Current Low - Previous Close|
|
||||
|
||||
This implementation uses exponential moving average for smoothing, which is
|
||||
more responsive than simple moving average and requires less memory.
|
||||
|
||||
Attributes:
|
||||
period (int): The ATR period
|
||||
ema_state (ExponentialMovingAverageState): EMA state for smoothing true ranges
|
||||
previous_close (float): Previous period's close price
|
||||
|
||||
Example:
|
||||
atr = ATRState(period=14)
|
||||
|
||||
# Add OHLC data incrementally
|
||||
ohlc = {'open': 100, 'high': 105, 'low': 98, 'close': 103}
|
||||
atr_value = atr.update(ohlc) # Returns current ATR value
|
||||
|
||||
# Check if warmed up
|
||||
if atr.is_warmed_up():
|
||||
current_atr = atr.get_current_value()
|
||||
"""
|
||||
|
||||
def __init__(self, period: int = 14):
|
||||
"""
|
||||
Initialize ATR state.
|
||||
|
||||
Args:
|
||||
period: Number of periods for ATR calculation (default: 14)
|
||||
|
||||
Raises:
|
||||
ValueError: If period is not a positive integer
|
||||
"""
|
||||
super().__init__(period)
|
||||
self.ema_state = ExponentialMovingAverageState(period)
|
||||
self.previous_close = None
|
||||
self.is_initialized = True
|
||||
|
||||
def update(self, ohlc_data: Dict[str, float]) -> float:
|
||||
"""
|
||||
Update ATR with new OHLC data.
|
||||
|
||||
Args:
|
||||
ohlc_data: Dictionary with 'open', 'high', 'low', 'close' keys
|
||||
|
||||
Returns:
|
||||
Current ATR value
|
||||
|
||||
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'])
|
||||
|
||||
# Calculate True Range
|
||||
if self.previous_close is None:
|
||||
# First period - True Range is just High - Low
|
||||
true_range = high - low
|
||||
else:
|
||||
# True Range is the maximum of:
|
||||
# 1. Current High - Current Low
|
||||
# 2. |Current High - Previous Close|
|
||||
# 3. |Current Low - Previous Close|
|
||||
tr1 = high - low
|
||||
tr2 = abs(high - self.previous_close)
|
||||
tr3 = abs(low - self.previous_close)
|
||||
true_range = max(tr1, tr2, tr3)
|
||||
|
||||
# Update EMA with the true range
|
||||
atr_value = self.ema_state.update(true_range)
|
||||
|
||||
# Store current close as previous close for next calculation
|
||||
self.previous_close = close
|
||||
self.values_received += 1
|
||||
|
||||
# Store current ATR value
|
||||
self._current_values = {'atr': atr_value}
|
||||
|
||||
return atr_value
|
||||
|
||||
def is_warmed_up(self) -> bool:
|
||||
"""
|
||||
Check if ATR has enough data for reliable values.
|
||||
|
||||
Returns:
|
||||
True if EMA state is warmed up (has enough true range values)
|
||||
"""
|
||||
return self.ema_state.is_warmed_up()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset ATR state to initial conditions."""
|
||||
self.ema_state.reset()
|
||||
self.previous_close = None
|
||||
self.values_received = 0
|
||||
self._current_values = {}
|
||||
|
||||
def get_current_value(self) -> Optional[float]:
|
||||
"""
|
||||
Get current ATR value without updating.
|
||||
|
||||
Returns:
|
||||
Current ATR value, or None if not warmed up
|
||||
"""
|
||||
if not self.is_warmed_up():
|
||||
return None
|
||||
return self.ema_state.get_current_value()
|
||||
|
||||
def get_state_summary(self) -> dict:
|
||||
"""Get detailed state summary for debugging."""
|
||||
base_summary = super().get_state_summary()
|
||||
base_summary.update({
|
||||
'previous_close': self.previous_close,
|
||||
'ema_state': self.ema_state.get_state_summary(),
|
||||
'current_atr': self.get_current_value()
|
||||
})
|
||||
return base_summary
|
||||
|
||||
|
||||
class SimpleATRState(OHLCIndicatorState):
|
||||
"""
|
||||
Simple ATR implementation using simple moving average instead of EMA.
|
||||
|
||||
This version uses a simple moving average for smoothing true ranges,
|
||||
which matches some traditional ATR implementations but requires more memory.
|
||||
"""
|
||||
|
||||
def __init__(self, period: int = 14):
|
||||
"""
|
||||
Initialize simple ATR state.
|
||||
|
||||
Args:
|
||||
period: Number of periods for ATR calculation (default: 14)
|
||||
"""
|
||||
super().__init__(period)
|
||||
from collections import deque
|
||||
self.true_ranges = deque(maxlen=period)
|
||||
self.tr_sum = 0.0
|
||||
self.previous_close = None
|
||||
self.is_initialized = True
|
||||
|
||||
def update(self, ohlc_data: Dict[str, float]) -> float:
|
||||
"""
|
||||
Update simple ATR with new OHLC data.
|
||||
|
||||
Args:
|
||||
ohlc_data: Dictionary with 'open', 'high', 'low', 'close' keys
|
||||
|
||||
Returns:
|
||||
Current ATR value
|
||||
"""
|
||||
# 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'])
|
||||
|
||||
# Calculate True Range
|
||||
if self.previous_close is None:
|
||||
true_range = high - low
|
||||
else:
|
||||
tr1 = high - low
|
||||
tr2 = abs(high - self.previous_close)
|
||||
tr3 = abs(low - self.previous_close)
|
||||
true_range = max(tr1, tr2, tr3)
|
||||
|
||||
# Update rolling sum
|
||||
if len(self.true_ranges) == self.period:
|
||||
self.tr_sum -= self.true_ranges[0] # Remove oldest value
|
||||
|
||||
self.true_ranges.append(true_range)
|
||||
self.tr_sum += true_range
|
||||
|
||||
# Calculate ATR
|
||||
atr_value = self.tr_sum / len(self.true_ranges)
|
||||
|
||||
# Store current close as previous close for next calculation
|
||||
self.previous_close = close
|
||||
self.values_received += 1
|
||||
|
||||
# Store current ATR value
|
||||
self._current_values = {'atr': atr_value}
|
||||
|
||||
return atr_value
|
||||
|
||||
def is_warmed_up(self) -> bool:
|
||||
"""
|
||||
Check if simple ATR has enough data for reliable values.
|
||||
|
||||
Returns:
|
||||
True if we have at least 'period' number of true range values
|
||||
"""
|
||||
return len(self.true_ranges) >= self.period
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset simple ATR state to initial conditions."""
|
||||
self.true_ranges.clear()
|
||||
self.tr_sum = 0.0
|
||||
self.previous_close = None
|
||||
self.values_received = 0
|
||||
self._current_values = {}
|
||||
|
||||
def get_current_value(self) -> Optional[float]:
|
||||
"""
|
||||
Get current simple ATR value without updating.
|
||||
|
||||
Returns:
|
||||
Current ATR value, or None if not warmed up
|
||||
"""
|
||||
if not self.is_warmed_up():
|
||||
return None
|
||||
return self.tr_sum / len(self.true_ranges)
|
||||
|
||||
def get_state_summary(self) -> dict:
|
||||
"""Get detailed state summary for debugging."""
|
||||
base_summary = super().get_state_summary()
|
||||
base_summary.update({
|
||||
'previous_close': self.previous_close,
|
||||
'tr_sum': self.tr_sum,
|
||||
'true_ranges_count': len(self.true_ranges),
|
||||
'current_atr': self.get_current_value()
|
||||
})
|
||||
return base_summary
|
||||
197
IncrementalTrader/strategies/indicators/base.py
Normal file
197
IncrementalTrader/strategies/indicators/base.py
Normal file
@@ -0,0 +1,197 @@
|
||||
"""
|
||||
Base Indicator State Class
|
||||
|
||||
This module contains the abstract base class for all incremental indicator states.
|
||||
All indicator implementations must inherit from IndicatorState and implement
|
||||
the required methods for incremental calculation.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Any, Dict, Optional, Union
|
||||
import numpy as np
|
||||
|
||||
|
||||
class IndicatorState(ABC):
|
||||
"""
|
||||
Abstract base class for maintaining indicator calculation state.
|
||||
|
||||
This class defines the interface that all incremental indicators must implement.
|
||||
Indicators maintain their internal state and can be updated incrementally with
|
||||
new data points, providing constant memory usage and high performance.
|
||||
|
||||
Attributes:
|
||||
period (int): The period/window size for the indicator
|
||||
values_received (int): Number of values processed so far
|
||||
is_initialized (bool): Whether the indicator has been initialized
|
||||
|
||||
Example:
|
||||
class MyIndicator(IndicatorState):
|
||||
def __init__(self, period: int):
|
||||
super().__init__(period)
|
||||
self._sum = 0.0
|
||||
|
||||
def update(self, new_value: float) -> float:
|
||||
self._sum += new_value
|
||||
self.values_received += 1
|
||||
return self._sum / min(self.values_received, self.period)
|
||||
"""
|
||||
|
||||
def __init__(self, period: int):
|
||||
"""
|
||||
Initialize the indicator state.
|
||||
|
||||
Args:
|
||||
period: The period/window size for the indicator calculation
|
||||
|
||||
Raises:
|
||||
ValueError: If period is not a positive integer
|
||||
"""
|
||||
if not isinstance(period, int) or period <= 0:
|
||||
raise ValueError(f"Period must be a positive integer, got {period}")
|
||||
|
||||
self.period = period
|
||||
self.values_received = 0
|
||||
self.is_initialized = False
|
||||
|
||||
@abstractmethod
|
||||
def update(self, new_value: Union[float, Dict[str, float]]) -> Union[float, Dict[str, float]]:
|
||||
"""
|
||||
Update indicator with new value and return current indicator value.
|
||||
|
||||
This method processes a new data point and updates the internal state
|
||||
of the indicator. It returns the current indicator value after the update.
|
||||
|
||||
Args:
|
||||
new_value: New data point (can be single value or OHLCV dict)
|
||||
|
||||
Returns:
|
||||
Current indicator value after update (single value or dict)
|
||||
|
||||
Raises:
|
||||
ValueError: If new_value is invalid or incompatible
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def is_warmed_up(self) -> bool:
|
||||
"""
|
||||
Check whether indicator has enough data for reliable values.
|
||||
|
||||
Returns:
|
||||
True if indicator has received enough data points for reliable calculation
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def reset(self) -> None:
|
||||
"""
|
||||
Reset indicator state to initial conditions.
|
||||
|
||||
This method clears all internal state and resets the indicator
|
||||
as if it was just initialized.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_current_value(self) -> Union[float, Dict[str, float], None]:
|
||||
"""
|
||||
Get the current indicator value without updating.
|
||||
|
||||
Returns:
|
||||
Current indicator value, or None if not warmed up
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_state_summary(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get summary of current indicator state for debugging.
|
||||
|
||||
Returns:
|
||||
Dictionary containing indicator state information
|
||||
"""
|
||||
return {
|
||||
'indicator_type': self.__class__.__name__,
|
||||
'period': self.period,
|
||||
'values_received': self.values_received,
|
||||
'is_warmed_up': self.is_warmed_up(),
|
||||
'is_initialized': self.is_initialized,
|
||||
'current_value': self.get_current_value()
|
||||
}
|
||||
|
||||
def validate_input(self, value: Union[float, Dict[str, float]]) -> None:
|
||||
"""
|
||||
Validate input value for the indicator.
|
||||
|
||||
Args:
|
||||
value: Input value to validate
|
||||
|
||||
Raises:
|
||||
ValueError: If value is invalid
|
||||
TypeError: If value type is incorrect
|
||||
"""
|
||||
if isinstance(value, (int, float)):
|
||||
if not np.isfinite(value):
|
||||
raise ValueError(f"Input value must be finite, got {value}")
|
||||
elif isinstance(value, dict):
|
||||
required_keys = ['open', 'high', 'low', 'close']
|
||||
for key in required_keys:
|
||||
if key not in value:
|
||||
raise ValueError(f"OHLCV dict missing required key: {key}")
|
||||
if not np.isfinite(value[key]):
|
||||
raise ValueError(f"OHLCV value for {key} must be finite, got {value[key]}")
|
||||
# Validate OHLC relationships
|
||||
if not (value['low'] <= value['open'] <= value['high'] and
|
||||
value['low'] <= value['close'] <= value['high']):
|
||||
raise ValueError(f"Invalid OHLC relationships: {value}")
|
||||
else:
|
||||
raise TypeError(f"Input value must be float or OHLCV dict, got {type(value)}")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of the indicator state."""
|
||||
return (f"{self.__class__.__name__}(period={self.period}, "
|
||||
f"values_received={self.values_received}, "
|
||||
f"warmed_up={self.is_warmed_up()})")
|
||||
|
||||
|
||||
class SimpleIndicatorState(IndicatorState):
|
||||
"""
|
||||
Base class for simple single-value indicators.
|
||||
|
||||
This class provides common functionality for indicators that work with
|
||||
single float values and maintain a simple rolling calculation.
|
||||
"""
|
||||
|
||||
def __init__(self, period: int):
|
||||
"""Initialize simple indicator state."""
|
||||
super().__init__(period)
|
||||
self._current_value = None
|
||||
|
||||
def get_current_value(self) -> Optional[float]:
|
||||
"""Get current indicator value."""
|
||||
return self._current_value if self.is_warmed_up() else None
|
||||
|
||||
def is_warmed_up(self) -> bool:
|
||||
"""Check if indicator is warmed up."""
|
||||
return self.values_received >= self.period
|
||||
|
||||
|
||||
class OHLCIndicatorState(IndicatorState):
|
||||
"""
|
||||
Base class for OHLC-based indicators.
|
||||
|
||||
This class provides common functionality for indicators that work with
|
||||
OHLC data (Open, High, Low, Close) and may return multiple values.
|
||||
"""
|
||||
|
||||
def __init__(self, period: int):
|
||||
"""Initialize OHLC indicator state."""
|
||||
super().__init__(period)
|
||||
self._current_values = {}
|
||||
|
||||
def get_current_value(self) -> Optional[Dict[str, float]]:
|
||||
"""Get current indicator values."""
|
||||
return self._current_values.copy() if self.is_warmed_up() else None
|
||||
|
||||
def is_warmed_up(self) -> bool:
|
||||
"""Check if indicator is warmed up."""
|
||||
return self.values_received >= self.period
|
||||
325
IncrementalTrader/strategies/indicators/bollinger_bands.py
Normal file
325
IncrementalTrader/strategies/indicators/bollinger_bands.py
Normal file
@@ -0,0 +1,325 @@
|
||||
"""
|
||||
Bollinger Bands Indicator State
|
||||
|
||||
This module implements incremental Bollinger Bands calculation that maintains constant memory usage
|
||||
and provides identical results to traditional batch calculations. Used by the BBRSStrategy.
|
||||
"""
|
||||
|
||||
from typing import Dict, Union, Optional
|
||||
from collections import deque
|
||||
import math
|
||||
from .base import OHLCIndicatorState
|
||||
from .moving_average import MovingAverageState
|
||||
|
||||
|
||||
class BollingerBandsState(OHLCIndicatorState):
|
||||
"""
|
||||
Incremental Bollinger Bands calculation state.
|
||||
|
||||
Bollinger Bands consist of:
|
||||
- Middle Band: Simple Moving Average of close prices
|
||||
- Upper Band: Middle Band + (Standard Deviation * multiplier)
|
||||
- Lower Band: Middle Band - (Standard Deviation * multiplier)
|
||||
|
||||
This implementation maintains a rolling window for standard deviation calculation
|
||||
while using the MovingAverageState for the middle band.
|
||||
|
||||
Attributes:
|
||||
period (int): Period for moving average and standard deviation
|
||||
std_dev_multiplier (float): Multiplier for standard deviation
|
||||
ma_state (MovingAverageState): Moving average state for middle band
|
||||
close_values (deque): Rolling window of close prices for std dev calculation
|
||||
close_sum_sq (float): Sum of squared close values for variance calculation
|
||||
|
||||
Example:
|
||||
bb = BollingerBandsState(period=20, std_dev_multiplier=2.0)
|
||||
|
||||
# Add price data incrementally
|
||||
result = bb.update(103.5) # Close price
|
||||
upper_band = result['upper_band']
|
||||
middle_band = result['middle_band']
|
||||
lower_band = result['lower_band']
|
||||
bandwidth = result['bandwidth']
|
||||
"""
|
||||
|
||||
def __init__(self, period: int = 20, std_dev_multiplier: float = 2.0):
|
||||
"""
|
||||
Initialize Bollinger Bands state.
|
||||
|
||||
Args:
|
||||
period: Period for moving average and standard deviation (default: 20)
|
||||
std_dev_multiplier: Multiplier for standard deviation (default: 2.0)
|
||||
|
||||
Raises:
|
||||
ValueError: If period is not positive or multiplier is not positive
|
||||
"""
|
||||
super().__init__(period)
|
||||
|
||||
if std_dev_multiplier <= 0:
|
||||
raise ValueError(f"Standard deviation multiplier must be positive, got {std_dev_multiplier}")
|
||||
|
||||
self.std_dev_multiplier = std_dev_multiplier
|
||||
self.ma_state = MovingAverageState(period)
|
||||
|
||||
# For incremental standard deviation calculation
|
||||
self.close_values = deque(maxlen=period)
|
||||
self.close_sum_sq = 0.0 # Sum of squared values
|
||||
|
||||
self.is_initialized = True
|
||||
|
||||
def update(self, close_price: Union[float, int]) -> Dict[str, float]:
|
||||
"""
|
||||
Update Bollinger Bands with new close price.
|
||||
|
||||
Args:
|
||||
close_price: New closing price
|
||||
|
||||
Returns:
|
||||
Dictionary with 'upper_band', 'middle_band', 'lower_band', 'bandwidth', 'std_dev'
|
||||
|
||||
Raises:
|
||||
ValueError: If close_price is not finite
|
||||
TypeError: If close_price is not numeric
|
||||
"""
|
||||
# Validate input
|
||||
if not isinstance(close_price, (int, float)):
|
||||
raise TypeError(f"close_price must be numeric, got {type(close_price)}")
|
||||
|
||||
self.validate_input(close_price)
|
||||
|
||||
close_price = float(close_price)
|
||||
|
||||
# Update moving average (middle band)
|
||||
middle_band = self.ma_state.update(close_price)
|
||||
|
||||
# Update rolling window for standard deviation
|
||||
if len(self.close_values) == self.period:
|
||||
# Remove oldest value from sum of squares
|
||||
old_value = self.close_values[0]
|
||||
self.close_sum_sq -= old_value * old_value
|
||||
|
||||
# Add new value
|
||||
self.close_values.append(close_price)
|
||||
self.close_sum_sq += close_price * close_price
|
||||
|
||||
# Calculate standard deviation
|
||||
n = len(self.close_values)
|
||||
if n < 2:
|
||||
# Not enough data for standard deviation
|
||||
std_dev = 0.0
|
||||
else:
|
||||
# Incremental variance calculation: Var = (sum_sq - n*mean^2) / (n-1)
|
||||
mean = middle_band
|
||||
variance = (self.close_sum_sq - n * mean * mean) / (n - 1)
|
||||
std_dev = math.sqrt(max(variance, 0.0)) # Ensure non-negative
|
||||
|
||||
# Calculate bands
|
||||
upper_band = middle_band + (self.std_dev_multiplier * std_dev)
|
||||
lower_band = middle_band - (self.std_dev_multiplier * std_dev)
|
||||
|
||||
# Calculate bandwidth (normalized band width)
|
||||
if middle_band != 0:
|
||||
bandwidth = (upper_band - lower_band) / middle_band
|
||||
else:
|
||||
bandwidth = 0.0
|
||||
|
||||
self.values_received += 1
|
||||
|
||||
# Store current values
|
||||
result = {
|
||||
'upper_band': upper_band,
|
||||
'middle_band': middle_band,
|
||||
'lower_band': lower_band,
|
||||
'bandwidth': bandwidth,
|
||||
'std_dev': std_dev
|
||||
}
|
||||
|
||||
self._current_values = result
|
||||
return result
|
||||
|
||||
def is_warmed_up(self) -> bool:
|
||||
"""
|
||||
Check if Bollinger Bands has enough data for reliable values.
|
||||
|
||||
Returns:
|
||||
True if we have at least 'period' number of values
|
||||
"""
|
||||
return self.ma_state.is_warmed_up()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset Bollinger Bands state to initial conditions."""
|
||||
self.ma_state.reset()
|
||||
self.close_values.clear()
|
||||
self.close_sum_sq = 0.0
|
||||
self.values_received = 0
|
||||
self._current_values = {}
|
||||
|
||||
def get_current_value(self) -> Optional[Dict[str, float]]:
|
||||
"""
|
||||
Get current Bollinger Bands values without updating.
|
||||
|
||||
Returns:
|
||||
Dictionary with current BB 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_squeeze_status(self, squeeze_threshold: float = 0.05) -> bool:
|
||||
"""
|
||||
Check if Bollinger Bands are in a squeeze condition.
|
||||
|
||||
Args:
|
||||
squeeze_threshold: Bandwidth threshold for squeeze detection
|
||||
|
||||
Returns:
|
||||
True if bandwidth is below threshold (squeeze condition)
|
||||
"""
|
||||
if not self.is_warmed_up() or not self._current_values:
|
||||
return False
|
||||
|
||||
bandwidth = self._current_values.get('bandwidth', float('inf'))
|
||||
return bandwidth < squeeze_threshold
|
||||
|
||||
def get_position_relative_to_bands(self, current_price: float) -> str:
|
||||
"""
|
||||
Get current price position relative to Bollinger Bands.
|
||||
|
||||
Args:
|
||||
current_price: Current price to evaluate
|
||||
|
||||
Returns:
|
||||
'above_upper', 'between_bands', 'below_lower', or 'unknown'
|
||||
"""
|
||||
if not self.is_warmed_up() or not self._current_values:
|
||||
return 'unknown'
|
||||
|
||||
upper_band = self._current_values['upper_band']
|
||||
lower_band = self._current_values['lower_band']
|
||||
|
||||
if current_price > upper_band:
|
||||
return 'above_upper'
|
||||
elif current_price < lower_band:
|
||||
return 'below_lower'
|
||||
else:
|
||||
return 'between_bands'
|
||||
|
||||
def get_state_summary(self) -> dict:
|
||||
"""Get detailed state summary for debugging."""
|
||||
base_summary = super().get_state_summary()
|
||||
base_summary.update({
|
||||
'std_dev_multiplier': self.std_dev_multiplier,
|
||||
'close_values_count': len(self.close_values),
|
||||
'close_sum_sq': self.close_sum_sq,
|
||||
'ma_state': self.ma_state.get_state_summary(),
|
||||
'current_squeeze': self.get_squeeze_status() if self.is_warmed_up() else None
|
||||
})
|
||||
return base_summary
|
||||
|
||||
|
||||
class BollingerBandsOHLCState(OHLCIndicatorState):
|
||||
"""
|
||||
Bollinger Bands implementation that works with OHLC data.
|
||||
|
||||
This version can calculate Bollinger Bands based on different price types
|
||||
(close, typical price, etc.) and provides additional OHLC-based analysis.
|
||||
"""
|
||||
|
||||
def __init__(self, period: int = 20, std_dev_multiplier: float = 2.0, price_type: str = 'close'):
|
||||
"""
|
||||
Initialize OHLC Bollinger Bands state.
|
||||
|
||||
Args:
|
||||
period: Period for calculation
|
||||
std_dev_multiplier: Standard deviation multiplier
|
||||
price_type: Price type to use ('close', 'typical', 'median', 'weighted')
|
||||
"""
|
||||
super().__init__(period)
|
||||
|
||||
if price_type not in ['close', 'typical', 'median', 'weighted']:
|
||||
raise ValueError(f"Invalid price_type: {price_type}")
|
||||
|
||||
self.std_dev_multiplier = std_dev_multiplier
|
||||
self.price_type = price_type
|
||||
self.bb_state = BollingerBandsState(period, std_dev_multiplier)
|
||||
self.is_initialized = True
|
||||
|
||||
def _extract_price(self, ohlc_data: Dict[str, float]) -> float:
|
||||
"""Extract price based on price_type setting."""
|
||||
if self.price_type == 'close':
|
||||
return ohlc_data['close']
|
||||
elif self.price_type == 'typical':
|
||||
return (ohlc_data['high'] + ohlc_data['low'] + ohlc_data['close']) / 3.0
|
||||
elif self.price_type == 'median':
|
||||
return (ohlc_data['high'] + ohlc_data['low']) / 2.0
|
||||
elif self.price_type == 'weighted':
|
||||
return (ohlc_data['high'] + ohlc_data['low'] + 2 * ohlc_data['close']) / 4.0
|
||||
else:
|
||||
return ohlc_data['close']
|
||||
|
||||
def update(self, ohlc_data: Dict[str, float]) -> Dict[str, float]:
|
||||
"""
|
||||
Update Bollinger Bands with OHLC data.
|
||||
|
||||
Args:
|
||||
ohlc_data: Dictionary with OHLC data
|
||||
|
||||
Returns:
|
||||
Dictionary with Bollinger Bands values plus OHLC analysis
|
||||
"""
|
||||
# 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)
|
||||
|
||||
# Extract price based on type
|
||||
price = self._extract_price(ohlc_data)
|
||||
|
||||
# Update underlying BB state
|
||||
bb_result = self.bb_state.update(price)
|
||||
|
||||
# Add OHLC-specific analysis
|
||||
high = ohlc_data['high']
|
||||
low = ohlc_data['low']
|
||||
close = ohlc_data['close']
|
||||
|
||||
# Check if high/low touched bands
|
||||
upper_band = bb_result['upper_band']
|
||||
lower_band = bb_result['lower_band']
|
||||
|
||||
bb_result.update({
|
||||
'high_above_upper': high > upper_band,
|
||||
'low_below_lower': low < lower_band,
|
||||
'close_position': self.bb_state.get_position_relative_to_bands(close),
|
||||
'price_type': self.price_type,
|
||||
'extracted_price': price
|
||||
})
|
||||
|
||||
self.values_received += 1
|
||||
self._current_values = bb_result
|
||||
|
||||
return bb_result
|
||||
|
||||
def is_warmed_up(self) -> bool:
|
||||
"""Check if OHLC Bollinger Bands is warmed up."""
|
||||
return self.bb_state.is_warmed_up()
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset OHLC Bollinger Bands state."""
|
||||
self.bb_state.reset()
|
||||
self.values_received = 0
|
||||
self._current_values = {}
|
||||
|
||||
def get_current_value(self) -> Optional[Dict[str, float]]:
|
||||
"""Get current OHLC Bollinger Bands values."""
|
||||
return self.bb_state.get_current_value()
|
||||
|
||||
def get_state_summary(self) -> dict:
|
||||
"""Get detailed state summary."""
|
||||
base_summary = super().get_state_summary()
|
||||
base_summary.update({
|
||||
'price_type': self.price_type,
|
||||
'bb_state': self.bb_state.get_state_summary()
|
||||
})
|
||||
return base_summary
|
||||
228
IncrementalTrader/strategies/indicators/moving_average.py
Normal file
228
IncrementalTrader/strategies/indicators/moving_average.py
Normal file
@@ -0,0 +1,228 @@
|
||||
"""
|
||||
Moving Average Indicator State
|
||||
|
||||
This module implements incremental moving average calculation that maintains
|
||||
constant memory usage and provides identical results to traditional batch calculations.
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
from typing import Union
|
||||
from .base import SimpleIndicatorState
|
||||
|
||||
|
||||
class MovingAverageState(SimpleIndicatorState):
|
||||
"""
|
||||
Incremental moving average calculation state.
|
||||
|
||||
This class maintains the state for calculating a simple moving average
|
||||
incrementally. It uses a rolling window approach with constant memory usage.
|
||||
|
||||
Attributes:
|
||||
period (int): The moving average period
|
||||
values (deque): Rolling window of values (max length = period)
|
||||
sum (float): Current sum of values in the window
|
||||
|
||||
Example:
|
||||
ma = MovingAverageState(period=20)
|
||||
|
||||
# Add values incrementally
|
||||
ma_value = ma.update(100.0) # Returns current MA value
|
||||
ma_value = ma.update(105.0) # Updates and returns new MA value
|
||||
|
||||
# Check if warmed up (has enough values)
|
||||
if ma.is_warmed_up():
|
||||
current_ma = ma.get_current_value()
|
||||
"""
|
||||
|
||||
def __init__(self, period: int):
|
||||
"""
|
||||
Initialize moving average state.
|
||||
|
||||
Args:
|
||||
period: Number of periods for the moving average
|
||||
|
||||
Raises:
|
||||
ValueError: If period is not a positive integer
|
||||
"""
|
||||
super().__init__(period)
|
||||
self.values = deque(maxlen=period)
|
||||
self.sum = 0.0
|
||||
self.is_initialized = True
|
||||
|
||||
def update(self, new_value: Union[float, int]) -> float:
|
||||
"""
|
||||
Update moving average with new value.
|
||||
|
||||
Args:
|
||||
new_value: New price/value to add to the moving average
|
||||
|
||||
Returns:
|
||||
Current moving average value
|
||||
|
||||
Raises:
|
||||
ValueError: If new_value is not finite
|
||||
TypeError: If new_value is not numeric
|
||||
"""
|
||||
# Validate input
|
||||
if not isinstance(new_value, (int, float)):
|
||||
raise TypeError(f"new_value must be numeric, got {type(new_value)}")
|
||||
|
||||
self.validate_input(new_value)
|
||||
|
||||
# If deque is at max capacity, subtract the value being removed
|
||||
if len(self.values) == self.period:
|
||||
self.sum -= self.values[0] # Will be automatically removed by deque
|
||||
|
||||
# Add new value
|
||||
self.values.append(float(new_value))
|
||||
self.sum += float(new_value)
|
||||
self.values_received += 1
|
||||
|
||||
# Calculate current moving average
|
||||
current_count = len(self.values)
|
||||
self._current_value = self.sum / current_count
|
||||
|
||||
return self._current_value
|
||||
|
||||
def is_warmed_up(self) -> bool:
|
||||
"""
|
||||
Check if moving average has enough data for reliable values.
|
||||
|
||||
Returns:
|
||||
True if we have at least 'period' number of values
|
||||
"""
|
||||
return len(self.values) >= self.period
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset moving average state to initial conditions."""
|
||||
self.values.clear()
|
||||
self.sum = 0.0
|
||||
self.values_received = 0
|
||||
self._current_value = None
|
||||
|
||||
def get_current_value(self) -> Union[float, None]:
|
||||
"""
|
||||
Get current moving average value without updating.
|
||||
|
||||
Returns:
|
||||
Current moving average value, or None if not enough data
|
||||
"""
|
||||
if len(self.values) == 0:
|
||||
return None
|
||||
return self.sum / len(self.values)
|
||||
|
||||
def get_state_summary(self) -> dict:
|
||||
"""Get detailed state summary for debugging."""
|
||||
base_summary = super().get_state_summary()
|
||||
base_summary.update({
|
||||
'window_size': len(self.values),
|
||||
'sum': self.sum,
|
||||
'values_in_window': list(self.values) if len(self.values) <= 10 else f"[{len(self.values)} values]"
|
||||
})
|
||||
return base_summary
|
||||
|
||||
|
||||
class ExponentialMovingAverageState(SimpleIndicatorState):
|
||||
"""
|
||||
Incremental exponential moving average calculation state.
|
||||
|
||||
This class maintains the state for calculating an exponential moving average (EMA)
|
||||
incrementally. EMA gives more weight to recent values and requires minimal memory.
|
||||
|
||||
Attributes:
|
||||
period (int): The EMA period (used to calculate smoothing factor)
|
||||
alpha (float): Smoothing factor (2 / (period + 1))
|
||||
ema_value (float): Current EMA value
|
||||
|
||||
Example:
|
||||
ema = ExponentialMovingAverageState(period=20)
|
||||
|
||||
# Add values incrementally
|
||||
ema_value = ema.update(100.0) # Returns current EMA value
|
||||
ema_value = ema.update(105.0) # Updates and returns new EMA value
|
||||
"""
|
||||
|
||||
def __init__(self, period: int):
|
||||
"""
|
||||
Initialize exponential moving average state.
|
||||
|
||||
Args:
|
||||
period: Number of periods for the EMA (used to calculate alpha)
|
||||
|
||||
Raises:
|
||||
ValueError: If period is not a positive integer
|
||||
"""
|
||||
super().__init__(period)
|
||||
self.alpha = 2.0 / (period + 1) # Smoothing factor
|
||||
self.ema_value = None
|
||||
self.is_initialized = True
|
||||
|
||||
def update(self, new_value: Union[float, int]) -> float:
|
||||
"""
|
||||
Update exponential moving average with new value.
|
||||
|
||||
Args:
|
||||
new_value: New price/value to add to the EMA
|
||||
|
||||
Returns:
|
||||
Current EMA value
|
||||
|
||||
Raises:
|
||||
ValueError: If new_value is not finite
|
||||
TypeError: If new_value is not numeric
|
||||
"""
|
||||
# Validate input
|
||||
if not isinstance(new_value, (int, float)):
|
||||
raise TypeError(f"new_value must be numeric, got {type(new_value)}")
|
||||
|
||||
self.validate_input(new_value)
|
||||
|
||||
new_value = float(new_value)
|
||||
|
||||
if self.ema_value is None:
|
||||
# First value - initialize EMA
|
||||
self.ema_value = new_value
|
||||
else:
|
||||
# EMA formula: EMA = alpha * new_value + (1 - alpha) * previous_EMA
|
||||
self.ema_value = self.alpha * new_value + (1 - self.alpha) * self.ema_value
|
||||
|
||||
self.values_received += 1
|
||||
self._current_value = self.ema_value
|
||||
|
||||
return self.ema_value
|
||||
|
||||
def is_warmed_up(self) -> bool:
|
||||
"""
|
||||
Check if EMA has enough data for reliable values.
|
||||
|
||||
For EMA, we consider it warmed up after receiving 'period' number of values,
|
||||
though it starts producing values immediately.
|
||||
|
||||
Returns:
|
||||
True if we have received at least 'period' number of values
|
||||
"""
|
||||
return self.values_received >= self.period
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset EMA state to initial conditions."""
|
||||
self.ema_value = None
|
||||
self.values_received = 0
|
||||
self._current_value = None
|
||||
|
||||
def get_current_value(self) -> Union[float, None]:
|
||||
"""
|
||||
Get current EMA value without updating.
|
||||
|
||||
Returns:
|
||||
Current EMA value, or None if no values received yet
|
||||
"""
|
||||
return self.ema_value
|
||||
|
||||
def get_state_summary(self) -> dict:
|
||||
"""Get detailed state summary for debugging."""
|
||||
base_summary = super().get_state_summary()
|
||||
base_summary.update({
|
||||
'alpha': self.alpha,
|
||||
'ema_value': self.ema_value
|
||||
})
|
||||
return base_summary
|
||||
289
IncrementalTrader/strategies/indicators/rsi.py
Normal file
289
IncrementalTrader/strategies/indicators/rsi.py
Normal file
@@ -0,0 +1,289 @@
|
||||
"""
|
||||
RSI (Relative Strength Index) Indicator State
|
||||
|
||||
This module implements incremental RSI calculation that maintains constant memory usage
|
||||
and provides identical results to traditional batch calculations.
|
||||
"""
|
||||
|
||||
from typing import Union, Optional
|
||||
from .base import SimpleIndicatorState
|
||||
from .moving_average import ExponentialMovingAverageState
|
||||
|
||||
|
||||
class RSIState(SimpleIndicatorState):
|
||||
"""
|
||||
Incremental RSI calculation state using Wilder's smoothing.
|
||||
|
||||
RSI measures the speed and magnitude of price changes to evaluate overbought
|
||||
or oversold conditions. It oscillates between 0 and 100.
|
||||
|
||||
RSI = 100 - (100 / (1 + RS))
|
||||
where RS = Average Gain / Average Loss over the specified period
|
||||
|
||||
This implementation uses Wilder's smoothing (alpha = 1/period) to match
|
||||
the original pandas implementation exactly.
|
||||
|
||||
Attributes:
|
||||
period (int): The RSI period (typically 14)
|
||||
alpha (float): Wilder's smoothing factor (1/period)
|
||||
avg_gain (float): Current average gain
|
||||
avg_loss (float): Current average loss
|
||||
previous_close (float): Previous period's close price
|
||||
|
||||
Example:
|
||||
rsi = RSIState(period=14)
|
||||
|
||||
# Add price data incrementally
|
||||
rsi_value = rsi.update(100.0) # Returns current RSI value
|
||||
rsi_value = rsi.update(105.0) # Updates and returns new RSI value
|
||||
|
||||
# Check if warmed up
|
||||
if rsi.is_warmed_up():
|
||||
current_rsi = rsi.get_current_value()
|
||||
"""
|
||||
|
||||
def __init__(self, period: int = 14):
|
||||
"""
|
||||
Initialize RSI state.
|
||||
|
||||
Args:
|
||||
period: Number of periods for RSI calculation (default: 14)
|
||||
|
||||
Raises:
|
||||
ValueError: If period is not a positive integer
|
||||
"""
|
||||
super().__init__(period)
|
||||
self.alpha = 1.0 / period # Wilder's smoothing factor
|
||||
self.avg_gain = None
|
||||
self.avg_loss = None
|
||||
self.previous_close = None
|
||||
self.is_initialized = True
|
||||
|
||||
def update(self, new_close: Union[float, int]) -> float:
|
||||
"""
|
||||
Update RSI with new close price using Wilder's smoothing.
|
||||
|
||||
Args:
|
||||
new_close: New closing price
|
||||
|
||||
Returns:
|
||||
Current RSI value (0-100), or NaN if not warmed up
|
||||
|
||||
Raises:
|
||||
ValueError: If new_close is not finite
|
||||
TypeError: If new_close is not numeric
|
||||
"""
|
||||
# Validate input - accept numpy types as well
|
||||
import numpy as np
|
||||
if not isinstance(new_close, (int, float, np.integer, np.floating)):
|
||||
raise TypeError(f"new_close must be numeric, got {type(new_close)}")
|
||||
|
||||
self.validate_input(float(new_close))
|
||||
|
||||
new_close = float(new_close)
|
||||
|
||||
if self.previous_close is None:
|
||||
# First value - no gain/loss to calculate
|
||||
self.previous_close = new_close
|
||||
self.values_received += 1
|
||||
# Return NaN until warmed up (matches original behavior)
|
||||
self._current_value = float('nan')
|
||||
return self._current_value
|
||||
|
||||
# Calculate price change
|
||||
price_change = new_close - self.previous_close
|
||||
|
||||
# Separate gains and losses
|
||||
gain = max(price_change, 0.0)
|
||||
loss = max(-price_change, 0.0)
|
||||
|
||||
if self.avg_gain is None:
|
||||
# Initialize with first gain/loss
|
||||
self.avg_gain = gain
|
||||
self.avg_loss = loss
|
||||
else:
|
||||
# Wilder's smoothing: avg = alpha * new_value + (1 - alpha) * previous_avg
|
||||
self.avg_gain = self.alpha * gain + (1 - self.alpha) * self.avg_gain
|
||||
self.avg_loss = self.alpha * loss + (1 - self.alpha) * self.avg_loss
|
||||
|
||||
# Calculate RSI only if warmed up
|
||||
# RSI should start when we have 'period' price changes (not including the first value)
|
||||
if self.values_received > self.period:
|
||||
if self.avg_loss == 0.0:
|
||||
# Avoid division by zero - all gains, no losses
|
||||
if self.avg_gain > 0:
|
||||
rsi_value = 100.0
|
||||
else:
|
||||
rsi_value = 50.0 # Neutral when both are zero
|
||||
else:
|
||||
rs = self.avg_gain / self.avg_loss
|
||||
rsi_value = 100.0 - (100.0 / (1.0 + rs))
|
||||
else:
|
||||
# Not warmed up yet - return NaN
|
||||
rsi_value = float('nan')
|
||||
|
||||
# Store state
|
||||
self.previous_close = new_close
|
||||
self.values_received += 1
|
||||
self._current_value = rsi_value
|
||||
|
||||
return rsi_value
|
||||
|
||||
def is_warmed_up(self) -> bool:
|
||||
"""
|
||||
Check if RSI has enough data for reliable values.
|
||||
|
||||
Returns:
|
||||
True if we have enough price changes for RSI calculation
|
||||
"""
|
||||
return self.values_received > self.period
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset RSI state to initial conditions."""
|
||||
self.alpha = 1.0 / self.period
|
||||
self.avg_gain = None
|
||||
self.avg_loss = None
|
||||
self.previous_close = None
|
||||
self.values_received = 0
|
||||
self._current_value = None
|
||||
|
||||
def get_current_value(self) -> Optional[float]:
|
||||
"""
|
||||
Get current RSI value without updating.
|
||||
|
||||
Returns:
|
||||
Current RSI value (0-100), or None if not enough data
|
||||
"""
|
||||
if not self.is_warmed_up():
|
||||
return None
|
||||
return self._current_value
|
||||
|
||||
def get_state_summary(self) -> dict:
|
||||
"""Get detailed state summary for debugging."""
|
||||
base_summary = super().get_state_summary()
|
||||
base_summary.update({
|
||||
'alpha': self.alpha,
|
||||
'previous_close': self.previous_close,
|
||||
'avg_gain': self.avg_gain,
|
||||
'avg_loss': self.avg_loss,
|
||||
'current_rsi': self.get_current_value()
|
||||
})
|
||||
return base_summary
|
||||
|
||||
|
||||
class SimpleRSIState(SimpleIndicatorState):
|
||||
"""
|
||||
Simple RSI implementation using simple moving averages instead of EMAs.
|
||||
|
||||
This version uses simple moving averages for gain and loss smoothing,
|
||||
which matches traditional RSI implementations but requires more memory.
|
||||
"""
|
||||
|
||||
def __init__(self, period: int = 14):
|
||||
"""
|
||||
Initialize simple RSI state.
|
||||
|
||||
Args:
|
||||
period: Number of periods for RSI calculation (default: 14)
|
||||
"""
|
||||
super().__init__(period)
|
||||
from collections import deque
|
||||
self.gains = deque(maxlen=period)
|
||||
self.losses = deque(maxlen=period)
|
||||
self.gain_sum = 0.0
|
||||
self.loss_sum = 0.0
|
||||
self.previous_close = None
|
||||
self.is_initialized = True
|
||||
|
||||
def update(self, new_close: Union[float, int]) -> float:
|
||||
"""
|
||||
Update simple RSI with new close price.
|
||||
|
||||
Args:
|
||||
new_close: New closing price
|
||||
|
||||
Returns:
|
||||
Current RSI value (0-100)
|
||||
"""
|
||||
# Validate input
|
||||
if not isinstance(new_close, (int, float)):
|
||||
raise TypeError(f"new_close must be numeric, got {type(new_close)}")
|
||||
|
||||
self.validate_input(new_close)
|
||||
|
||||
new_close = float(new_close)
|
||||
|
||||
if self.previous_close is None:
|
||||
# First value
|
||||
self.previous_close = new_close
|
||||
self.values_received += 1
|
||||
self._current_value = 50.0
|
||||
return self._current_value
|
||||
|
||||
# Calculate price change
|
||||
price_change = new_close - self.previous_close
|
||||
gain = max(price_change, 0.0)
|
||||
loss = max(-price_change, 0.0)
|
||||
|
||||
# Update rolling sums
|
||||
if len(self.gains) == self.period:
|
||||
self.gain_sum -= self.gains[0]
|
||||
self.loss_sum -= self.losses[0]
|
||||
|
||||
self.gains.append(gain)
|
||||
self.losses.append(loss)
|
||||
self.gain_sum += gain
|
||||
self.loss_sum += loss
|
||||
|
||||
# Calculate RSI
|
||||
if len(self.gains) == 0:
|
||||
rsi_value = 50.0
|
||||
else:
|
||||
avg_gain = self.gain_sum / len(self.gains)
|
||||
avg_loss = self.loss_sum / len(self.losses)
|
||||
|
||||
if avg_loss == 0.0:
|
||||
rsi_value = 100.0
|
||||
else:
|
||||
rs = avg_gain / avg_loss
|
||||
rsi_value = 100.0 - (100.0 / (1.0 + rs))
|
||||
|
||||
# Store state
|
||||
self.previous_close = new_close
|
||||
self.values_received += 1
|
||||
self._current_value = rsi_value
|
||||
|
||||
return rsi_value
|
||||
|
||||
def is_warmed_up(self) -> bool:
|
||||
"""Check if simple RSI is warmed up."""
|
||||
return len(self.gains) >= self.period
|
||||
|
||||
def reset(self) -> None:
|
||||
"""Reset simple RSI state."""
|
||||
self.gains.clear()
|
||||
self.losses.clear()
|
||||
self.gain_sum = 0.0
|
||||
self.loss_sum = 0.0
|
||||
self.previous_close = None
|
||||
self.values_received = 0
|
||||
self._current_value = None
|
||||
|
||||
def get_current_value(self) -> Optional[float]:
|
||||
"""Get current simple RSI value."""
|
||||
if self.values_received == 0:
|
||||
return None
|
||||
return self._current_value
|
||||
|
||||
def get_state_summary(self) -> dict:
|
||||
"""Get detailed state summary for debugging."""
|
||||
base_summary = super().get_state_summary()
|
||||
base_summary.update({
|
||||
'previous_close': self.previous_close,
|
||||
'gains_window_size': len(self.gains),
|
||||
'losses_window_size': len(self.losses),
|
||||
'gain_sum': self.gain_sum,
|
||||
'loss_sum': self.loss_sum,
|
||||
'current_rsi': self.get_current_value()
|
||||
})
|
||||
return base_summary
|
||||
316
IncrementalTrader/strategies/indicators/supertrend.py
Normal file
316
IncrementalTrader/strategies/indicators/supertrend.py
Normal file
@@ -0,0 +1,316 @@
|
||||
"""
|
||||
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]
|
||||
}
|
||||
423
IncrementalTrader/strategies/metatrend.py
Normal file
423
IncrementalTrader/strategies/metatrend.py
Normal file
@@ -0,0 +1,423 @@
|
||||
"""
|
||||
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
|
||||
"""
|
||||
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)
|
||||
|
||||
# Initialize Supertrend collection with exact parameters from original strategy
|
||||
self.supertrend_configs = [
|
||||
(12, 3.0), # period=12, multiplier=3.0
|
||||
(10, 1.0), # period=10, multiplier=1.0
|
||||
(11, 2.0) # period=11, multiplier=2.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}")
|
||||
|
||||
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 (matching original DefaultStrategy):
|
||||
- All 3 Supertrends must agree for directional signal
|
||||
- If all trends are the same, meta-trend = that trend
|
||||
- If trends disagree, meta-trend = 0 (neutral)
|
||||
|
||||
Args:
|
||||
supertrend_results: Results from SupertrendCollection.update()
|
||||
|
||||
Returns:
|
||||
int: Meta-trend value (1, -1, or 0)
|
||||
"""
|
||||
trends = supertrend_results['trends']
|
||||
|
||||
# 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 _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
|
||||
332
IncrementalTrader/strategies/random.py
Normal file
332
IncrementalTrader/strategies/random.py
Normal file
@@ -0,0 +1,332 @@
|
||||
"""
|
||||
Incremental Random Strategy for Testing
|
||||
|
||||
This strategy generates random entry and exit signals for testing the incremental strategy system.
|
||||
It's useful for verifying that the incremental strategy framework is working correctly.
|
||||
"""
|
||||
|
||||
import random
|
||||
import logging
|
||||
import time
|
||||
from typing import Dict, Optional, Any
|
||||
import pandas as pd
|
||||
|
||||
from .base import IncStrategyBase, IncStrategySignal
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RandomStrategy(IncStrategyBase):
|
||||
"""
|
||||
Incremental random signal generator strategy for testing.
|
||||
|
||||
This strategy generates random entry and exit signals with configurable
|
||||
probability and confidence levels. It's designed to test the incremental
|
||||
strategy framework and signal processing system.
|
||||
|
||||
The incremental version maintains minimal state and processes each new
|
||||
data point independently, making it ideal for testing real-time performance.
|
||||
|
||||
Parameters:
|
||||
entry_probability: Probability of generating an entry signal (0.0-1.0)
|
||||
exit_probability: Probability of generating an exit signal (0.0-1.0)
|
||||
min_confidence: Minimum confidence level for signals
|
||||
max_confidence: Maximum confidence level for signals
|
||||
timeframe: Timeframe to operate on (default: "1min")
|
||||
signal_frequency: How often to generate signals (every N bars)
|
||||
random_seed: Optional seed for reproducible random signals
|
||||
|
||||
Example:
|
||||
strategy = RandomStrategy(
|
||||
name="random_test",
|
||||
weight=1.0,
|
||||
params={
|
||||
"entry_probability": 0.1,
|
||||
"exit_probability": 0.15,
|
||||
"min_confidence": 0.7,
|
||||
"max_confidence": 0.9,
|
||||
"signal_frequency": 5,
|
||||
"random_seed": 42 # For reproducible testing
|
||||
}
|
||||
)
|
||||
"""
|
||||
|
||||
def __init__(self, name: str = "random", weight: float = 1.0, params: Optional[Dict] = None):
|
||||
"""Initialize the incremental random strategy."""
|
||||
super().__init__(name, weight, params)
|
||||
|
||||
# Strategy parameters with defaults
|
||||
self.entry_probability = self.params.get("entry_probability", 0.05) # 5% chance per bar
|
||||
self.exit_probability = self.params.get("exit_probability", 0.1) # 10% chance per bar
|
||||
self.min_confidence = self.params.get("min_confidence", 0.6)
|
||||
self.max_confidence = self.params.get("max_confidence", 0.9)
|
||||
self.timeframe = self.params.get("timeframe", "1min")
|
||||
self.signal_frequency = self.params.get("signal_frequency", 1) # Every bar
|
||||
|
||||
# Create separate random instance for this strategy
|
||||
self._random = random.Random()
|
||||
random_seed = self.params.get("random_seed")
|
||||
if random_seed is not None:
|
||||
self._random.seed(random_seed)
|
||||
logger.info(f"RandomStrategy: Set random seed to {random_seed}")
|
||||
|
||||
# Internal state (minimal for random strategy)
|
||||
self._bar_count = 0
|
||||
self._last_signal_bar = -1
|
||||
self._current_price = None
|
||||
self._last_timestamp = None
|
||||
|
||||
logger.info(f"RandomStrategy initialized with entry_prob={self.entry_probability}, "
|
||||
f"exit_prob={self.exit_probability}, timeframe={self.timeframe}, "
|
||||
f"aggregation_enabled={self._timeframe_aggregator is not None}")
|
||||
|
||||
def get_minimum_buffer_size(self) -> Dict[str, int]:
|
||||
"""
|
||||
Return minimum data points needed for each timeframe.
|
||||
|
||||
Random strategy doesn't need any historical data for calculations,
|
||||
so we only need 1 data point to start generating signals.
|
||||
With the new base class timeframe aggregation, we only specify
|
||||
our primary timeframe.
|
||||
|
||||
Returns:
|
||||
Dict[str, int]: Minimal buffer requirements
|
||||
"""
|
||||
return {self.timeframe: 1} # Only need current data point
|
||||
|
||||
def supports_incremental_calculation(self) -> bool:
|
||||
"""
|
||||
Whether strategy supports incremental calculation.
|
||||
|
||||
Random strategy is ideal for incremental mode since it doesn't
|
||||
depend on historical calculations.
|
||||
|
||||
Returns:
|
||||
bool: Always True for random strategy
|
||||
"""
|
||||
return True
|
||||
|
||||
def calculate_on_data(self, new_data_point: Dict[str, float], timestamp: pd.Timestamp) -> None:
|
||||
"""
|
||||
Process a single new data point incrementally.
|
||||
|
||||
For random strategy, we just update our internal state with the
|
||||
current price. The base class now handles timeframe aggregation
|
||||
automatically, so we only receive data when a complete timeframe
|
||||
bar is formed.
|
||||
|
||||
Args:
|
||||
new_data_point: OHLCV data point {open, high, low, close, volume}
|
||||
timestamp: Timestamp of the data point
|
||||
"""
|
||||
start_time = time.perf_counter()
|
||||
|
||||
try:
|
||||
# Update internal state - base class handles timeframe aggregation
|
||||
self._current_price = new_data_point['close']
|
||||
self._last_timestamp = timestamp
|
||||
self._data_points_received += 1
|
||||
|
||||
# Increment bar count for each processed timeframe bar
|
||||
self._bar_count += 1
|
||||
|
||||
# Debug logging every 10 bars
|
||||
if self._bar_count % 10 == 0:
|
||||
logger.debug(f"RandomStrategy: Processing bar {self._bar_count}, "
|
||||
f"price=${self._current_price:.2f}, timestamp={timestamp}")
|
||||
|
||||
# Update warm-up status
|
||||
if not self._is_warmed_up and self._data_points_received >= 1:
|
||||
self._is_warmed_up = True
|
||||
self._calculation_mode = "incremental"
|
||||
logger.info(f"RandomStrategy: Warmed up after {self._data_points_received} data points")
|
||||
|
||||
# Record performance metrics
|
||||
update_time = time.perf_counter() - start_time
|
||||
self._performance_metrics['update_times'].append(update_time)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"RandomStrategy: Error in calculate_on_data: {e}")
|
||||
self._performance_metrics['state_validation_failures'] += 1
|
||||
raise
|
||||
|
||||
def get_entry_signal(self) -> IncStrategySignal:
|
||||
"""
|
||||
Generate random entry signals based on current state.
|
||||
|
||||
Returns:
|
||||
IncStrategySignal: Entry signal with confidence level
|
||||
"""
|
||||
if not self._is_warmed_up:
|
||||
return IncStrategySignal.HOLD()
|
||||
|
||||
start_time = time.perf_counter()
|
||||
|
||||
try:
|
||||
# Check if we should generate a signal based on frequency
|
||||
if (self._bar_count - self._last_signal_bar) < self.signal_frequency:
|
||||
return IncStrategySignal.HOLD()
|
||||
|
||||
# Generate random entry signal using strategy's random instance
|
||||
random_value = self._random.random()
|
||||
if random_value < self.entry_probability:
|
||||
confidence = self._random.uniform(self.min_confidence, self.max_confidence)
|
||||
self._last_signal_bar = self._bar_count
|
||||
|
||||
logger.info(f"RandomStrategy: Generated ENTRY signal at bar {self._bar_count}, "
|
||||
f"price=${self._current_price:.2f}, confidence={confidence:.2f}, "
|
||||
f"random_value={random_value:.3f}")
|
||||
|
||||
signal = IncStrategySignal.BUY(
|
||||
confidence=confidence,
|
||||
price=self._current_price,
|
||||
metadata={
|
||||
"strategy": "random",
|
||||
"bar_count": self._bar_count,
|
||||
"timeframe": self.timeframe,
|
||||
"random_value": random_value,
|
||||
"timestamp": self._last_timestamp
|
||||
}
|
||||
)
|
||||
|
||||
# Record performance metrics
|
||||
signal_time = time.perf_counter() - start_time
|
||||
self._performance_metrics['signal_generation_times'].append(signal_time)
|
||||
|
||||
return signal
|
||||
|
||||
return IncStrategySignal.HOLD()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"RandomStrategy: Error in get_entry_signal: {e}")
|
||||
return IncStrategySignal.HOLD()
|
||||
|
||||
def get_exit_signal(self) -> IncStrategySignal:
|
||||
"""
|
||||
Generate random exit signals based on current state.
|
||||
|
||||
Returns:
|
||||
IncStrategySignal: Exit signal with confidence level
|
||||
"""
|
||||
if not self._is_warmed_up:
|
||||
return IncStrategySignal.HOLD()
|
||||
|
||||
start_time = time.perf_counter()
|
||||
|
||||
try:
|
||||
# Generate random exit signal using strategy's random instance
|
||||
random_value = self._random.random()
|
||||
if random_value < self.exit_probability:
|
||||
confidence = self._random.uniform(self.min_confidence, self.max_confidence)
|
||||
|
||||
# Randomly choose exit type
|
||||
exit_types = ["SELL_SIGNAL", "TAKE_PROFIT", "STOP_LOSS"]
|
||||
exit_type = self._random.choice(exit_types)
|
||||
|
||||
logger.info(f"RandomStrategy: Generated EXIT signal at bar {self._bar_count}, "
|
||||
f"price=${self._current_price:.2f}, confidence={confidence:.2f}, "
|
||||
f"type={exit_type}, random_value={random_value:.3f}")
|
||||
|
||||
signal = IncStrategySignal.SELL(
|
||||
confidence=confidence,
|
||||
price=self._current_price,
|
||||
metadata={
|
||||
"type": exit_type,
|
||||
"strategy": "random",
|
||||
"bar_count": self._bar_count,
|
||||
"timeframe": self.timeframe,
|
||||
"random_value": random_value,
|
||||
"timestamp": self._last_timestamp
|
||||
}
|
||||
)
|
||||
|
||||
# Record performance metrics
|
||||
signal_time = time.perf_counter() - start_time
|
||||
self._performance_metrics['signal_generation_times'].append(signal_time)
|
||||
|
||||
return signal
|
||||
|
||||
return IncStrategySignal.HOLD()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"RandomStrategy: Error in get_exit_signal: {e}")
|
||||
return IncStrategySignal.HOLD()
|
||||
|
||||
def get_confidence(self) -> float:
|
||||
"""
|
||||
Return random confidence level for current market state.
|
||||
|
||||
Returns:
|
||||
float: Random confidence level between min and max confidence
|
||||
"""
|
||||
if not self._is_warmed_up:
|
||||
return 0.0
|
||||
|
||||
return self._random.uniform(self.min_confidence, self.max_confidence)
|
||||
|
||||
def reset_calculation_state(self) -> None:
|
||||
"""Reset internal calculation state for reinitialization."""
|
||||
super().reset_calculation_state()
|
||||
|
||||
# Reset random strategy specific state
|
||||
self._bar_count = 0
|
||||
self._last_signal_bar = -1
|
||||
self._current_price = None
|
||||
self._last_timestamp = None
|
||||
|
||||
# Reset random state if seed was provided
|
||||
random_seed = self.params.get("random_seed")
|
||||
if random_seed is not None:
|
||||
self._random.seed(random_seed)
|
||||
|
||||
logger.info("RandomStrategy: Calculation state reset")
|
||||
|
||||
def _reinitialize_from_buffers(self) -> None:
|
||||
"""
|
||||
Reinitialize indicators from available buffer data.
|
||||
|
||||
For random strategy, we just need to restore the current price
|
||||
from the latest data point in the buffer.
|
||||
"""
|
||||
try:
|
||||
# Get the latest data point from 1min buffer
|
||||
buffer_1min = self._timeframe_buffers.get("1min")
|
||||
if buffer_1min and len(buffer_1min) > 0:
|
||||
latest_data = buffer_1min[-1]
|
||||
self._current_price = latest_data['close']
|
||||
self._last_timestamp = latest_data.get('timestamp')
|
||||
self._bar_count = len(buffer_1min)
|
||||
|
||||
logger.info(f"RandomStrategy: Reinitialized from buffer with {self._bar_count} bars")
|
||||
else:
|
||||
logger.warning("RandomStrategy: No buffer data available for reinitialization")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"RandomStrategy: Error reinitializing from buffers: {e}")
|
||||
raise
|
||||
|
||||
def get_current_state_summary(self) -> Dict[str, Any]:
|
||||
"""Get summary of current calculation state for debugging."""
|
||||
base_summary = super().get_current_state_summary()
|
||||
base_summary.update({
|
||||
'entry_probability': self.entry_probability,
|
||||
'exit_probability': self.exit_probability,
|
||||
'bar_count': self._bar_count,
|
||||
'last_signal_bar': self._last_signal_bar,
|
||||
'current_price': self._current_price,
|
||||
'last_timestamp': self._last_timestamp,
|
||||
'signal_frequency': self.signal_frequency,
|
||||
'timeframe': self.timeframe
|
||||
})
|
||||
return base_summary
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""String representation of the strategy."""
|
||||
return (f"RandomStrategy(entry_prob={self.entry_probability}, "
|
||||
f"exit_prob={self.exit_probability}, timeframe={self.timeframe}, "
|
||||
f"mode={self._calculation_mode}, warmed_up={self._is_warmed_up}, "
|
||||
f"bars={self._bar_count})")
|
||||
|
||||
|
||||
# Compatibility alias for easier imports
|
||||
IncRandomStrategy = RandomStrategy
|
||||
Reference in New Issue
Block a user