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:
Vasily.onl
2025-05-28 16:29:48 +08:00
parent 8055f46328
commit c9ae507bb7
21 changed files with 5898 additions and 0 deletions

View 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",
]

View 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})")

View 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

View 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",
]

View 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

View 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

View 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

View 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

View 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

View 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]
}

View 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

View 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