558 lines
20 KiB
Python
558 lines
20 KiB
Python
|
|
"""
|
||
|
|
Tests for real-time strategy execution pipeline.
|
||
|
|
"""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
import pandas as pd
|
||
|
|
from datetime import datetime, timezone, timedelta
|
||
|
|
from unittest.mock import Mock, patch, MagicMock
|
||
|
|
import time
|
||
|
|
from queue import Queue, Empty
|
||
|
|
import threading
|
||
|
|
|
||
|
|
from strategies.realtime_execution import (
|
||
|
|
RealTimeStrategyProcessor,
|
||
|
|
StrategySignalBroadcaster,
|
||
|
|
RealTimeConfig,
|
||
|
|
StrategyExecutionContext,
|
||
|
|
RealTimeSignal,
|
||
|
|
get_realtime_strategy_processor,
|
||
|
|
initialize_realtime_strategy_system,
|
||
|
|
shutdown_realtime_strategy_system
|
||
|
|
)
|
||
|
|
from strategies.data_types import StrategyResult, StrategySignal, SignalType
|
||
|
|
from data.common.data_types import OHLCVCandle
|
||
|
|
|
||
|
|
|
||
|
|
class TestRealTimeConfig:
|
||
|
|
"""Test RealTimeConfig dataclass."""
|
||
|
|
|
||
|
|
def test_default_config(self):
|
||
|
|
"""Test default configuration values."""
|
||
|
|
config = RealTimeConfig()
|
||
|
|
|
||
|
|
assert config.refresh_interval_seconds == 30
|
||
|
|
assert config.max_strategies_concurrent == 5
|
||
|
|
assert config.incremental_calculation == True
|
||
|
|
assert config.signal_batch_size == 100
|
||
|
|
assert config.enable_signal_broadcasting == True
|
||
|
|
assert config.max_signal_queue_size == 1000
|
||
|
|
assert config.chart_update_throttle_ms == 1000
|
||
|
|
assert config.error_retry_attempts == 3
|
||
|
|
assert config.error_retry_delay_seconds == 5
|
||
|
|
|
||
|
|
def test_custom_config(self):
|
||
|
|
"""Test custom configuration values."""
|
||
|
|
config = RealTimeConfig(
|
||
|
|
refresh_interval_seconds=15,
|
||
|
|
max_strategies_concurrent=3,
|
||
|
|
incremental_calculation=False,
|
||
|
|
signal_batch_size=50
|
||
|
|
)
|
||
|
|
|
||
|
|
assert config.refresh_interval_seconds == 15
|
||
|
|
assert config.max_strategies_concurrent == 3
|
||
|
|
assert config.incremental_calculation == False
|
||
|
|
assert config.signal_batch_size == 50
|
||
|
|
|
||
|
|
|
||
|
|
class TestStrategyExecutionContext:
|
||
|
|
"""Test StrategyExecutionContext dataclass."""
|
||
|
|
|
||
|
|
def test_context_creation(self):
|
||
|
|
"""Test strategy execution context creation."""
|
||
|
|
context = StrategyExecutionContext(
|
||
|
|
strategy_name="ema_crossover",
|
||
|
|
strategy_config={"short_period": 12, "long_period": 26},
|
||
|
|
symbol="BTC-USDT",
|
||
|
|
timeframe="1h"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert context.strategy_name == "ema_crossover"
|
||
|
|
assert context.strategy_config == {"short_period": 12, "long_period": 26}
|
||
|
|
assert context.symbol == "BTC-USDT"
|
||
|
|
assert context.timeframe == "1h"
|
||
|
|
assert context.exchange == "okx"
|
||
|
|
assert context.last_calculation_time is None
|
||
|
|
assert context.consecutive_errors == 0
|
||
|
|
assert context.is_active == True
|
||
|
|
|
||
|
|
def test_context_with_custom_exchange(self):
|
||
|
|
"""Test context with custom exchange."""
|
||
|
|
context = StrategyExecutionContext(
|
||
|
|
strategy_name="rsi",
|
||
|
|
strategy_config={"period": 14},
|
||
|
|
symbol="ETH-USDT",
|
||
|
|
timeframe="4h",
|
||
|
|
exchange="binance"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert context.exchange == "binance"
|
||
|
|
|
||
|
|
|
||
|
|
class TestRealTimeSignal:
|
||
|
|
"""Test RealTimeSignal dataclass."""
|
||
|
|
|
||
|
|
def test_signal_creation(self):
|
||
|
|
"""Test real-time signal creation."""
|
||
|
|
# Create mock strategy result
|
||
|
|
strategy_result = Mock(spec=StrategyResult)
|
||
|
|
strategy_result.timestamp = datetime.now(timezone.utc)
|
||
|
|
strategy_result.confidence = 0.8
|
||
|
|
|
||
|
|
# Create context
|
||
|
|
context = StrategyExecutionContext(
|
||
|
|
strategy_name="macd",
|
||
|
|
strategy_config={"fast_period": 12},
|
||
|
|
symbol="BTC-USDT",
|
||
|
|
timeframe="1d"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Create signal
|
||
|
|
signal = RealTimeSignal(
|
||
|
|
strategy_result=strategy_result,
|
||
|
|
context=context
|
||
|
|
)
|
||
|
|
|
||
|
|
assert signal.strategy_result == strategy_result
|
||
|
|
assert signal.context == context
|
||
|
|
assert signal.chart_update_required == True
|
||
|
|
assert isinstance(signal.generation_time, datetime)
|
||
|
|
|
||
|
|
|
||
|
|
class TestStrategySignalBroadcaster:
|
||
|
|
"""Test StrategySignalBroadcaster class."""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def config(self):
|
||
|
|
"""Test configuration."""
|
||
|
|
return RealTimeConfig(
|
||
|
|
signal_batch_size=5,
|
||
|
|
max_signal_queue_size=10,
|
||
|
|
chart_update_throttle_ms=100
|
||
|
|
)
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_db_ops(self):
|
||
|
|
"""Mock database operations."""
|
||
|
|
with patch('strategies.realtime_execution.get_database_operations') as mock:
|
||
|
|
db_ops = Mock()
|
||
|
|
db_ops.strategy = Mock()
|
||
|
|
db_ops.strategy.store_signals_batch = Mock(return_value=5)
|
||
|
|
mock.return_value = db_ops
|
||
|
|
yield db_ops
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def broadcaster(self, config, mock_db_ops):
|
||
|
|
"""Create broadcaster instance."""
|
||
|
|
return StrategySignalBroadcaster(config)
|
||
|
|
|
||
|
|
def test_broadcaster_initialization(self, broadcaster, config):
|
||
|
|
"""Test broadcaster initialization."""
|
||
|
|
assert broadcaster.config == config
|
||
|
|
assert broadcaster._is_running == False
|
||
|
|
assert broadcaster._chart_update_callback is None
|
||
|
|
|
||
|
|
def test_start_stop_broadcaster(self, broadcaster):
|
||
|
|
"""Test starting and stopping broadcaster."""
|
||
|
|
assert not broadcaster._is_running
|
||
|
|
|
||
|
|
broadcaster.start()
|
||
|
|
assert broadcaster._is_running
|
||
|
|
assert broadcaster._processing_thread is not None
|
||
|
|
|
||
|
|
broadcaster.stop()
|
||
|
|
assert not broadcaster._is_running
|
||
|
|
|
||
|
|
def test_broadcast_signal(self, broadcaster):
|
||
|
|
"""Test broadcasting signals."""
|
||
|
|
# Create test signal
|
||
|
|
strategy_result = Mock(spec=StrategyResult)
|
||
|
|
context = StrategyExecutionContext(
|
||
|
|
strategy_name="test",
|
||
|
|
strategy_config={},
|
||
|
|
symbol="BTC-USDT",
|
||
|
|
timeframe="1h"
|
||
|
|
)
|
||
|
|
signal = RealTimeSignal(strategy_result=strategy_result, context=context)
|
||
|
|
|
||
|
|
# Broadcast signal
|
||
|
|
success = broadcaster.broadcast_signal(signal)
|
||
|
|
assert success == True
|
||
|
|
|
||
|
|
# Check queue has signal
|
||
|
|
assert broadcaster._signal_queue.qsize() == 1
|
||
|
|
|
||
|
|
def test_broadcast_signal_queue_full(self, config, mock_db_ops):
|
||
|
|
"""Test broadcasting when queue is full."""
|
||
|
|
# Create broadcaster with very small queue
|
||
|
|
small_config = RealTimeConfig(max_signal_queue_size=1)
|
||
|
|
broadcaster = StrategySignalBroadcaster(small_config)
|
||
|
|
|
||
|
|
# Create test signals
|
||
|
|
strategy_result = Mock(spec=StrategyResult)
|
||
|
|
context = StrategyExecutionContext(
|
||
|
|
strategy_name="test",
|
||
|
|
strategy_config={},
|
||
|
|
symbol="BTC-USDT",
|
||
|
|
timeframe="1h"
|
||
|
|
)
|
||
|
|
signal1 = RealTimeSignal(strategy_result=strategy_result, context=context)
|
||
|
|
signal2 = RealTimeSignal(strategy_result=strategy_result, context=context)
|
||
|
|
|
||
|
|
# Fill queue
|
||
|
|
success1 = broadcaster.broadcast_signal(signal1)
|
||
|
|
assert success1 == True
|
||
|
|
|
||
|
|
# Try to overfill queue
|
||
|
|
success2 = broadcaster.broadcast_signal(signal2)
|
||
|
|
assert success2 == False # Should fail due to full queue
|
||
|
|
|
||
|
|
def test_set_chart_update_callback(self, broadcaster):
|
||
|
|
"""Test setting chart update callback."""
|
||
|
|
callback = Mock()
|
||
|
|
broadcaster.set_chart_update_callback(callback)
|
||
|
|
assert broadcaster._chart_update_callback == callback
|
||
|
|
|
||
|
|
def test_get_signal_stats(self, broadcaster):
|
||
|
|
"""Test getting signal statistics."""
|
||
|
|
stats = broadcaster.get_signal_stats()
|
||
|
|
|
||
|
|
assert 'queue_size' in stats
|
||
|
|
assert 'chart_queue_size' in stats
|
||
|
|
assert 'is_running' in stats
|
||
|
|
assert 'last_chart_updates' in stats
|
||
|
|
assert stats['is_running'] == False
|
||
|
|
|
||
|
|
|
||
|
|
class TestRealTimeStrategyProcessor:
|
||
|
|
"""Test RealTimeStrategyProcessor class."""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def config(self):
|
||
|
|
"""Test configuration."""
|
||
|
|
return RealTimeConfig(
|
||
|
|
max_strategies_concurrent=2,
|
||
|
|
error_retry_attempts=2
|
||
|
|
)
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def mock_dependencies(self):
|
||
|
|
"""Mock all external dependencies."""
|
||
|
|
mocks = {}
|
||
|
|
|
||
|
|
with patch('strategies.realtime_execution.StrategyDataIntegrator') as mock_integrator:
|
||
|
|
mocks['data_integrator'] = Mock()
|
||
|
|
mock_integrator.return_value = mocks['data_integrator']
|
||
|
|
|
||
|
|
with patch('strategies.realtime_execution.MarketDataIntegrator') as mock_market:
|
||
|
|
mocks['market_integrator'] = Mock()
|
||
|
|
mock_market.return_value = mocks['market_integrator']
|
||
|
|
|
||
|
|
with patch('strategies.realtime_execution.StrategyFactory') as mock_factory:
|
||
|
|
mocks['strategy_factory'] = Mock()
|
||
|
|
mock_factory.return_value = mocks['strategy_factory']
|
||
|
|
|
||
|
|
yield mocks
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def processor(self, config, mock_dependencies):
|
||
|
|
"""Create processor instance."""
|
||
|
|
return RealTimeStrategyProcessor(config)
|
||
|
|
|
||
|
|
def test_processor_initialization(self, processor, config):
|
||
|
|
"""Test processor initialization."""
|
||
|
|
assert processor.config == config
|
||
|
|
assert processor._execution_contexts == {}
|
||
|
|
assert processor._performance_stats['total_calculations'] == 0
|
||
|
|
|
||
|
|
def test_start_stop_processor(self, processor):
|
||
|
|
"""Test starting and stopping processor."""
|
||
|
|
processor.start()
|
||
|
|
assert processor.signal_broadcaster._is_running == True
|
||
|
|
|
||
|
|
processor.stop()
|
||
|
|
assert processor.signal_broadcaster._is_running == False
|
||
|
|
|
||
|
|
def test_register_strategy(self, processor):
|
||
|
|
"""Test registering strategy for real-time execution."""
|
||
|
|
context_id = processor.register_strategy(
|
||
|
|
strategy_name="ema_crossover",
|
||
|
|
strategy_config={"short_period": 12, "long_period": 26},
|
||
|
|
symbol="BTC-USDT",
|
||
|
|
timeframe="1h"
|
||
|
|
)
|
||
|
|
|
||
|
|
expected_id = "ema_crossover_BTC-USDT_1h_okx"
|
||
|
|
assert context_id == expected_id
|
||
|
|
assert context_id in processor._execution_contexts
|
||
|
|
|
||
|
|
context = processor._execution_contexts[context_id]
|
||
|
|
assert context.strategy_name == "ema_crossover"
|
||
|
|
assert context.symbol == "BTC-USDT"
|
||
|
|
assert context.timeframe == "1h"
|
||
|
|
assert context.is_active == True
|
||
|
|
|
||
|
|
def test_unregister_strategy(self, processor):
|
||
|
|
"""Test unregistering strategy."""
|
||
|
|
# Register first
|
||
|
|
context_id = processor.register_strategy(
|
||
|
|
strategy_name="rsi",
|
||
|
|
strategy_config={"period": 14},
|
||
|
|
symbol="ETH-USDT",
|
||
|
|
timeframe="4h"
|
||
|
|
)
|
||
|
|
|
||
|
|
assert context_id in processor._execution_contexts
|
||
|
|
|
||
|
|
# Unregister
|
||
|
|
success = processor.unregister_strategy(context_id)
|
||
|
|
assert success == True
|
||
|
|
assert context_id not in processor._execution_contexts
|
||
|
|
|
||
|
|
# Try to unregister again
|
||
|
|
success2 = processor.unregister_strategy(context_id)
|
||
|
|
assert success2 == False
|
||
|
|
|
||
|
|
def test_execute_realtime_update_no_strategies(self, processor):
|
||
|
|
"""Test real-time update with no registered strategies."""
|
||
|
|
signals = processor.execute_realtime_update("BTC-USDT", "1h")
|
||
|
|
assert signals == []
|
||
|
|
|
||
|
|
def test_execute_realtime_update_with_strategies(self, processor, mock_dependencies):
|
||
|
|
"""Test real-time update with registered strategies."""
|
||
|
|
# Mock strategy calculation results
|
||
|
|
mock_result = Mock(spec=StrategyResult)
|
||
|
|
mock_result.timestamp = datetime.now(timezone.utc)
|
||
|
|
mock_result.confidence = 0.8
|
||
|
|
|
||
|
|
mock_dependencies['data_integrator'].calculate_strategy_signals.return_value = [mock_result]
|
||
|
|
|
||
|
|
# Register strategy
|
||
|
|
processor.register_strategy(
|
||
|
|
strategy_name="ema_crossover",
|
||
|
|
strategy_config={"short_period": 12, "long_period": 26},
|
||
|
|
symbol="BTC-USDT",
|
||
|
|
timeframe="1h"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Execute update
|
||
|
|
signals = processor.execute_realtime_update("BTC-USDT", "1h")
|
||
|
|
|
||
|
|
assert len(signals) == 1
|
||
|
|
assert isinstance(signals[0], RealTimeSignal)
|
||
|
|
assert signals[0].strategy_result == mock_result
|
||
|
|
|
||
|
|
def test_get_active_strategies(self, processor):
|
||
|
|
"""Test getting active strategies."""
|
||
|
|
# Register some strategies
|
||
|
|
processor.register_strategy("ema", {}, "BTC-USDT", "1h")
|
||
|
|
processor.register_strategy("rsi", {}, "ETH-USDT", "4h")
|
||
|
|
|
||
|
|
active = processor.get_active_strategies()
|
||
|
|
assert len(active) == 2
|
||
|
|
|
||
|
|
# Pause one strategy
|
||
|
|
context_id = list(active.keys())[0]
|
||
|
|
processor.pause_strategy(context_id)
|
||
|
|
|
||
|
|
active_after_pause = processor.get_active_strategies()
|
||
|
|
assert len(active_after_pause) == 1
|
||
|
|
|
||
|
|
def test_pause_resume_strategy(self, processor):
|
||
|
|
"""Test pausing and resuming strategies."""
|
||
|
|
context_id = processor.register_strategy("macd", {}, "BTC-USDT", "1d")
|
||
|
|
|
||
|
|
# Pause strategy
|
||
|
|
success = processor.pause_strategy(context_id)
|
||
|
|
assert success == True
|
||
|
|
assert not processor._execution_contexts[context_id].is_active
|
||
|
|
|
||
|
|
# Resume strategy
|
||
|
|
success = processor.resume_strategy(context_id)
|
||
|
|
assert success == True
|
||
|
|
assert processor._execution_contexts[context_id].is_active
|
||
|
|
|
||
|
|
# Test with invalid context_id
|
||
|
|
invalid_success = processor.pause_strategy("invalid_id")
|
||
|
|
assert invalid_success == False
|
||
|
|
|
||
|
|
def test_get_performance_stats(self, processor):
|
||
|
|
"""Test getting performance statistics."""
|
||
|
|
stats = processor.get_performance_stats()
|
||
|
|
|
||
|
|
assert 'total_calculations' in stats
|
||
|
|
assert 'successful_calculations' in stats
|
||
|
|
assert 'failed_calculations' in stats
|
||
|
|
assert 'average_calculation_time_ms' in stats
|
||
|
|
assert 'signals_generated' in stats
|
||
|
|
assert 'queue_size' in stats # From signal broadcaster
|
||
|
|
|
||
|
|
|
||
|
|
class TestSingletonAndInitialization:
|
||
|
|
"""Test singleton pattern and system initialization."""
|
||
|
|
|
||
|
|
def test_get_realtime_strategy_processor_singleton(self):
|
||
|
|
"""Test that processor is singleton."""
|
||
|
|
# Clean up any existing processor
|
||
|
|
shutdown_realtime_strategy_system()
|
||
|
|
|
||
|
|
processor1 = get_realtime_strategy_processor()
|
||
|
|
processor2 = get_realtime_strategy_processor()
|
||
|
|
|
||
|
|
assert processor1 is processor2
|
||
|
|
|
||
|
|
# Clean up
|
||
|
|
shutdown_realtime_strategy_system()
|
||
|
|
|
||
|
|
def test_initialize_realtime_strategy_system(self):
|
||
|
|
"""Test system initialization."""
|
||
|
|
# Clean up any existing processor
|
||
|
|
shutdown_realtime_strategy_system()
|
||
|
|
|
||
|
|
config = RealTimeConfig(max_strategies_concurrent=2)
|
||
|
|
processor = initialize_realtime_strategy_system(config)
|
||
|
|
|
||
|
|
assert processor is not None
|
||
|
|
assert processor.signal_broadcaster._is_running == True
|
||
|
|
|
||
|
|
# Clean up
|
||
|
|
shutdown_realtime_strategy_system()
|
||
|
|
|
||
|
|
def test_shutdown_realtime_strategy_system(self):
|
||
|
|
"""Test system shutdown."""
|
||
|
|
# Initialize system
|
||
|
|
processor = initialize_realtime_strategy_system()
|
||
|
|
assert processor.signal_broadcaster._is_running == True
|
||
|
|
|
||
|
|
# Shutdown
|
||
|
|
shutdown_realtime_strategy_system()
|
||
|
|
|
||
|
|
# Verify shutdown
|
||
|
|
# Note: After shutdown, the global processor is set to None
|
||
|
|
# So we can't check the processor state, but we can verify
|
||
|
|
# a new processor is created on next call
|
||
|
|
new_processor = get_realtime_strategy_processor()
|
||
|
|
assert new_processor is not None
|
||
|
|
|
||
|
|
|
||
|
|
class TestIntegration:
|
||
|
|
"""Integration tests for real-time execution pipeline."""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def integration_config(self):
|
||
|
|
"""Configuration for integration tests."""
|
||
|
|
return RealTimeConfig(
|
||
|
|
signal_batch_size=2,
|
||
|
|
max_signal_queue_size=5,
|
||
|
|
chart_update_throttle_ms=50
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_end_to_end_signal_flow(self, integration_config):
|
||
|
|
"""Test complete signal flow from strategy to storage."""
|
||
|
|
with patch('strategies.realtime_execution.get_database_operations') as mock_db:
|
||
|
|
# Setup mocks
|
||
|
|
db_ops = Mock()
|
||
|
|
db_ops.strategy = Mock()
|
||
|
|
db_ops.strategy.store_signals_batch = Mock(return_value=2)
|
||
|
|
mock_db.return_value = db_ops
|
||
|
|
|
||
|
|
# Create processor
|
||
|
|
processor = RealTimeStrategyProcessor(integration_config)
|
||
|
|
processor.start()
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Mock strategy calculation
|
||
|
|
mock_result = Mock(spec=StrategyResult)
|
||
|
|
mock_result.timestamp = datetime.now(timezone.utc)
|
||
|
|
mock_result.confidence = 0.8
|
||
|
|
mock_result.signal = Mock()
|
||
|
|
mock_result.signal.signal_type = SignalType.BUY
|
||
|
|
mock_result.price = 50000.0
|
||
|
|
mock_result.metadata = {"test": True}
|
||
|
|
|
||
|
|
with patch.object(processor.data_integrator, 'calculate_strategy_signals') as mock_calc:
|
||
|
|
mock_calc.return_value = [mock_result]
|
||
|
|
|
||
|
|
# Register strategy
|
||
|
|
processor.register_strategy(
|
||
|
|
strategy_name="test_strategy",
|
||
|
|
strategy_config={"param": "value"},
|
||
|
|
symbol="BTC-USDT",
|
||
|
|
timeframe="1h"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Execute real-time update
|
||
|
|
signals = processor.execute_realtime_update("BTC-USDT", "1h")
|
||
|
|
|
||
|
|
assert len(signals) == 1
|
||
|
|
|
||
|
|
# Wait for signal processing
|
||
|
|
time.sleep(0.2) # Allow background processing
|
||
|
|
|
||
|
|
# Verify calculation was called
|
||
|
|
mock_calc.assert_called_once()
|
||
|
|
|
||
|
|
finally:
|
||
|
|
processor.stop()
|
||
|
|
|
||
|
|
def test_error_handling_and_retry(self, integration_config):
|
||
|
|
"""Test error handling and retry mechanisms."""
|
||
|
|
processor = RealTimeStrategyProcessor(integration_config)
|
||
|
|
processor.start()
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Mock strategy calculation to raise error
|
||
|
|
with patch.object(processor.data_integrator, 'calculate_strategy_signals') as mock_calc:
|
||
|
|
mock_calc.side_effect = Exception("Test error")
|
||
|
|
|
||
|
|
# Register strategy
|
||
|
|
context_id = processor.register_strategy(
|
||
|
|
strategy_name="error_strategy",
|
||
|
|
strategy_config={},
|
||
|
|
symbol="BTC-USDT",
|
||
|
|
timeframe="1h"
|
||
|
|
)
|
||
|
|
|
||
|
|
# Execute multiple times to trigger error handling
|
||
|
|
for _ in range(integration_config.error_retry_attempts + 1):
|
||
|
|
processor.execute_realtime_update("BTC-USDT", "1h")
|
||
|
|
|
||
|
|
# Strategy should be disabled after max errors
|
||
|
|
context = processor._execution_contexts[context_id]
|
||
|
|
assert not context.is_active
|
||
|
|
assert context.consecutive_errors >= integration_config.error_retry_attempts
|
||
|
|
|
||
|
|
finally:
|
||
|
|
processor.stop()
|
||
|
|
|
||
|
|
def test_concurrent_strategy_execution(self, integration_config):
|
||
|
|
"""Test concurrent execution of multiple strategies."""
|
||
|
|
processor = RealTimeStrategyProcessor(integration_config)
|
||
|
|
processor.start()
|
||
|
|
|
||
|
|
try:
|
||
|
|
# Mock strategy calculations
|
||
|
|
mock_result1 = Mock(spec=StrategyResult)
|
||
|
|
mock_result1.timestamp = datetime.now(timezone.utc)
|
||
|
|
mock_result1.confidence = 0.7
|
||
|
|
|
||
|
|
mock_result2 = Mock(spec=StrategyResult)
|
||
|
|
mock_result2.timestamp = datetime.now(timezone.utc)
|
||
|
|
mock_result2.confidence = 0.9
|
||
|
|
|
||
|
|
with patch.object(processor.data_integrator, 'calculate_strategy_signals') as mock_calc:
|
||
|
|
mock_calc.side_effect = [[mock_result1], [mock_result2]]
|
||
|
|
|
||
|
|
# Register multiple strategies for same symbol/timeframe
|
||
|
|
processor.register_strategy("strategy1", {}, "BTC-USDT", "1h")
|
||
|
|
processor.register_strategy("strategy2", {}, "BTC-USDT", "1h")
|
||
|
|
|
||
|
|
# Execute update
|
||
|
|
signals = processor.execute_realtime_update("BTC-USDT", "1h")
|
||
|
|
|
||
|
|
# Should get signals from both strategies
|
||
|
|
assert len(signals) == 2
|
||
|
|
|
||
|
|
finally:
|
||
|
|
processor.stop()
|