336 lines
14 KiB
Python
336 lines
14 KiB
Python
"""
|
|
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}")
|
|
|
|
if self._timeframe_aggregator is not None:
|
|
logger.info(f"Using new timeframe utilities with mathematically correct aggregation")
|
|
logger.info(f"Random signals will be generated on complete {self.timeframe} bars only")
|
|
|
|
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 |