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,191 @@
1
+ """
2
+ Trading monitoring and alerts for live trading.
3
+ """
4
+
5
+ import pandas as pd
6
+ from typing import Dict, List, Any, Optional, Callable
7
+ from datetime import datetime, timedelta
8
+ from ..core.base import BaseComponent
9
+
10
+
11
+ class TradingMonitor(BaseComponent):
12
+ """Monitor trading activities and generate alerts."""
13
+
14
+ def __init__(self, **kwargs):
15
+ """Initialize trading monitor."""
16
+ super().__init__(**kwargs)
17
+ self.alerts = []
18
+ self.monitoring_rules = {}
19
+ self.alert_callbacks = []
20
+ self.monitoring_active = False
21
+
22
+ def initialize(self) -> bool:
23
+ """Initialize trading monitor."""
24
+ self.logger.info("Initializing trading monitor")
25
+ return True
26
+
27
+ def add_monitoring_rule(self, rule_name: str, rule_func: Callable,
28
+ threshold: float = None, **kwargs) -> bool:
29
+ """Add a monitoring rule."""
30
+ try:
31
+ self.monitoring_rules[rule_name] = {
32
+ 'function': rule_func,
33
+ 'threshold': threshold,
34
+ 'parameters': kwargs,
35
+ 'active': True,
36
+ 'last_triggered': None
37
+ }
38
+
39
+ self.logger.info(f"Added monitoring rule: {rule_name}")
40
+ return True
41
+
42
+ except Exception as e:
43
+ self.logger.error(f"Failed to add monitoring rule: {str(e)}")
44
+ return False
45
+
46
+ def add_alert_callback(self, callback: Callable) -> bool:
47
+ """Add alert callback function."""
48
+ self.alert_callbacks.append(callback)
49
+ return True
50
+
51
+ def start_monitoring(self) -> bool:
52
+ """Start monitoring."""
53
+ try:
54
+ self.monitoring_active = True
55
+ self.logger.info("Trading monitoring started")
56
+
57
+ # In a real implementation, this would start a monitoring loop
58
+ # For now, we'll provide the framework
59
+
60
+ return True
61
+
62
+ except Exception as e:
63
+ self.monitoring_active = False
64
+ self.logger.error(f"Failed to start monitoring: {str(e)}")
65
+ return False
66
+
67
+ def stop_monitoring(self) -> bool:
68
+ """Stop monitoring."""
69
+ try:
70
+ self.monitoring_active = False
71
+ self.logger.info("Trading monitoring stopped")
72
+ return True
73
+
74
+ except Exception as e:
75
+ self.logger.error(f"Failed to stop monitoring: {str(e)}")
76
+ return False
77
+
78
+ def check_rule(self, rule_name: str, data: Dict[str, Any]) -> bool:
79
+ """Check a specific monitoring rule."""
80
+ if rule_name not in self.monitoring_rules:
81
+ return False
82
+
83
+ rule = self.monitoring_rules[rule_name]
84
+ if not rule['active']:
85
+ return False
86
+
87
+ try:
88
+ # Execute the rule function
89
+ result = rule['function'](data, rule['threshold'], **rule['parameters'])
90
+
91
+ # Check if rule was triggered
92
+ if result:
93
+ self._trigger_alert(rule_name, data, result)
94
+ rule['last_triggered'] = datetime.now()
95
+ return True
96
+
97
+ return False
98
+
99
+ except Exception as e:
100
+ self.logger.error(f"Rule check failed for {rule_name}: {str(e)}")
101
+ return False
102
+
103
+ def check_all_rules(self, data: Dict[str, Any]) -> Dict[str, bool]:
104
+ """Check all active monitoring rules."""
105
+ results = {}
106
+
107
+ for rule_name in self.monitoring_rules:
108
+ if self.monitoring_rules[rule_name]['active']:
109
+ results[rule_name] = self.check_rule(rule_name, data)
110
+
111
+ return results
112
+
113
+ def _trigger_alert(self, rule_name: str, data: Dict[str, Any], result: Any):
114
+ """Trigger an alert."""
115
+ alert = {
116
+ 'rule_name': rule_name,
117
+ 'timestamp': datetime.now(),
118
+ 'data': data,
119
+ 'result': result,
120
+ 'severity': self._determine_severity(rule_name, result)
121
+ }
122
+
123
+ self.alerts.append(alert)
124
+ self.logger.warning(f"Alert triggered: {rule_name}")
125
+
126
+ # Notify alert callbacks
127
+ for callback in self.alert_callbacks:
128
+ try:
129
+ callback(alert)
130
+ except Exception as e:
131
+ self.logger.error(f"Alert callback failed: {str(e)}")
132
+
133
+ def _determine_severity(self, rule_name: str, result: Any) -> str:
134
+ """Determine alert severity."""
135
+ # Simple severity determination based on rule name
136
+ if 'loss' in rule_name.lower() or 'drawdown' in rule_name.lower():
137
+ return 'HIGH'
138
+ elif 'position' in rule_name.lower() or 'concentration' in rule_name.lower():
139
+ return 'MEDIUM'
140
+ else:
141
+ return 'LOW'
142
+
143
+ def get_alerts(self, hours: int = 24) -> List[Dict[str, Any]]:
144
+ """Get alerts from the last N hours."""
145
+ cutoff_time = datetime.now() - timedelta(hours=hours)
146
+ return [alert for alert in self.alerts if alert['timestamp'] > cutoff_time]
147
+
148
+ def clear_alerts(self, hours: int = None) -> int:
149
+ """Clear old alerts."""
150
+ if hours is None:
151
+ # Clear all alerts
152
+ count = len(self.alerts)
153
+ self.alerts.clear()
154
+ else:
155
+ # Clear alerts older than specified hours
156
+ cutoff_time = datetime.now() - timedelta(hours=hours)
157
+ original_count = len(self.alerts)
158
+ self.alerts = [alert for alert in self.alerts if alert['timestamp'] > cutoff_time]
159
+ count = original_count - len(self.alerts)
160
+
161
+ self.logger.info(f"Cleared {count} alerts")
162
+ return count
163
+
164
+ def enable_rule(self, rule_name: str) -> bool:
165
+ """Enable a monitoring rule."""
166
+ if rule_name in self.monitoring_rules:
167
+ self.monitoring_rules[rule_name]['active'] = True
168
+ return True
169
+ return False
170
+
171
+ def disable_rule(self, rule_name: str) -> bool:
172
+ """Disable a monitoring rule."""
173
+ if rule_name in self.monitoring_rules:
174
+ self.monitoring_rules[rule_name]['active'] = False
175
+ return True
176
+ return False
177
+
178
+ def get_monitoring_status(self) -> Dict[str, Any]:
179
+ """Get monitoring status."""
180
+ return {
181
+ 'monitoring_active': self.monitoring_active,
182
+ 'total_rules': len(self.monitoring_rules),
183
+ 'active_rules': sum(1 for rule in self.monitoring_rules.values() if rule['active']),
184
+ 'total_alerts': len(self.alerts),
185
+ 'recent_alerts': len(self.get_alerts(hours=1)),
186
+ 'rules': {name: {
187
+ 'active': rule['active'],
188
+ 'last_triggered': rule['last_triggered']
189
+ } for name, rule in self.monitoring_rules.items()}
190
+ }
191
+
@@ -0,0 +1,192 @@
1
+ """
2
+ Real-time risk control 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 LiveRiskControl(BaseComponent):
12
+ """Real-time risk control system."""
13
+
14
+ def __init__(self, max_position_size: float = 0.1, max_daily_loss: float = 0.05,
15
+ max_drawdown: float = 0.15, **kwargs):
16
+ """Initialize risk control."""
17
+ super().__init__(**kwargs)
18
+ self.max_position_size = max_position_size
19
+ self.max_daily_loss = max_daily_loss
20
+ self.max_drawdown = max_drawdown
21
+ self.daily_pnl = 0.0
22
+ self.peak_equity = 0.0
23
+ self.risk_limits = {}
24
+ self.alert_callbacks = []
25
+
26
+ def initialize(self) -> bool:
27
+ """Initialize risk control."""
28
+ self.logger.info("Initializing live risk control")
29
+ return True
30
+
31
+ def add_alert_callback(self, callback) -> bool:
32
+ """Add alert callback function."""
33
+ self.alert_callbacks.append(callback)
34
+ return True
35
+
36
+ def check_position_risk(self, symbol: str, quantity: float, portfolio_value: float,
37
+ current_price: float) -> bool:
38
+ """Check if position size is within risk limits."""
39
+ try:
40
+ position_value = abs(quantity) * current_price
41
+ position_ratio = position_value / portfolio_value
42
+
43
+ # Check maximum position size
44
+ if position_ratio > self.max_position_size:
45
+ self._trigger_alert("POSITION_SIZE_EXCEEDED", {
46
+ 'symbol': symbol,
47
+ 'position_ratio': position_ratio,
48
+ 'max_allowed': self.max_position_size
49
+ })
50
+ return False
51
+
52
+ # Check symbol-specific limits
53
+ if symbol in self.risk_limits:
54
+ symbol_limit = self.risk_limits[symbol]
55
+ if position_ratio > symbol_limit:
56
+ self._trigger_alert("SYMBOL_LIMIT_EXCEEDED", {
57
+ 'symbol': symbol,
58
+ 'position_ratio': position_ratio,
59
+ 'symbol_limit': symbol_limit
60
+ })
61
+ return False
62
+
63
+ return True
64
+
65
+ except Exception as e:
66
+ self.logger.error(f"Risk check failed: {str(e)}")
67
+ return False
68
+
69
+ def check_daily_loss_limit(self, current_equity: float, initial_equity: float) -> bool:
70
+ """Check if daily loss limit is exceeded."""
71
+ try:
72
+ daily_pnl = current_equity - initial_equity
73
+ daily_pnl_ratio = daily_pnl / initial_equity
74
+
75
+ if daily_pnl_ratio < -self.max_daily_loss:
76
+ self._trigger_alert("DAILY_LOSS_LIMIT_EXCEEDED", {
77
+ 'daily_pnl': daily_pnl,
78
+ 'daily_pnl_ratio': daily_pnl_ratio,
79
+ 'max_daily_loss': self.max_daily_loss
80
+ })
81
+ return False
82
+
83
+ return True
84
+
85
+ except Exception as e:
86
+ self.logger.error(f"Daily loss check failed: {str(e)}")
87
+ return False
88
+
89
+ def check_drawdown_limit(self, current_equity: float) -> bool:
90
+ """Check if maximum drawdown is exceeded."""
91
+ try:
92
+ if current_equity > self.peak_equity:
93
+ self.peak_equity = current_equity
94
+
95
+ if self.peak_equity > 0:
96
+ drawdown = (self.peak_equity - current_equity) / self.peak_equity
97
+
98
+ if drawdown > self.max_drawdown:
99
+ self._trigger_alert("DRAWDOWN_LIMIT_EXCEEDED", {
100
+ 'current_drawdown': drawdown,
101
+ 'max_drawdown': self.max_drawdown,
102
+ 'peak_equity': self.peak_equity,
103
+ 'current_equity': current_equity
104
+ })
105
+ return False
106
+
107
+ return True
108
+
109
+ except Exception as e:
110
+ self.logger.error(f"Drawdown check failed: {str(e)}")
111
+ return False
112
+
113
+ def check_concentration_risk(self, positions: Dict[str, float], portfolio_value: float) -> bool:
114
+ """Check portfolio concentration risk."""
115
+ try:
116
+ total_position_value = sum(abs(value) for value in positions.values())
117
+
118
+ if total_position_value > portfolio_value * 0.95: # 95% of portfolio
119
+ self._trigger_alert("CONCENTRATION_RISK_HIGH", {
120
+ 'total_position_value': total_position_value,
121
+ 'portfolio_value': portfolio_value,
122
+ 'concentration_ratio': total_position_value / portfolio_value
123
+ })
124
+ return False
125
+
126
+ return True
127
+
128
+ except Exception as e:
129
+ self.logger.error(f"Concentration risk check failed: {str(e)}")
130
+ return False
131
+
132
+ def set_symbol_limit(self, symbol: str, limit: float) -> bool:
133
+ """Set position size limit for specific symbol."""
134
+ try:
135
+ self.risk_limits[symbol] = limit
136
+ self.logger.info(f"Set risk limit for {symbol}: {limit:.2%}")
137
+ return True
138
+
139
+ except Exception as e:
140
+ self.logger.error(f"Failed to set symbol limit: {str(e)}")
141
+ return False
142
+
143
+ def comprehensive_risk_check(self, symbol: str, quantity: float, portfolio_value: float,
144
+ current_price: float, current_equity: float,
145
+ initial_equity: float, positions: Dict[str, float]) -> Dict[str, bool]:
146
+ """Perform comprehensive risk checks."""
147
+ results = {
148
+ 'position_risk': self.check_position_risk(symbol, quantity, portfolio_value, current_price),
149
+ 'daily_loss': self.check_daily_loss_limit(current_equity, initial_equity),
150
+ 'drawdown': self.check_drawdown_limit(current_equity),
151
+ 'concentration': self.check_concentration_risk(positions, portfolio_value)
152
+ }
153
+
154
+ # Overall risk check passes only if all individual checks pass
155
+ results['overall'] = all(results.values())
156
+
157
+ return results
158
+
159
+ def _trigger_alert(self, alert_type: str, details: Dict[str, Any]):
160
+ """Trigger risk alert."""
161
+ alert = {
162
+ 'type': alert_type,
163
+ 'timestamp': datetime.now(),
164
+ 'details': details
165
+ }
166
+
167
+ self.logger.warning(f"Risk alert: {alert_type}")
168
+
169
+ # Notify alert callbacks
170
+ for callback in self.alert_callbacks:
171
+ try:
172
+ callback(alert)
173
+ except Exception as e:
174
+ self.logger.error(f"Alert callback failed: {str(e)}")
175
+
176
+ def reset_daily_limits(self):
177
+ """Reset daily risk limits (call at start of trading day)."""
178
+ self.daily_pnl = 0.0
179
+ self.logger.info("Daily risk limits reset")
180
+
181
+ def get_risk_status(self) -> Dict[str, Any]:
182
+ """Get current risk status."""
183
+ return {
184
+ 'max_position_size': self.max_position_size,
185
+ 'max_daily_loss': self.max_daily_loss,
186
+ 'max_drawdown': self.max_drawdown,
187
+ 'current_daily_pnl': self.daily_pnl,
188
+ 'peak_equity': self.peak_equity,
189
+ 'symbol_limits': dict(self.risk_limits),
190
+ 'active_alerts': len(self.alert_callbacks)
191
+ }
192
+
@@ -0,0 +1,12 @@
1
+ """
2
+ Strategy module for DeltaFQ.
3
+ """
4
+
5
+ from .base import BaseStrategy
6
+ from .signals import SignalGenerator
7
+
8
+ __all__ = [
9
+ "BaseStrategy",
10
+ "SignalGenerator"
11
+ ]
12
+
@@ -0,0 +1,34 @@
1
+ """Common utilities for simple trading strategies."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Dict
5
+
6
+ import pandas as pd
7
+
8
+ from ..core.base import BaseComponent
9
+ from ..core.exceptions import StrategyError
10
+
11
+
12
+ class BaseStrategy(BaseComponent, ABC):
13
+ """Minimal base class: fetch signals from `generate_signals` and return them."""
14
+
15
+ def __init__(self, name: str | None = None, **kwargs: Any) -> None:
16
+ super().__init__(name=name, **kwargs)
17
+ self.signals: pd.DataFrame = pd.DataFrame()
18
+
19
+ def initialize(self) -> bool:
20
+ self.logger.info("Initializing strategy: %s", self.name)
21
+ return True
22
+
23
+ @abstractmethod
24
+ def generate_signals(self, data: pd.DataFrame) -> pd.DataFrame:
25
+ """Return a `DataFrame` containing strategy signals."""
26
+
27
+ def run(self, data: pd.DataFrame) -> Dict[str, Any]:
28
+ self.logger.info("Running strategy: %s", self.name)
29
+ try:
30
+ self.signals = self.generate_signals(data)
31
+ return {"strategy_name": self.name, "signals": self.signals}
32
+ except Exception as exc: # pragma: no cover - thin wrapper
33
+ raise StrategyError(f"Strategy execution failed: {exc}") from exc
34
+
@@ -0,0 +1,193 @@
1
+ """Essential signal generation and combination utilities."""
2
+
3
+ import pandas as pd
4
+ import numpy as np
5
+ from typing import Dict, Any, Optional
6
+ from ..core.base import BaseComponent
7
+
8
+
9
+ class SignalGenerator(BaseComponent):
10
+ """Generate trading signals from precomputed indicators and combine them."""
11
+
12
+ def initialize(self) -> bool:
13
+ """Initialize signal generator."""
14
+ self.logger.info("Initializing signal generator")
15
+ return True
16
+
17
+ def _log_signal_counts(self, label: str, series: pd.Series) -> None:
18
+ """Log the number of buy, sell, and flat signals."""
19
+ buy = int((series == 1).sum())
20
+ sell = int((series == -1).sum())
21
+ flat = int((series == 0).sum())
22
+ self.logger.info(f"{label} signals -> buy={buy}, sell={sell}, flat={flat}")
23
+
24
+ # --- SMA -----------------------------------------------------------------
25
+ def sma_signals(self, fast_ma: pd.Series, slow_ma: pd.Series) -> pd.Series:
26
+ """Bullish when the fast MA is above the slow MA, bearish when below."""
27
+ if not fast_ma.index.equals(slow_ma.index):
28
+ slow_ma = slow_ma.reindex(fast_ma.index)
29
+ signals = pd.Series(
30
+ np.where(fast_ma > slow_ma, 1, np.where(fast_ma < slow_ma, -1, 0)),
31
+ index=fast_ma.index,
32
+ dtype=int,
33
+ )
34
+ self._log_signal_counts("SMA crossover", signals)
35
+ return signals
36
+
37
+ def ma_signals(self, fast_ma: pd.Series, slow_ma: pd.Series) -> pd.Series:
38
+ """Alias for backwards compatibility."""
39
+ return self.sma_signals(fast_ma, slow_ma)
40
+
41
+ # --- EMA -----------------------------------------------------------------
42
+ def ema_signals(self, price: pd.Series, ema: pd.Series) -> pd.Series:
43
+ """Bullish when price sits above the EMA, bearish when it falls below."""
44
+ if not price.index.equals(ema.index):
45
+ ema = ema.reindex(price.index)
46
+ signals = pd.Series(
47
+ np.where(price > ema, 1, np.where(price < ema, -1, 0)),
48
+ index=price.index,
49
+ dtype=int,
50
+ )
51
+ self._log_signal_counts("EMA price-vs-ema", signals)
52
+ return signals
53
+
54
+ # --- RSI -----------------------------------------------------------------
55
+ def rsi_signals(self, rsi: pd.Series, oversold: float = 30, overbought: float = 70) -> pd.Series:
56
+ """Buy when RSI drops beneath the oversold band, sell when above overbought."""
57
+ signals = pd.Series(
58
+ np.where(rsi < oversold, 1, np.where(rsi > overbought, -1, 0)),
59
+ index=rsi.index,
60
+ dtype=int,
61
+ )
62
+ self._log_signal_counts("RSI", signals)
63
+ return signals
64
+
65
+ # --- KDJ -----------------------------------------------------------------
66
+ def kdj_signals(self, kdj: pd.DataFrame) -> pd.Series:
67
+ """Bullish on K crossing above D, bearish on K crossing beneath D."""
68
+ for col in ("k", "d"):
69
+ if col not in kdj:
70
+ raise ValueError("kdj must contain 'k' and 'd' columns")
71
+ signals = pd.Series(
72
+ np.where(kdj["k"] > kdj["d"], 1, np.where(kdj["k"] < kdj["d"], -1, 0)),
73
+ index=kdj.index,
74
+ dtype=int,
75
+ )
76
+ self._log_signal_counts("KDJ K>D", signals)
77
+ return signals
78
+
79
+ # --- BOLL ----------------------------------------------------------------
80
+ def boll_signals(self, price: pd.Series, bands: pd.DataFrame, method: str = "cross") -> pd.Series:
81
+ """Bollinger logic: touch or cross of the outer bands triggers entries."""
82
+ if method not in ["touch", "cross", "cross_current"]:
83
+ raise ValueError("Invalid method")
84
+ if not all(col in bands for col in ("upper", "middle", "lower")):
85
+ raise ValueError("bands missing required columns")
86
+
87
+ signals = pd.Series(0, index=price.index, dtype=int)
88
+
89
+ if method == "touch":
90
+ buy_condition = price <= bands["lower"]
91
+ sell_condition = price >= bands["upper"]
92
+ signals = np.where(buy_condition, 1, np.where(sell_condition, -1, 0))
93
+
94
+ elif method == "cross":
95
+ prev_price = price.shift(1)
96
+ prev_bands = bands.shift(1)
97
+ buy_condition = (prev_price <= prev_bands["lower"]) & (price >= bands["lower"])
98
+ sell_condition = (prev_price >= prev_bands["upper"]) & (price <= bands["upper"])
99
+ signals = np.where(buy_condition, 1, np.where(sell_condition, -1, 0))
100
+
101
+ elif method == "cross_current":
102
+ prev_price = price.shift(1)
103
+ buy_condition = (prev_price <= bands["lower"]) & (price >= bands["lower"])
104
+ sell_condition = (prev_price >= bands["upper"]) & (price <= bands["upper"])
105
+ signals = np.where(buy_condition, 1, np.where(sell_condition, -1, 0))
106
+
107
+ series = pd.Series(signals, index=price.index, dtype=int)
108
+ self._log_signal_counts(f"Boll ({method})", series)
109
+ return series
110
+
111
+ # --- OBV -----------------------------------------------------------------
112
+ def obv_signals(self, obv: pd.Series) -> pd.Series:
113
+ """Positive OBV slope hints at buying pressure; negative slope at selling."""
114
+ obv_change = obv.diff().fillna(0)
115
+ signals = pd.Series(
116
+ np.where(obv_change > 0, 1, np.where(obv_change < 0, -1, 0)),
117
+ index=obv.index,
118
+ dtype=int,
119
+ )
120
+ self._log_signal_counts("OBV slope", signals)
121
+ return signals
122
+
123
+ def combine_signals(
124
+ self,
125
+ signals_dict: Dict[str, pd.Series],
126
+ method: str = 'vote',
127
+ weights: Optional[Dict[str, float]] = None,
128
+ threshold: float = 0.5
129
+ ) -> pd.Series:
130
+ """Combine multiple {-1,0,1} Series using 'vote' | 'weighted' | 'threshold'."""
131
+ if not signals_dict:
132
+ raise ValueError("signals_dict cannot be empty")
133
+
134
+ signal_names = list(signals_dict.keys())
135
+ first_signal = signals_dict[signal_names[0]]
136
+ index = first_signal.index
137
+
138
+ for name, signal in signals_dict.items():
139
+ if len(signal) != len(first_signal):
140
+ raise ValueError(f"Signal '{name}' has different length")
141
+ if not signal.index.equals(index):
142
+ signals_dict[name] = signal.reindex(index)
143
+ self.logger.info(f"Aligned signal '{name}' index")
144
+
145
+ signals_df = pd.DataFrame(signals_dict)
146
+
147
+ if method == 'vote':
148
+ buy_votes = (signals_df == 1).sum(axis=1)
149
+ sell_votes = (signals_df == -1).sum(axis=1)
150
+ combined = pd.Series(0, index=index, dtype=int)
151
+ combined = np.where(buy_votes > sell_votes, 1, combined)
152
+ combined = np.where(sell_votes > buy_votes, -1, combined)
153
+
154
+ elif method == 'weighted':
155
+ if weights is None:
156
+ weights = {name: 1.0 / len(signals_dict) for name in signal_names}
157
+ else:
158
+ total_weight = sum(weights.values())
159
+ if total_weight == 0:
160
+ raise ValueError("Total weight cannot be zero")
161
+ weights = {k: v / total_weight for k, v in weights.items()}
162
+
163
+ weighted_sum = pd.Series(0.0, index=index)
164
+ for name in signal_names:
165
+ weighted_sum += signals_df[name] * weights.get(name, 0)
166
+
167
+ combined = pd.Series(0, index=index, dtype=int)
168
+ combined = np.where(weighted_sum > 0.33, 1, combined)
169
+ combined = np.where(weighted_sum < -0.33, -1, combined)
170
+
171
+ elif method == 'threshold':
172
+ if weights is None:
173
+ weights = {name: 1.0 / len(signals_dict) for name in signal_names}
174
+ else:
175
+ total_weight = sum(weights.values())
176
+ if total_weight == 0:
177
+ raise ValueError("Total weight cannot be zero")
178
+ weights = {k: v / total_weight for k, v in weights.items()}
179
+
180
+ weighted_sum = pd.Series(0.0, index=index)
181
+ for name in signal_names:
182
+ weighted_sum += signals_df[name] * weights.get(name, 0)
183
+
184
+ combined = pd.Series(0, index=index, dtype=int)
185
+ combined = np.where(weighted_sum >= threshold, 1, combined)
186
+ combined = np.where(weighted_sum <= -threshold, -1, combined)
187
+
188
+ else:
189
+ raise ValueError("Invalid method")
190
+
191
+ combined_series = pd.Series(combined, index=index, dtype=int)
192
+ self._log_signal_counts(f"Combined ({method})", combined_series)
193
+ return combined_series
@@ -0,0 +1,16 @@
1
+ """
2
+ Trader module for DeltaFQ.
3
+ """
4
+
5
+ from .broker import Broker
6
+ from .order_manager import OrderManager
7
+ from .position_manager import PositionManager
8
+ from .engine import ExecutionEngine
9
+
10
+ __all__ = [
11
+ "Broker",
12
+ "OrderManager",
13
+ "PositionManager",
14
+ "ExecutionEngine"
15
+ ]
16
+