deltafq 0.4.0__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.
Files changed (42) hide show
  1. deltafq/__init__.py +29 -0
  2. deltafq/backtest/__init__.py +32 -0
  3. deltafq/backtest/engine.py +145 -0
  4. deltafq/backtest/metrics.py +74 -0
  5. deltafq/backtest/performance.py +350 -0
  6. deltafq/charts/__init__.py +14 -0
  7. deltafq/charts/performance.py +319 -0
  8. deltafq/charts/price.py +64 -0
  9. deltafq/charts/signals.py +181 -0
  10. deltafq/core/__init__.py +18 -0
  11. deltafq/core/base.py +21 -0
  12. deltafq/core/config.py +62 -0
  13. deltafq/core/exceptions.py +34 -0
  14. deltafq/core/logger.py +44 -0
  15. deltafq/data/__init__.py +16 -0
  16. deltafq/data/cleaner.py +39 -0
  17. deltafq/data/fetcher.py +58 -0
  18. deltafq/data/storage.py +264 -0
  19. deltafq/data/validator.py +51 -0
  20. deltafq/indicators/__init__.py +14 -0
  21. deltafq/indicators/fundamental.py +28 -0
  22. deltafq/indicators/talib_indicators.py +67 -0
  23. deltafq/indicators/technical.py +251 -0
  24. deltafq/live/__init__.py +16 -0
  25. deltafq/live/connection.py +235 -0
  26. deltafq/live/data_feed.py +158 -0
  27. deltafq/live/monitoring.py +191 -0
  28. deltafq/live/risk_control.py +192 -0
  29. deltafq/strategy/__init__.py +12 -0
  30. deltafq/strategy/base.py +34 -0
  31. deltafq/strategy/signals.py +193 -0
  32. deltafq/trader/__init__.py +16 -0
  33. deltafq/trader/broker.py +119 -0
  34. deltafq/trader/engine.py +174 -0
  35. deltafq/trader/order_manager.py +110 -0
  36. deltafq/trader/position_manager.py +92 -0
  37. deltafq-0.4.0.dist-info/METADATA +115 -0
  38. deltafq-0.4.0.dist-info/RECORD +42 -0
  39. deltafq-0.4.0.dist-info/WHEEL +5 -0
  40. deltafq-0.4.0.dist-info/entry_points.txt +2 -0
  41. deltafq-0.4.0.dist-info/licenses/LICENSE +21 -0
  42. deltafq-0.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,251 @@
