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.
- deltafq/__init__.py +29 -0
- deltafq/backtest/__init__.py +32 -0
- deltafq/backtest/engine.py +145 -0
- deltafq/backtest/metrics.py +74 -0
- deltafq/backtest/performance.py +350 -0
- deltafq/charts/__init__.py +14 -0
- deltafq/charts/performance.py +319 -0
- deltafq/charts/price.py +64 -0
- deltafq/charts/signals.py +181 -0
- deltafq/core/__init__.py +18 -0
- deltafq/core/base.py +21 -0
- deltafq/core/config.py +62 -0
- deltafq/core/exceptions.py +34 -0
- deltafq/core/logger.py +44 -0
- deltafq/data/__init__.py +16 -0
- deltafq/data/cleaner.py +39 -0
- deltafq/data/fetcher.py +58 -0
- deltafq/data/storage.py +264 -0
- deltafq/data/validator.py +51 -0
- deltafq/indicators/__init__.py +14 -0
- deltafq/indicators/fundamental.py +28 -0
- deltafq/indicators/talib_indicators.py +67 -0
- deltafq/indicators/technical.py +251 -0
- deltafq/live/__init__.py +16 -0
- deltafq/live/connection.py +235 -0
- deltafq/live/data_feed.py +158 -0
- deltafq/live/monitoring.py +191 -0
- deltafq/live/risk_control.py +192 -0
- deltafq/strategy/__init__.py +12 -0
- deltafq/strategy/base.py +34 -0
- deltafq/strategy/signals.py +193 -0
- deltafq/trader/__init__.py +16 -0
- deltafq/trader/broker.py +119 -0
- deltafq/trader/engine.py +174 -0
- deltafq/trader/order_manager.py +110 -0
- deltafq/trader/position_manager.py +92 -0
- deltafq-0.4.0.dist-info/METADATA +115 -0
- deltafq-0.4.0.dist-info/RECORD +42 -0
- deltafq-0.4.0.dist-info/WHEEL +5 -0
- deltafq-0.4.0.dist-info/entry_points.txt +2 -0
- deltafq-0.4.0.dist-info/licenses/LICENSE +21 -0
- 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
|
deltafq/live/__init__.py
ADDED
|
@@ -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
|
+
|