deltafq 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of deltafq might be problematic. Click here for more details.
- deltafq/__init__.py +30 -31
- deltafq/backtest/__init__.py +17 -7
- deltafq/backtest/engine.py +99 -52
- deltafq/backtest/metrics.py +113 -0
- deltafq/backtest/performance.py +81 -0
- deltafq/backtest/reporter.py +91 -0
- deltafq/core/__init__.py +19 -0
- deltafq/core/base.py +37 -0
- deltafq/core/config.py +63 -0
- deltafq/core/exceptions.py +35 -0
- deltafq/core/logger.py +46 -0
- deltafq/data/__init__.py +17 -7
- deltafq/data/cleaner.py +41 -0
- deltafq/data/fetcher.py +52 -0
- deltafq/data/storage.py +56 -0
- deltafq/data/validator.py +52 -0
- deltafq/indicators/__init__.py +17 -8
- deltafq/indicators/momentum.py +56 -23
- deltafq/indicators/technical.py +59 -0
- deltafq/indicators/trend.py +129 -61
- deltafq/indicators/volatility.py +67 -27
- deltafq/live/__init__.py +17 -0
- deltafq/live/connection.py +235 -0
- deltafq/live/data_feed.py +159 -0
- deltafq/live/monitoring.py +192 -0
- deltafq/live/risk_control.py +193 -0
- deltafq/strategy/__init__.py +17 -6
- deltafq/strategy/base_strategy.py +53 -0
- deltafq/strategy/portfolio.py +82 -0
- deltafq/strategy/risk_manager.py +64 -0
- deltafq/strategy/signal_generator.py +52 -0
- deltafq/trading/__init__.py +19 -0
- deltafq/trading/broker.py +119 -0
- deltafq/trading/execution.py +176 -0
- deltafq/trading/order_manager.py +111 -0
- deltafq/trading/position_manager.py +157 -0
- deltafq/trading/simulator.py +150 -0
- deltafq-0.1.2.dist-info/METADATA +110 -0
- deltafq-0.1.2.dist-info/RECORD +43 -0
- deltafq-0.1.2.dist-info/entry_points.txt +2 -0
- {deltafq-0.1.0.dist-info → deltafq-0.1.2.dist-info}/licenses/LICENSE +21 -22
- deltafq/backtest/result.py +0 -45
- deltafq/data/base.py +0 -30
- deltafq/data/loader.py +0 -63
- deltafq/optimization/__init__.py +0 -6
- deltafq/optimization/grid_search.py +0 -41
- deltafq/performance/__init__.py +0 -6
- deltafq/performance/metrics.py +0 -37
- deltafq/risk/__init__.py +0 -7
- deltafq/risk/metrics.py +0 -33
- deltafq/risk/position.py +0 -39
- deltafq/strategy/base.py +0 -44
- deltafq/trade/__init__.py +0 -6
- deltafq/trade/broker.py +0 -40
- deltafq/utils/__init__.py +0 -6
- deltafq/utils/time.py +0 -32
- deltafq-0.1.0.dist-info/METADATA +0 -195
- deltafq-0.1.0.dist-info/RECORD +0 -29
- {deltafq-0.1.0.dist-info → deltafq-0.1.2.dist-info}/WHEEL +0 -0
- {deltafq-0.1.0.dist-info → deltafq-0.1.2.dist-info}/top_level.txt +0 -0
deltafq/indicators/volatility.py
CHANGED
|
@@ -1,27 +1,67 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
1
|
+
"""
|
|
2
|
+
Volatility indicators for DeltaFQ.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import numpy as np
|
|
7
|
+
from ..core.base import BaseComponent
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class VolatilityIndicators(BaseComponent):
|
|
11
|
+
"""Volatility-based technical indicators."""
|
|
12
|
+
|
|
13
|
+
def initialize(self) -> bool:
|
|
14
|
+
"""Initialize volatility indicators."""
|
|
15
|
+
self.logger.info("Initializing volatility indicators")
|
|
16
|
+
return True
|
|
17
|
+
|
|
18
|
+
def atr(self, high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14) -> pd.Series:
|
|
19
|
+
"""Average True Range."""
|
|
20
|
+
# True Range
|
|
21
|
+
tr1 = high - low
|
|
22
|
+
tr2 = abs(high - close.shift())
|
|
23
|
+
tr3 = abs(low - close.shift())
|
|
24
|
+
tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
|
|
25
|
+
|
|
26
|
+
# Average True Range
|
|
27
|
+
return tr.rolling(window=period).mean()
|
|
28
|
+
|
|
29
|
+
def bollinger_bandwidth(self, data: pd.Series, period: int = 20, std_dev: float = 2) -> pd.Series:
|
|
30
|
+
"""Bollinger Band Width."""
|
|
31
|
+
sma = data.rolling(window=period).mean()
|
|
32
|
+
std = data.rolling(window=period).std()
|
|
33
|
+
|
|
34
|
+
upper_band = sma + (std * std_dev)
|
|
35
|
+
lower_band = sma - (std * std_dev)
|
|
36
|
+
|
|
37
|
+
return (upper_band - lower_band) / sma
|
|
38
|
+
|
|
39
|
+
def keltner_channels(self, high: pd.Series, low: pd.Series, close: pd.Series,
|
|
40
|
+
period: int = 20, multiplier: float = 2) -> pd.DataFrame:
|
|
41
|
+
"""Keltner Channels."""
|
|
42
|
+
typical_price = (high + low + close) / 3
|
|
43
|
+
middle_line = typical_price.rolling(window=period).mean()
|
|
44
|
+
atr = self.atr(high, low, close, period)
|
|
45
|
+
|
|
46
|
+
return pd.DataFrame({
|
|
47
|
+
'upper': middle_line + (multiplier * atr),
|
|
48
|
+
'middle': middle_line,
|
|
49
|
+
'lower': middle_line - (multiplier * atr)
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
def donchian_channels(self, high: pd.Series, low: pd.Series, period: int = 20) -> pd.DataFrame:
|
|
53
|
+
"""Donchian Channels."""
|
|
54
|
+
return pd.DataFrame({
|
|
55
|
+
'upper': high.rolling(window=period).max(),
|
|
56
|
+
'lower': low.rolling(window=period).min(),
|
|
57
|
+
'middle': (high.rolling(window=period).max() + low.rolling(window=period).min()) / 2
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
def volatility_ratio(self, data: pd.Series, short_period: int = 10, long_period: int = 30) -> pd.Series:
|
|
61
|
+
"""Volatility Ratio."""
|
|
62
|
+
short_vol = data.rolling(window=short_period).std()
|
|
63
|
+
long_vol = data.rolling(window=long_period).std()
|
|
64
|
+
|
|
65
|
+
return short_vol / long_vol
|
|
66
|
+
|
|
67
|
+
|
deltafq/live/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Live trading module for DeltaFQ.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .data_feed import LiveDataFeed
|
|
6
|
+
from .risk_control import LiveRiskControl
|
|
7
|
+
from .monitoring import TradingMonitor
|
|
8
|
+
from .connection import ConnectionManager
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"LiveDataFeed",
|
|
12
|
+
"LiveRiskControl",
|
|
13
|
+
"TradingMonitor",
|
|
14
|
+
"ConnectionManager"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Connection management for live trading.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Dict, Any, Optional, List
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from ..core.base import BaseComponent
|
|
8
|
+
from ..core.exceptions import TradingError
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ConnectionManager(BaseComponent):
|
|
12
|
+
"""Manage connections to trading systems."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, **kwargs):
|
|
15
|
+
"""Initialize connection manager."""
|
|
16
|
+
super().__init__(**kwargs)
|
|
17
|
+
self.connections = {}
|
|
18
|
+
self.connection_configs = {}
|
|
19
|
+
self.health_check_interval = 60 # seconds
|
|
20
|
+
self.last_health_check = {}
|
|
21
|
+
|
|
22
|
+
def initialize(self) -> bool:
|
|
23
|
+
"""Initialize connection manager."""
|
|
24
|
+
self.logger.info("Initializing connection manager")
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
def add_connection(self, name: str, connection_type: str, config: Dict[str, Any]) -> bool:
|
|
28
|
+
"""Add a connection configuration."""
|
|
29
|
+
try:
|
|
30
|
+
self.connection_configs[name] = {
|
|
31
|
+
'type': connection_type,
|
|
32
|
+
'config': config,
|
|
33
|
+
'created_at': datetime.now(),
|
|
34
|
+
'last_used': None,
|
|
35
|
+
'status': 'disconnected'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
self.logger.info(f"Added connection: {name} ({connection_type})")
|
|
39
|
+
return True
|
|
40
|
+
|
|
41
|
+
except Exception as e:
|
|
42
|
+
self.logger.error(f"Failed to add connection: {str(e)}")
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
def connect(self, name: str) -> bool:
|
|
46
|
+
"""Establish connection."""
|
|
47
|
+
try:
|
|
48
|
+
if name not in self.connection_configs:
|
|
49
|
+
raise TradingError(f"Connection {name} not found")
|
|
50
|
+
|
|
51
|
+
config = self.connection_configs[name]
|
|
52
|
+
connection_type = config['type']
|
|
53
|
+
|
|
54
|
+
# Create connection based on type
|
|
55
|
+
if connection_type == 'broker':
|
|
56
|
+
connection = self._create_broker_connection(config['config'])
|
|
57
|
+
elif connection == 'data_feed':
|
|
58
|
+
connection = self._create_data_feed_connection(config['config'])
|
|
59
|
+
else:
|
|
60
|
+
raise TradingError(f"Unknown connection type: {connection_type}")
|
|
61
|
+
|
|
62
|
+
# Test connection
|
|
63
|
+
if self._test_connection(connection):
|
|
64
|
+
self.connections[name] = connection
|
|
65
|
+
self.connection_configs[name]['status'] = 'connected'
|
|
66
|
+
self.connection_configs[name]['last_used'] = datetime.now()
|
|
67
|
+
self.last_health_check[name] = datetime.now()
|
|
68
|
+
|
|
69
|
+
self.logger.info(f"Connected: {name}")
|
|
70
|
+
return True
|
|
71
|
+
else:
|
|
72
|
+
self.logger.error(f"Connection test failed: {name}")
|
|
73
|
+
return False
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
self.logger.error(f"Failed to connect {name}: {str(e)}")
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
def disconnect(self, name: str) -> bool:
|
|
80
|
+
"""Disconnect from service."""
|
|
81
|
+
try:
|
|
82
|
+
if name in self.connections:
|
|
83
|
+
# Close connection
|
|
84
|
+
connection = self.connections[name]
|
|
85
|
+
if hasattr(connection, 'close'):
|
|
86
|
+
connection.close()
|
|
87
|
+
|
|
88
|
+
del self.connections[name]
|
|
89
|
+
self.connection_configs[name]['status'] = 'disconnected'
|
|
90
|
+
|
|
91
|
+
self.logger.info(f"Disconnected: {name}")
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
self.logger.error(f"Failed to disconnect {name}: {str(e)}")
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def get_connection(self, name: str):
|
|
101
|
+
"""Get connection object."""
|
|
102
|
+
return self.connections.get(name)
|
|
103
|
+
|
|
104
|
+
def is_connected(self, name: str) -> bool:
|
|
105
|
+
"""Check if connection is active."""
|
|
106
|
+
return name in self.connections and self.connection_configs.get(name, {}).get('status') == 'connected'
|
|
107
|
+
|
|
108
|
+
def health_check(self, name: str) -> bool:
|
|
109
|
+
"""Perform health check on connection."""
|
|
110
|
+
try:
|
|
111
|
+
if name not in self.connections:
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
connection = self.connections[name]
|
|
115
|
+
|
|
116
|
+
# Perform health check based on connection type
|
|
117
|
+
if hasattr(connection, 'ping'):
|
|
118
|
+
result = connection.ping()
|
|
119
|
+
elif hasattr(connection, 'is_connected'):
|
|
120
|
+
result = connection.is_connected()
|
|
121
|
+
else:
|
|
122
|
+
# Default health check
|
|
123
|
+
result = True
|
|
124
|
+
|
|
125
|
+
# Update last health check time
|
|
126
|
+
self.last_health_check[name] = datetime.now()
|
|
127
|
+
|
|
128
|
+
if not result:
|
|
129
|
+
self.logger.warning(f"Health check failed: {name}")
|
|
130
|
+
self.connection_configs[name]['status'] = 'unhealthy'
|
|
131
|
+
else:
|
|
132
|
+
self.connection_configs[name]['status'] = 'connected'
|
|
133
|
+
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
self.logger.error(f"Health check error for {name}: {str(e)}")
|
|
138
|
+
self.connection_configs[name]['status'] = 'error'
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
def health_check_all(self) -> Dict[str, bool]:
|
|
142
|
+
"""Perform health check on all connections."""
|
|
143
|
+
results = {}
|
|
144
|
+
|
|
145
|
+
for name in self.connections:
|
|
146
|
+
results[name] = self.health_check(name)
|
|
147
|
+
|
|
148
|
+
return results
|
|
149
|
+
|
|
150
|
+
def auto_reconnect(self, name: str, max_attempts: int = 3) -> bool:
|
|
151
|
+
"""Attempt to automatically reconnect."""
|
|
152
|
+
try:
|
|
153
|
+
if name not in self.connection_configs:
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
for attempt in range(max_attempts):
|
|
157
|
+
self.logger.info(f"Reconnection attempt {attempt + 1} for {name}")
|
|
158
|
+
|
|
159
|
+
if self.connect(name):
|
|
160
|
+
self.logger.info(f"Successfully reconnected: {name}")
|
|
161
|
+
return True
|
|
162
|
+
|
|
163
|
+
# Wait before next attempt
|
|
164
|
+
import time
|
|
165
|
+
time.sleep(5)
|
|
166
|
+
|
|
167
|
+
self.logger.error(f"Failed to reconnect {name} after {max_attempts} attempts")
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
except Exception as e:
|
|
171
|
+
self.logger.error(f"Auto-reconnect failed for {name}: {str(e)}")
|
|
172
|
+
return False
|
|
173
|
+
|
|
174
|
+
def _create_broker_connection(self, config: Dict[str, Any]):
|
|
175
|
+
"""Create broker connection."""
|
|
176
|
+
# Placeholder for broker connection creation
|
|
177
|
+
return MockConnection('broker', config)
|
|
178
|
+
|
|
179
|
+
def _create_data_feed_connection(self, config: Dict[str, Any]):
|
|
180
|
+
"""Create data feed connection."""
|
|
181
|
+
# Placeholder for data feed connection creation
|
|
182
|
+
return MockConnection('data_feed', config)
|
|
183
|
+
|
|
184
|
+
def _test_connection(self, connection) -> bool:
|
|
185
|
+
"""Test connection."""
|
|
186
|
+
try:
|
|
187
|
+
# Simple connection test
|
|
188
|
+
if hasattr(connection, 'test'):
|
|
189
|
+
return connection.test()
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
except Exception as e:
|
|
193
|
+
self.logger.error(f"Connection test failed: {str(e)}")
|
|
194
|
+
return False
|
|
195
|
+
|
|
196
|
+
def get_connection_status(self) -> Dict[str, Any]:
|
|
197
|
+
"""Get status of all connections."""
|
|
198
|
+
return {
|
|
199
|
+
'total_connections': len(self.connection_configs),
|
|
200
|
+
'active_connections': len(self.connections),
|
|
201
|
+
'connections': {
|
|
202
|
+
name: {
|
|
203
|
+
'type': config['type'],
|
|
204
|
+
'status': config['status'],
|
|
205
|
+
'last_used': config['last_used'],
|
|
206
|
+
'last_health_check': self.last_health_check.get(name)
|
|
207
|
+
}
|
|
208
|
+
for name, config in self.connection_configs.items()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
class MockConnection:
|
|
214
|
+
"""Mock connection for testing."""
|
|
215
|
+
|
|
216
|
+
def __init__(self, connection_type: str, config: Dict[str, Any]):
|
|
217
|
+
self.connection_type = connection_type
|
|
218
|
+
self.config = config
|
|
219
|
+
self.connected = True
|
|
220
|
+
|
|
221
|
+
def test(self) -> bool:
|
|
222
|
+
"""Test connection."""
|
|
223
|
+
return True
|
|
224
|
+
|
|
225
|
+
def ping(self) -> bool:
|
|
226
|
+
"""Ping connection."""
|
|
227
|
+
return True
|
|
228
|
+
|
|
229
|
+
def is_connected(self) -> bool:
|
|
230
|
+
"""Check if connected."""
|
|
231
|
+
return self.connected
|
|
232
|
+
|
|
233
|
+
def close(self):
|
|
234
|
+
"""Close connection."""
|
|
235
|
+
self.connected = False
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Live data feed management for real-time trading.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
from typing import Dict, List, Callable, Optional, Any
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from ..core.base import BaseComponent
|
|
9
|
+
from ..core.exceptions import TradingError
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LiveDataFeed(BaseComponent):
|
|
13
|
+
"""Manages real-time market data feeds."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, **kwargs):
|
|
16
|
+
"""Initialize live data feed."""
|
|
17
|
+
super().__init__(**kwargs)
|
|
18
|
+
self.subscribers = {}
|
|
19
|
+
self.data_callbacks = []
|
|
20
|
+
self.is_running = False
|
|
21
|
+
self.last_prices = {}
|
|
22
|
+
|
|
23
|
+
def initialize(self) -> bool:
|
|
24
|
+
"""Initialize live data feed."""
|
|
25
|
+
self.logger.info("Initializing live data feed")
|
|
26
|
+
return True
|
|
27
|
+
|
|
28
|
+
def subscribe(self, symbols: List[str], callback: Optional[Callable] = None) -> bool:
|
|
29
|
+
"""Subscribe to live data for given symbols."""
|
|
30
|
+
try:
|
|
31
|
+
for symbol in symbols:
|
|
32
|
+
if symbol not in self.subscribers:
|
|
33
|
+
self.subscribers[symbol] = []
|
|
34
|
+
|
|
35
|
+
if callback:
|
|
36
|
+
self.subscribers[symbol].append(callback)
|
|
37
|
+
|
|
38
|
+
self.logger.info(f"Subscribed to {symbol}")
|
|
39
|
+
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
except Exception as e:
|
|
43
|
+
raise TradingError(f"Failed to subscribe to symbols: {str(e)}")
|
|
44
|
+
|
|
45
|
+
def unsubscribe(self, symbols: List[str]) -> bool:
|
|
46
|
+
"""Unsubscribe from live data."""
|
|
47
|
+
try:
|
|
48
|
+
for symbol in symbols:
|
|
49
|
+
if symbol in self.subscribers:
|
|
50
|
+
del self.subscribers[symbol]
|
|
51
|
+
self.logger.info(f"Unsubscribed from {symbol}")
|
|
52
|
+
|
|
53
|
+
return True
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
self.logger.error(f"Failed to unsubscribe: {str(e)}")
|
|
57
|
+
return False
|
|
58
|
+
|
|
59
|
+
def add_data_callback(self, callback: Callable) -> bool:
|
|
60
|
+
"""Add a general data callback."""
|
|
61
|
+
self.data_callbacks.append(callback)
|
|
62
|
+
return True
|
|
63
|
+
|
|
64
|
+
def start_feed(self) -> bool:
|
|
65
|
+
"""Start the live data feed."""
|
|
66
|
+
try:
|
|
67
|
+
self.is_running = True
|
|
68
|
+
self.logger.info("Live data feed started")
|
|
69
|
+
|
|
70
|
+
# In a real implementation, this would start the actual data feed
|
|
71
|
+
# For now, we'll simulate with periodic updates
|
|
72
|
+
self._simulate_data_feed()
|
|
73
|
+
|
|
74
|
+
return True
|
|
75
|
+
|
|
76
|
+
except Exception as e:
|
|
77
|
+
self.is_running = False
|
|
78
|
+
raise TradingError(f"Failed to start data feed: {str(e)}")
|
|
79
|
+
|
|
80
|
+
def stop_feed(self) -> bool:
|
|
81
|
+
"""Stop the live data feed."""
|
|
82
|
+
try:
|
|
83
|
+
self.is_running = False
|
|
84
|
+
self.logger.info("Live data feed stopped")
|
|
85
|
+
return True
|
|
86
|
+
|
|
87
|
+
except Exception as e:
|
|
88
|
+
self.logger.error(f"Failed to stop data feed: {str(e)}")
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
def get_latest_price(self, symbol: str) -> Optional[float]:
|
|
92
|
+
"""Get latest price for a symbol."""
|
|
93
|
+
return self.last_prices.get(symbol)
|
|
94
|
+
|
|
95
|
+
def get_latest_prices(self, symbols: List[str]) -> Dict[str, float]:
|
|
96
|
+
"""Get latest prices for multiple symbols."""
|
|
97
|
+
return {symbol: self.last_prices.get(symbol) for symbol in symbols if symbol in self.last_prices}
|
|
98
|
+
|
|
99
|
+
def _simulate_data_feed(self):
|
|
100
|
+
"""Simulate live data feed for testing."""
|
|
101
|
+
import time
|
|
102
|
+
import random
|
|
103
|
+
|
|
104
|
+
base_prices = {
|
|
105
|
+
'AAPL': 150.0,
|
|
106
|
+
'GOOGL': 2500.0,
|
|
107
|
+
'MSFT': 300.0,
|
|
108
|
+
'TSLA': 200.0
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
while self.is_running:
|
|
112
|
+
for symbol in self.subscribers.keys():
|
|
113
|
+
if symbol in base_prices:
|
|
114
|
+
# Simulate price movement
|
|
115
|
+
current_price = self.last_prices.get(symbol, base_prices[symbol])
|
|
116
|
+
change = random.uniform(-0.02, 0.02) # ±2% change
|
|
117
|
+
new_price = current_price * (1 + change)
|
|
118
|
+
|
|
119
|
+
self.last_prices[symbol] = new_price
|
|
120
|
+
|
|
121
|
+
# Create data point
|
|
122
|
+
data_point = {
|
|
123
|
+
'symbol': symbol,
|
|
124
|
+
'price': new_price,
|
|
125
|
+
'timestamp': datetime.now(),
|
|
126
|
+
'volume': random.randint(1000, 10000)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
# Notify subscribers
|
|
130
|
+
self._notify_subscribers(symbol, data_point)
|
|
131
|
+
|
|
132
|
+
time.sleep(1) # Update every second
|
|
133
|
+
|
|
134
|
+
def _notify_subscribers(self, symbol: str, data_point: Dict[str, Any]):
|
|
135
|
+
"""Notify subscribers of new data."""
|
|
136
|
+
try:
|
|
137
|
+
# Notify symbol-specific subscribers
|
|
138
|
+
if symbol in self.subscribers:
|
|
139
|
+
for callback in self.subscribers[symbol]:
|
|
140
|
+
callback(data_point)
|
|
141
|
+
|
|
142
|
+
# Notify general data callbacks
|
|
143
|
+
for callback in self.data_callbacks:
|
|
144
|
+
callback(data_point)
|
|
145
|
+
|
|
146
|
+
except Exception as e:
|
|
147
|
+
self.logger.error(f"Error notifying subscribers: {str(e)}")
|
|
148
|
+
|
|
149
|
+
def get_feed_status(self) -> Dict[str, Any]:
|
|
150
|
+
"""Get feed status information."""
|
|
151
|
+
return {
|
|
152
|
+
'is_running': self.is_running,
|
|
153
|
+
'subscribed_symbols': list(self.subscribers.keys()),
|
|
154
|
+
'total_subscribers': sum(len(callbacks) for callbacks in self.subscribers.values()),
|
|
155
|
+
'data_callbacks': len(self.data_callbacks),
|
|
156
|
+
'latest_prices': dict(self.last_prices)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
|