1
+ """
2
+ Technical 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 TechnicalIndicators(BaseComponent):
11
+ """Basic technical indicators."""
12
+
13
+ def initialize(self) -> bool:
14
+ """Initialize technical indicators."""
15
+ self.logger.info("Initializing technical indicators")
16
+ return True
17
+
18
+ def sma(self, data: pd.Series, period: int) -> pd.Series:
19
+ """Calculate Simple Moving Average (SMA)."""
20
+ self.logger.info(f"Calculating SMA(period={period})")
21
+ return data.rolling(window=period).mean()
22
+
23
+ def ema(self, data: pd.Series, period: int, method: str = 'pandas') -> pd.Series:
24
+ """
25
+ Calculate Exponential Moving Average (EMA).
26
+ Args:
27
+ data: Price series
28
+ period: Period for EMA calculation
29
+ method: 'pandas' uses pandas ewm (default), 'talib' uses inline calculation matching TA-Lib.
30
+ Talib method is more precise but slightly slower.
31
+ """
32
+ self.logger.info(f"Calculating EMA(period={period}, method={method})")
33
+
34
+ if method == 'talib':
35
+ # TA-Lib compatible: seed with SMA of the first full window
36
+ ema = pd.Series(index=data.index, dtype=float)
37
+ if len(data) == 0:
38
+ return ema
39
+
40
+ alpha = 2.0 / (period + 1.0)
41
+ first_valid_idx = data.first_valid_index()
42
+ if first_valid_idx is None:
43
+ return ema
44
+
45
+ first_valid_pos = data.index.get_loc(first_valid_idx)
46
+ # Require a full window for initial SMA
47
+ start = first_valid_pos
48
+ end = start + period # exclusive
49
+ if end > len(data):
50
+ # Not enough data to seed
51
+ return ema
52
+
53
+ # Attempt to find a clean initial window without NaNs
54
+ seed_pos = end - 1
55
+ initial_window = data.iloc[start:end]
56
+ if initial_window.isna().any():
57
+ clean_found = False
58
+ for shift in range(0, len(data) - start - period + 1):
59
+ window = data.iloc[start + shift:start + shift + period]
60
+ if not window.isna().any():
61
+ seed_pos = start + shift + period - 1
62
+ initial_window = window
63
+ clean_found = True
64
+ break
65
+ if not clean_found:
66
+ return ema
67
+
68
+ ema.iloc[seed_pos] = float(initial_window.mean())
69
+
70
+ # Forward recursion from the next bar after seed
71
+ for i in range(seed_pos + 1, len(data)):
72
+ x = data.iloc[i]
73
+ prev = ema.iloc[i - 1]
74
+ if pd.isna(x):
75
+ # Output NaN for this bar but keep previous state for the next step
76
+ ema.iloc[i] = np.nan
77
+ else:
78
+ # Use last non-NaN ema value as previous
79
+ j = i - 1
80
+ while j >= 0 and pd.isna(prev):
81
+ j -= 1
82
+ prev = ema.iloc[j] if j >= 0 else np.nan
83
+ if pd.isna(prev):
84
+ ema.iloc[i] = float(x)
85
+ else:
86
+ ema.iloc[i] = float(alpha * x + (1.0 - alpha) * prev)
87
+ return ema
88
+ else:
89
+ # Default: pandas ewm
90
+ return data.ewm(span=period, adjust=False).mean()
91
+
92
+ def rsi(self, data: pd.Series, period: int = 14, method: str = 'sma') -> pd.Series:
93
+ """
94
+ Calculate Relative Strength Index (RSI).
95
+ Args:
96
+ data: Price series
97
+ period: Period for RSI calculation. Default is 14.
98
+ method: 'sma' uses SMA for smoothing (default), 'rma' uses RMA (Wilder's Smoothing) matching TA-Lib.
99
+ RMA gives more weight to historical data, making it less responsive to recent changes.
100
+ """
101
+ self.logger.info(f"Calculating RSI(period={period}, method={method})")
102
+ delta = data.diff()
103
+ gains = delta.where(delta > 0, 0)
104
+ losses = -delta.where(delta < 0, 0)
105
+
106
+ if method == 'rma':
107
+ # TA-Lib compatible: RMA (Wilder's Smoothing)
108
+ avg_gain = pd.Series(index=data.index, dtype=float)
109
+ avg_loss = pd.Series(index=data.index, dtype=float)
110
+
111
+ if len(data) >= period + 1:
112
+ avg_gain.iloc[period] = gains.iloc[1:period+1].mean()
113
+ avg_loss.iloc[period] = losses.iloc[1:period+1].mean()
114
+
115
+ for i in range(period + 1, len(data)):
116
+ if pd.notna(avg_gain.iloc[i - 1]) and pd.notna(gains.iloc[i]):
117
+ avg_gain.iloc[i] = (avg_gain.iloc[i - 1] * (period - 1) + gains.iloc[i]) / period
118
+ else:
119
+ avg_gain.iloc[i] = np.nan
120
+
121
+ if pd.notna(avg_loss.iloc[i - 1]) and pd.notna(losses.iloc[i]):
122
+ avg_loss.iloc[i] = (avg_loss.iloc[i - 1] * (period - 1) + losses.iloc[i]) / period
123
+ else:
124
+ avg_loss.iloc[i] = np.nan
125
+
126
+ rs = avg_gain / avg_loss
127
+ else:
128
+ # Default: SMA
129
+ avg_gain = gains.rolling(window=period).mean()
130
+ avg_loss = losses.rolling(window=period).mean()
131
+ rs = avg_gain / avg_loss
132
+
133
+ return 100 - (100 / (1 + rs))
134
+
135
+ def kdj(self, high: pd.Series, low: pd.Series, close: pd.Series,
136
+ n: int = 9, m1: int = 3, m2: int = 3, method: str = 'ema') -> pd.DataFrame:
137
+ """
138
+ Calculate KDJ indicator (Stochastic Oscillator).
139
+ Args:
140
+ high: High price series
141
+ low: Low price series
142
+ close: Close price series
143
+ n: Period for RSV calculation. Default is 9.
144
+ m1: Period for K line smoothing. Default is 3.
145
+ m2: Period for D line smoothing. Default is 3.
146
+ method: 'ema' uses EMA for smoothing (default, more responsive),
147
+ 'sma' uses SMA for smoothing matching TA-Lib STOCH.
148
+ """
149
+ self.logger.info(f"Calculating KDJ(n={n}, m1={m1}, m2={m2}, method={method})")
150
+
151
+ # Calculate RSV (Raw Stochastic Value)
152
+ lowest_low = low.rolling(window=n).min()
153
+ highest_high = high.rolling(window=n).max()
154
+ rsv = 100 * (close - lowest_low) / (highest_high - lowest_low)
155
+ rsv = rsv.fillna(50) # Fill NaN with neutral value
156
+
157
+ if method == 'sma':
158
+ # TA-Lib compatible: SMA smoothing
159
+ k = self.sma(rsv, m1)
160
+ d = self.sma(k, m2)
161
+ else:
162
+ # Default: EMA smoothing (more responsive)
163
+ k = self.ema(rsv, m1)
164
+ d = self.ema(k, m2)
165
+
166
+ j = 3 * k - 2 * d
167
+
168
+ return pd.DataFrame({
169
+ 'k': k,
170
+ 'd': d,
171
+ 'j': j
172
+ })
173
+
174
+ def boll(self, data: pd.Series, period: int = 20, std_dev: float = 2, method: str = 'sample') -> pd.DataFrame:
175
+ """
176
+ Calculate Bollinger Bands.
177
+ Args:
178
+ data: Price series
179
+ period: Period for Bollinger Bands calculation. Default is 20.
180
+ std_dev: Number of standard deviations. Default is 2.
181
+ method: 'sample' uses sample std (ddof=1, default), 'population' uses population std (ddof=0) matching TA-Lib.
182
+ Population std is slightly smaller, making bands tighter. TA-Lib BBANDS uses population std.
183
+ """
184
+ self.logger.info(f"Calculating BOLL(period={period}, std_dev={std_dev}, method={method})")
185
+ sma = self.sma(data, period)
186
+ ddof = 0 if method == 'population' else 1
187
+ std = data.rolling(window=period).std(ddof=ddof)
188
+
189
+ return pd.DataFrame({
190
+ 'upper': sma + (std * std_dev),
191
+ 'middle': sma,
192
+ 'lower': sma - (std * std_dev)
193
+ })
194
+
195
+ def atr(self, high: pd.Series, low: pd.Series, close: pd.Series, period: int = 14, method: str = 'sma') -> pd.Series:
196
+ """
197
+ Calculate Average True Range (ATR).
198
+ Args:
199
+ high: High price series
200
+ low: Low price series
201
+ close: Close price series
202
+ period: Period for ATR calculation. Default is 14.
203
+ method: 'sma' uses SMA for smoothing (default), 'rma' uses RMA (Wilder's Smoothing) matching TA-Lib.
204
+ RMA gives more weight to historical data, making it less responsive to recent changes.
205
+ """
206
+ self.logger.info(f"Calculating ATR(period={period}, method={method})")
207
+
208
+ # Calculate True Range
209
+ tr1 = high - low
210
+ tr2 = abs(high - close.shift(1))
211
+ tr3 = abs(low - close.shift(1))
212
+ tr = pd.concat([tr1, tr2, tr3], axis=1).max(axis=1)
213
+
214
+ if method == 'rma':
215
+ # TA-Lib compatible: RMA (Wilder's Smoothing)
216
+ atr = pd.Series(index=tr.index, dtype=float)
217
+ if len(tr) >= period + 1:
218
+ atr.iloc[period] = tr.iloc[1:period+1].mean()
219
+ for i in range(period + 1, len(tr)):
220
+ if pd.notna(atr.iloc[i - 1]) and pd.notna(tr.iloc[i]):
221
+ atr.iloc[i] = (atr.iloc[i - 1] * (period - 1) + tr.iloc[i]) / period
222
+ else:
223
+ atr.iloc[i] = np.nan
224
+ return atr
225
+ else:
226
+ # Default: SMA
227
+ return tr.rolling(window=period).mean()
228
+
229
+ def obv(self, close: pd.Series, volume: pd.Series) -> pd.Series:
230
+ """
231
+ Calculate On-Balance Volume (OBV).
232
+ Args:
233
+ close: Close price series
234
+ volume: Volume series
235
+ """
236
+ self.logger.info("Calculating OBV")
237
+
238
+ # Calculate price change direction
239
+ price_change = close.diff()
240
+
241
+ # Calculate signed volume based on price direction
242
+ signed_volume = volume.copy()
243
+ signed_volume[price_change > 0] = volume[price_change > 0] # Add volume when price up
244
+ signed_volume[price_change < 0] = -volume[price_change < 0] # Subtract volume when price down
245
+ signed_volume[price_change == 0] = 0 # No change when price unchanged
246
+ signed_volume.iloc[0] = volume.iloc[0] # First value is first volume
247
+
248
+ # Calculate cumulative OBV
249
+ obv = signed_volume.cumsum()
250
+
251
+ return obv
@@ -0,0 +1,16 @@
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
+
@@ -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,158 @@
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
+