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,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
|
+
|
deltafq/strategy/base.py
ADDED
|
@@ -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
|
+
|