mcli-framework 7.1.3__py3-none-any.whl → 7.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mcli-framework might be problematic. Click here for more details.

Files changed (114) hide show
  1. mcli/__init__.py +160 -0
  2. mcli/__main__.py +14 -0
  3. mcli/app/__init__.py +23 -0
  4. mcli/app/main.py +10 -0
  5. mcli/app/model/__init__.py +0 -0
  6. mcli/app/video/__init__.py +5 -0
  7. mcli/chat/__init__.py +34 -0
  8. mcli/lib/__init__.py +0 -0
  9. mcli/lib/api/__init__.py +0 -0
  10. mcli/lib/auth/__init__.py +1 -0
  11. mcli/lib/config/__init__.py +1 -0
  12. mcli/lib/custom_commands.py +424 -0
  13. mcli/lib/erd/__init__.py +25 -0
  14. mcli/lib/files/__init__.py +0 -0
  15. mcli/lib/fs/__init__.py +1 -0
  16. mcli/lib/logger/__init__.py +3 -0
  17. mcli/lib/paths.py +12 -0
  18. mcli/lib/performance/__init__.py +17 -0
  19. mcli/lib/pickles/__init__.py +1 -0
  20. mcli/lib/shell/__init__.py +0 -0
  21. mcli/lib/toml/__init__.py +1 -0
  22. mcli/lib/watcher/__init__.py +0 -0
  23. mcli/ml/__init__.py +16 -0
  24. mcli/ml/api/__init__.py +30 -0
  25. mcli/ml/api/routers/__init__.py +27 -0
  26. mcli/ml/api/schemas.py +2 -2
  27. mcli/ml/auth/__init__.py +45 -0
  28. mcli/ml/auth/models.py +2 -2
  29. mcli/ml/backtesting/__init__.py +39 -0
  30. mcli/ml/cli/__init__.py +5 -0
  31. mcli/ml/cli/main.py +1 -1
  32. mcli/ml/config/__init__.py +33 -0
  33. mcli/ml/configs/__init__.py +16 -0
  34. mcli/ml/dashboard/__init__.py +12 -0
  35. mcli/ml/dashboard/app.py +13 -13
  36. mcli/ml/dashboard/app_integrated.py +1309 -148
  37. mcli/ml/dashboard/app_supabase.py +46 -21
  38. mcli/ml/dashboard/app_training.py +14 -14
  39. mcli/ml/dashboard/components/__init__.py +7 -0
  40. mcli/ml/dashboard/components/charts.py +258 -0
  41. mcli/ml/dashboard/components/metrics.py +125 -0
  42. mcli/ml/dashboard/components/tables.py +228 -0
  43. mcli/ml/dashboard/pages/__init__.py +6 -0
  44. mcli/ml/dashboard/pages/cicd.py +382 -0
  45. mcli/ml/dashboard/pages/predictions_enhanced.py +834 -0
  46. mcli/ml/dashboard/pages/scrapers_and_logs.py +1060 -0
  47. mcli/ml/dashboard/pages/test_portfolio.py +373 -0
  48. mcli/ml/dashboard/pages/trading.py +714 -0
  49. mcli/ml/dashboard/pages/workflows.py +533 -0
  50. mcli/ml/dashboard/utils.py +154 -0
  51. mcli/ml/data_ingestion/__init__.py +39 -0
  52. mcli/ml/database/__init__.py +47 -0
  53. mcli/ml/experimentation/__init__.py +29 -0
  54. mcli/ml/features/__init__.py +39 -0
  55. mcli/ml/mlops/__init__.py +33 -0
  56. mcli/ml/models/__init__.py +94 -0
  57. mcli/ml/monitoring/__init__.py +25 -0
  58. mcli/ml/optimization/__init__.py +27 -0
  59. mcli/ml/predictions/__init__.py +5 -0
  60. mcli/ml/preprocessing/__init__.py +28 -0
  61. mcli/ml/scripts/__init__.py +1 -0
  62. mcli/ml/trading/__init__.py +60 -0
  63. mcli/ml/trading/alpaca_client.py +353 -0
  64. mcli/ml/trading/migrations.py +164 -0
  65. mcli/ml/trading/models.py +418 -0
  66. mcli/ml/trading/paper_trading.py +326 -0
  67. mcli/ml/trading/risk_management.py +370 -0
  68. mcli/ml/trading/trading_service.py +480 -0
  69. mcli/ml/training/__init__.py +10 -0
  70. mcli/ml/training/train_model.py +569 -0
  71. mcli/mygroup/__init__.py +3 -0
  72. mcli/public/__init__.py +1 -0
  73. mcli/public/commands/__init__.py +2 -0
  74. mcli/self/__init__.py +3 -0
  75. mcli/self/self_cmd.py +579 -91
  76. mcli/workflow/__init__.py +0 -0
  77. mcli/workflow/daemon/__init__.py +15 -0
  78. mcli/workflow/daemon/daemon.py +21 -3
  79. mcli/workflow/dashboard/__init__.py +5 -0
  80. mcli/workflow/docker/__init__.py +0 -0
  81. mcli/workflow/file/__init__.py +0 -0
  82. mcli/workflow/gcloud/__init__.py +1 -0
  83. mcli/workflow/git_commit/__init__.py +0 -0
  84. mcli/workflow/interview/__init__.py +0 -0
  85. mcli/workflow/politician_trading/__init__.py +4 -0
  86. mcli/workflow/politician_trading/data_sources.py +259 -1
  87. mcli/workflow/politician_trading/models.py +159 -1
  88. mcli/workflow/politician_trading/scrapers_corporate_registry.py +846 -0
  89. mcli/workflow/politician_trading/scrapers_free_sources.py +516 -0
  90. mcli/workflow/politician_trading/scrapers_third_party.py +391 -0
  91. mcli/workflow/politician_trading/seed_database.py +539 -0
  92. mcli/workflow/registry/__init__.py +0 -0
  93. mcli/workflow/repo/__init__.py +0 -0
  94. mcli/workflow/scheduler/__init__.py +25 -0
  95. mcli/workflow/search/__init__.py +0 -0
  96. mcli/workflow/sync/__init__.py +5 -0
  97. mcli/workflow/videos/__init__.py +1 -0
  98. mcli/workflow/wakatime/__init__.py +80 -0
  99. mcli/workflow/workflow.py +8 -27
  100. {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/METADATA +3 -1
  101. {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/RECORD +105 -29
  102. mcli/workflow/daemon/api_daemon.py +0 -800
  103. mcli/workflow/daemon/commands.py +0 -1196
  104. mcli/workflow/dashboard/dashboard_cmd.py +0 -120
  105. mcli/workflow/file/file.py +0 -100
  106. mcli/workflow/git_commit/commands.py +0 -430
  107. mcli/workflow/politician_trading/commands.py +0 -1939
  108. mcli/workflow/scheduler/commands.py +0 -493
  109. mcli/workflow/sync/sync_cmd.py +0 -437
  110. mcli/workflow/videos/videos.py +0 -242
  111. {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/WHEEL +0 -0
  112. {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/entry_points.txt +0 -0
  113. {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/licenses/LICENSE +0 -0
  114. {mcli_framework-7.1.3.dist-info → mcli_framework-7.3.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,326 @@
1
+ """Paper trading implementation for testing portfolios without real money"""
2
+
3
+ import logging
4
+ from datetime import datetime, timedelta
5
+ from decimal import Decimal
6
+ from typing import Dict, List, Optional, Tuple
7
+ from uuid import UUID
8
+
9
+ import pandas as pd
10
+ import yfinance as yf
11
+ from sqlalchemy.orm import Session
12
+
13
+ from mcli.ml.trading.models import (
14
+ Portfolio, Position, TradingOrder, OrderStatus, OrderType, OrderSide, PositionSide
15
+ )
16
+ from mcli.ml.trading.trading_service import TradingService
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class PaperTradingEngine:
22
+ """Paper trading engine for testing strategies without real money"""
23
+
24
+ def __init__(self, trading_service: TradingService):
25
+ self.trading_service = trading_service
26
+ self.db = trading_service.db
27
+
28
+ def execute_paper_order(self, order: TradingOrder) -> bool:
29
+ """Execute a paper trade order"""
30
+ try:
31
+ # Get current market price
32
+ current_price = self._get_current_price(order.symbol)
33
+ if current_price is None:
34
+ logger.error(f"Could not get current price for {order.symbol}")
35
+ return False
36
+
37
+ # Calculate execution details
38
+ if order.order_type == OrderType.MARKET:
39
+ execution_price = current_price
40
+ elif order.order_type == OrderType.LIMIT:
41
+ if order.side == OrderSide.BUY and order.limit_price >= current_price:
42
+ execution_price = current_price
43
+ elif order.side == OrderSide.SELL and order.limit_price <= current_price:
44
+ execution_price = current_price
45
+ else:
46
+ # Order not executed
47
+ order.status = OrderStatus.CANCELLED
48
+ order.cancelled_at = datetime.utcnow()
49
+ self.db.commit()
50
+ return False
51
+ else:
52
+ logger.error(f"Unsupported order type for paper trading: {order.order_type}")
53
+ return False
54
+
55
+ # Execute the order
56
+ order.status = OrderStatus.FILLED
57
+ order.filled_at = datetime.utcnow()
58
+ order.filled_quantity = order.quantity
59
+ order.remaining_quantity = 0
60
+ order.average_fill_price = Decimal(str(execution_price))
61
+
62
+ # Update portfolio and positions
63
+ self._update_portfolio_positions(order, execution_price)
64
+
65
+ self.db.commit()
66
+ logger.info(f"Paper trade executed: {order.symbol} {order.side.value} {order.quantity} @ ${execution_price}")
67
+ return True
68
+
69
+ except Exception as e:
70
+ logger.error(f"Failed to execute paper order: {e}")
71
+ self.db.rollback()
72
+ return False
73
+
74
+ def _get_current_price(self, symbol: str) -> Optional[float]:
75
+ """Get current market price for a symbol"""
76
+ try:
77
+ ticker = yf.Ticker(symbol)
78
+ data = ticker.history(period="1d", interval="1m")
79
+ if not data.empty:
80
+ return float(data["Close"].iloc[-1])
81
+ return None
82
+ except Exception as e:
83
+ logger.error(f"Failed to get price for {symbol}: {e}")
84
+ return None
85
+
86
+ def _update_portfolio_positions(self, order: TradingOrder, execution_price: float):
87
+ """Update portfolio positions after order execution"""
88
+ try:
89
+ portfolio = self.trading_service.get_portfolio(order.portfolio_id)
90
+ if not portfolio:
91
+ return
92
+
93
+ # Calculate trade value
94
+ trade_value = execution_price * order.quantity
95
+
96
+ if order.side == OrderSide.BUY:
97
+ # Buying shares
98
+ portfolio.cash_balance -= Decimal(str(trade_value))
99
+
100
+ # Update or create position
101
+ position = self.db.query(Position).filter(
102
+ Position.portfolio_id == order.portfolio_id,
103
+ Position.symbol == order.symbol
104
+ ).first()
105
+
106
+ if position:
107
+ # Update existing position
108
+ total_quantity = position.quantity + order.quantity
109
+ total_cost = (position.cost_basis * position.quantity) + trade_value
110
+ new_avg_price = total_cost / total_quantity
111
+
112
+ position.quantity = total_quantity
113
+ position.average_price = new_avg_price
114
+ position.cost_basis = total_cost
115
+ position.current_price = Decimal(str(execution_price))
116
+ position.market_value = total_quantity * execution_price
117
+ position.unrealized_pnl = position.market_value - total_cost
118
+ position.unrealized_pnl_pct = float(position.unrealized_pnl / total_cost * 100)
119
+ else:
120
+ # Create new position
121
+ position = Position(
122
+ portfolio_id=order.portfolio_id,
123
+ symbol=order.symbol,
124
+ quantity=order.quantity,
125
+ side=PositionSide.LONG,
126
+ average_price=Decimal(str(execution_price)),
127
+ current_price=Decimal(str(execution_price)),
128
+ market_value=Decimal(str(trade_value)),
129
+ cost_basis=Decimal(str(trade_value)),
130
+ unrealized_pnl=Decimal("0"),
131
+ unrealized_pnl_pct=0.0,
132
+ realized_pnl=Decimal("0"),
133
+ position_size_pct=0.0,
134
+ weight=0.0,
135
+ )
136
+ self.db.add(position)
137
+
138
+ else: # SELL
139
+ # Selling shares
140
+ portfolio.cash_balance += Decimal(str(trade_value))
141
+
142
+ # Update position
143
+ position = self.db.query(Position).filter(
144
+ Position.portfolio_id == order.portfolio_id,
145
+ Position.symbol == order.symbol
146
+ ).first()
147
+
148
+ if position and position.quantity >= order.quantity:
149
+ # Calculate realized P&L
150
+ cost_basis = position.cost_basis * (order.quantity / position.quantity)
151
+ realized_pnl = trade_value - float(cost_basis)
152
+
153
+ # Update position
154
+ position.quantity -= order.quantity
155
+ position.cost_basis -= cost_basis
156
+ position.realized_pnl += Decimal(str(realized_pnl))
157
+
158
+ if position.quantity == 0:
159
+ # Remove position if fully sold
160
+ self.db.delete(position)
161
+ else:
162
+ # Update remaining position
163
+ position.current_price = Decimal(str(execution_price))
164
+ position.market_value = position.quantity * execution_price
165
+ position.unrealized_pnl = position.market_value - position.cost_basis
166
+ if position.cost_basis > 0:
167
+ position.unrealized_pnl_pct = float(position.unrealized_pnl / position.cost_basis * 100)
168
+
169
+ # Update portfolio value
170
+ self._update_portfolio_value(portfolio)
171
+
172
+ except Exception as e:
173
+ logger.error(f"Failed to update portfolio positions: {e}")
174
+ raise
175
+
176
+ def _update_portfolio_value(self, portfolio: Portfolio):
177
+ """Update portfolio value and metrics"""
178
+ try:
179
+ # Get all positions
180
+ positions = self.db.query(Position).filter(Position.portfolio_id == portfolio.id).all()
181
+
182
+ # Calculate total market value
183
+ total_market_value = sum(float(pos.market_value) for pos in positions)
184
+ portfolio.current_value = Decimal(str(total_market_value + float(portfolio.cash_balance)))
185
+
186
+ # Calculate returns
187
+ if portfolio.initial_capital > 0:
188
+ total_return = portfolio.current_value - portfolio.initial_capital
189
+ portfolio.total_return = float(total_return)
190
+ portfolio.total_return_pct = float(total_return / portfolio.initial_capital * 100)
191
+
192
+ # Update position weights
193
+ for position in positions:
194
+ if portfolio.current_value > 0:
195
+ position.weight = float(position.market_value / portfolio.current_value)
196
+ position.position_size_pct = position.weight * 100
197
+
198
+ except Exception as e:
199
+ logger.error(f"Failed to update portfolio value: {e}")
200
+ raise
201
+
202
+ def simulate_market_movement(self, portfolio_id: UUID, days: int = 1):
203
+ """Simulate market movement for paper trading"""
204
+ try:
205
+ portfolio = self.trading_service.get_portfolio(portfolio_id)
206
+ if not portfolio:
207
+ return
208
+
209
+ positions = self.db.query(Position).filter(Position.portfolio_id == portfolio_id).all()
210
+
211
+ for position in positions:
212
+ # Get historical data for the symbol
213
+ ticker = yf.Ticker(position.symbol)
214
+ end_date = datetime.now()
215
+ start_date = end_date - timedelta(days=days + 5) # Get extra days for weekend handling
216
+
217
+ data = ticker.history(start=start_date, end=end_date)
218
+ if not data.empty:
219
+ # Get the most recent price
220
+ new_price = float(data["Close"].iloc[-1])
221
+
222
+ # Update position
223
+ position.current_price = Decimal(str(new_price))
224
+ position.market_value = position.quantity * new_price
225
+ position.unrealized_pnl = position.market_value - position.cost_basis
226
+ if position.cost_basis > 0:
227
+ position.unrealized_pnl_pct = float(position.unrealized_pnl / position.cost_basis * 100)
228
+
229
+ # Update portfolio value
230
+ self._update_portfolio_value(portfolio)
231
+ self.db.commit()
232
+
233
+ logger.info(f"Simulated market movement for portfolio {portfolio_id}")
234
+
235
+ except Exception as e:
236
+ logger.error(f"Failed to simulate market movement: {e}")
237
+ self.db.rollback()
238
+
239
+ def create_test_portfolio(
240
+ self,
241
+ user_id: UUID,
242
+ name: str = "Test Portfolio",
243
+ initial_capital: float = 100000.0
244
+ ) -> Portfolio:
245
+ """Create a test portfolio for paper trading"""
246
+ try:
247
+ # Create trading account
248
+ from mcli.ml.trading.models import TradingAccountCreate
249
+ account_data = TradingAccountCreate(
250
+ account_name="Test Account",
251
+ account_type="test",
252
+ paper_trading=True
253
+ )
254
+ account = self.trading_service.create_trading_account(user_id, account_data)
255
+
256
+ # Create portfolio
257
+ portfolio_data = PortfolioCreate(
258
+ name=name,
259
+ description="Test portfolio for paper trading",
260
+ initial_capital=initial_capital
261
+ )
262
+ portfolio = self.trading_service.create_portfolio(account.id, portfolio_data)
263
+
264
+ logger.info(f"Created test portfolio {portfolio.id} for user {user_id}")
265
+ return portfolio
266
+
267
+ except Exception as e:
268
+ logger.error(f"Failed to create test portfolio: {e}")
269
+ raise
270
+
271
+ def run_backtest(
272
+ self,
273
+ portfolio_id: UUID,
274
+ start_date: datetime,
275
+ end_date: datetime,
276
+ initial_capital: float = 100000.0
277
+ ) -> Dict:
278
+ """Run a backtest on historical data"""
279
+ try:
280
+ portfolio = self.trading_service.get_portfolio(portfolio_id)
281
+ if not portfolio:
282
+ raise ValueError("Portfolio not found")
283
+
284
+ # Reset portfolio to initial state
285
+ portfolio.current_value = Decimal(str(initial_capital))
286
+ portfolio.cash_balance = Decimal(str(initial_capital))
287
+ portfolio.total_return = 0.0
288
+ portfolio.total_return_pct = 0.0
289
+
290
+ # Clear existing positions
291
+ self.db.query(Position).filter(Position.portfolio_id == portfolio_id).delete()
292
+
293
+ # Get historical data for the period
294
+ date_range = pd.date_range(start=start_date, end=end_date, freq='D')
295
+
296
+ # This is a simplified backtest - in practice you'd want to:
297
+ # 1. Get historical signals
298
+ # 2. Execute trades based on signals
299
+ # 3. Track performance over time
300
+
301
+ backtest_results = {
302
+ "start_date": start_date,
303
+ "end_date": end_date,
304
+ "initial_capital": initial_capital,
305
+ "final_value": float(portfolio.current_value),
306
+ "total_return": float(portfolio.total_return),
307
+ "total_return_pct": portfolio.total_return_pct,
308
+ "trades_executed": 0,
309
+ "max_drawdown": 0.0,
310
+ "sharpe_ratio": 0.0,
311
+ }
312
+
313
+ self.db.commit()
314
+ logger.info(f"Backtest completed for portfolio {portfolio_id}")
315
+ return backtest_results
316
+
317
+ except Exception as e:
318
+ logger.error(f"Failed to run backtest: {e}")
319
+ self.db.rollback()
320
+ raise
321
+
322
+
323
+ def create_paper_trading_engine(db_session: Session) -> PaperTradingEngine:
324
+ """Create a paper trading engine"""
325
+ trading_service = TradingService(db_session)
326
+ return PaperTradingEngine(trading_service)
@@ -0,0 +1,370 @@
1
+ """Risk management module for trading portfolios"""
2
+
3
+ import logging
4
+ from datetime import datetime, timedelta
5
+ from typing import Dict, List, Optional, Tuple
6
+ from uuid import UUID
7
+
8
+ import numpy as np
9
+ import pandas as pd
10
+ from sqlalchemy.orm import Session
11
+
12
+ from mcli.ml.trading.models import Portfolio, Position, TradingOrder, RiskLevel
13
+ from mcli.ml.trading.trading_service import TradingService
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class RiskManager:
19
+ """Risk management system for trading portfolios"""
20
+
21
+ def __init__(self, trading_service: TradingService):
22
+ self.trading_service = trading_service
23
+ self.db = trading_service.db
24
+
25
+ def calculate_portfolio_risk(self, portfolio_id: UUID) -> Dict:
26
+ """Calculate comprehensive risk metrics for a portfolio"""
27
+ try:
28
+ portfolio = self.trading_service.get_portfolio(portfolio_id)
29
+ if not portfolio:
30
+ return {}
31
+
32
+ positions = self.trading_service.get_portfolio_positions(portfolio_id)
33
+
34
+ if not positions:
35
+ return {
36
+ "total_risk": 0.0,
37
+ "concentration_risk": 0.0,
38
+ "var_95": 0.0,
39
+ "cvar_95": 0.0,
40
+ "max_position_risk": 0.0,
41
+ "portfolio_beta": 1.0,
42
+ "risk_score": 0.0,
43
+ }
44
+
45
+ # Calculate individual position risks
46
+ position_risks = []
47
+ total_market_value = sum(pos.market_value for pos in positions)
48
+
49
+ for position in positions:
50
+ position_risk = self._calculate_position_risk(position, total_market_value)
51
+ position_risks.append(position_risk)
52
+
53
+ # Calculate portfolio-level risk metrics
54
+ portfolio_risk = self._calculate_portfolio_risk_metrics(positions, position_risks)
55
+
56
+ return portfolio_risk
57
+
58
+ except Exception as e:
59
+ logger.error(f"Failed to calculate portfolio risk: {e}")
60
+ return {}
61
+
62
+ def _calculate_position_risk(self, position: Position, total_market_value: float) -> Dict:
63
+ """Calculate risk metrics for an individual position"""
64
+ try:
65
+ # Position size as percentage of portfolio
66
+ position_size_pct = (position.market_value / total_market_value) * 100
67
+
68
+ # Volatility estimate (simplified - in practice, use historical data)
69
+ volatility = self._estimate_volatility(position.symbol)
70
+
71
+ # Value at Risk (simplified calculation)
72
+ var_95 = position.market_value * volatility * 1.645 # 95% VaR
73
+
74
+ # Position risk score (combination of size and volatility)
75
+ risk_score = position_size_pct * volatility * 100
76
+
77
+ return {
78
+ "symbol": position.symbol,
79
+ "position_size_pct": position_size_pct,
80
+ "volatility": volatility,
81
+ "var_95": var_95,
82
+ "risk_score": risk_score,
83
+ "market_value": position.market_value,
84
+ }
85
+
86
+ except Exception as e:
87
+ logger.error(f"Failed to calculate position risk for {position.symbol}: {e}")
88
+ return {}
89
+
90
+ def _estimate_volatility(self, symbol: str, days: int = 30) -> float:
91
+ """Estimate volatility for a symbol (simplified)"""
92
+ try:
93
+ # In practice, you would calculate historical volatility
94
+ # For now, use a simplified approach based on market cap and sector
95
+ import yfinance as yf
96
+
97
+ ticker = yf.Ticker(symbol)
98
+ data = ticker.history(period=f"{days}d")
99
+
100
+ if not data.empty and len(data) > 1:
101
+ returns = data["Close"].pct_change().dropna()
102
+ volatility = returns.std() * np.sqrt(252) # Annualized
103
+ return min(volatility, 1.0) # Cap at 100%
104
+
105
+ # Default volatility based on symbol characteristics
106
+ if symbol in ["AAPL", "MSFT", "GOOGL", "AMZN"]:
107
+ return 0.25 # Large cap tech
108
+ elif symbol in ["TSLA", "NVDA", "AMD"]:
109
+ return 0.45 # High volatility tech
110
+ else:
111
+ return 0.30 # Default
112
+
113
+ except Exception as e:
114
+ logger.error(f"Failed to estimate volatility for {symbol}: {e}")
115
+ return 0.30 # Default volatility
116
+
117
+ def _calculate_portfolio_risk_metrics(self, positions: List, position_risks: List[Dict]) -> Dict:
118
+ """Calculate portfolio-level risk metrics"""
119
+ try:
120
+ if not positions or not position_risks:
121
+ return {}
122
+
123
+ # Total portfolio risk (sum of individual position risks)
124
+ total_risk = sum(risk["risk_score"] for risk in position_risks)
125
+
126
+ # Concentration risk (Herfindahl-Hirschman Index)
127
+ weights = [pos.weight for pos in positions]
128
+ concentration_risk = sum(w**2 for w in weights) * 100
129
+
130
+ # Portfolio Value at Risk (simplified)
131
+ portfolio_var = sum(risk["var_95"] for risk in position_risks)
132
+
133
+ # Conditional Value at Risk (simplified)
134
+ portfolio_cvar = portfolio_var * 1.3 # Rough approximation
135
+
136
+ # Maximum position risk
137
+ max_position_risk = max(risk["risk_score"] for risk in position_risks) if position_risks else 0
138
+
139
+ # Portfolio beta (simplified - assume equal weight)
140
+ portfolio_beta = np.mean([self._estimate_beta(pos.symbol) for pos in positions])
141
+
142
+ # Overall risk score (0-100)
143
+ risk_score = min(total_risk + concentration_risk, 100)
144
+
145
+ return {
146
+ "total_risk": total_risk,
147
+ "concentration_risk": concentration_risk,
148
+ "var_95": portfolio_var,
149
+ "cvar_95": portfolio_cvar,
150
+ "max_position_risk": max_position_risk,
151
+ "portfolio_beta": portfolio_beta,
152
+ "risk_score": risk_score,
153
+ "num_positions": len(positions),
154
+ }
155
+
156
+ except Exception as e:
157
+ logger.error(f"Failed to calculate portfolio risk metrics: {e}")
158
+ return {}
159
+
160
+ def _estimate_beta(self, symbol: str) -> float:
161
+ """Estimate beta for a symbol (simplified)"""
162
+ try:
163
+ # In practice, calculate beta against market index
164
+ # For now, use simplified estimates
165
+ beta_estimates = {
166
+ "AAPL": 1.2, "MSFT": 1.1, "GOOGL": 1.3, "AMZN": 1.4,
167
+ "TSLA": 2.0, "NVDA": 1.8, "AMD": 1.6, "META": 1.5,
168
+ "JPM": 1.3, "BAC": 1.4, "WMT": 0.5, "JNJ": 0.7,
169
+ }
170
+ return beta_estimates.get(symbol, 1.0)
171
+ except Exception as e:
172
+ logger.error(f"Failed to estimate beta for {symbol}: {e}")
173
+ return 1.0
174
+
175
+ def check_risk_limits(self, portfolio_id: UUID, new_order: Dict) -> Tuple[bool, List[str]]:
176
+ """Check if a new order would violate risk limits"""
177
+ try:
178
+ portfolio = self.trading_service.get_portfolio(portfolio_id)
179
+ if not portfolio:
180
+ return False, ["Portfolio not found"]
181
+
182
+ warnings = []
183
+
184
+ # Get current positions
185
+ positions = self.trading_service.get_portfolio_positions(portfolio_id)
186
+
187
+ # Calculate new position size
188
+ symbol = new_order["symbol"]
189
+ quantity = new_order["quantity"]
190
+ side = new_order["side"]
191
+
192
+ # Estimate order value (simplified)
193
+ order_value = self._estimate_order_value(symbol, quantity)
194
+ new_position_size_pct = (order_value / float(portfolio.current_value)) * 100
195
+
196
+ # Check position size limits
197
+ max_position_size = 10.0 # 10% default
198
+ if new_position_size_pct > max_position_size:
199
+ warnings.append(f"Position size ({new_position_size_pct:.1f}%) exceeds limit ({max_position_size}%)")
200
+
201
+ # Check if adding to existing position
202
+ existing_position = next((pos for pos in positions if pos.symbol == symbol), None)
203
+ if existing_position:
204
+ if side == "buy":
205
+ new_total_size = ((existing_position.market_value + order_value) / float(portfolio.current_value)) * 100
206
+ if new_total_size > max_position_size:
207
+ warnings.append(f"Total position size ({new_total_size:.1f}%) would exceed limit")
208
+
209
+ # Check portfolio concentration
210
+ if len(positions) >= 10: # Max 10 positions
211
+ warnings.append("Portfolio already has maximum number of positions")
212
+
213
+ # Check buying power
214
+ if side == "buy" and order_value > float(portfolio.cash_balance):
215
+ warnings.append("Insufficient buying power for this order")
216
+
217
+ # Check risk level compliance
218
+ risk_level = getattr(portfolio, 'risk_level', RiskLevel.MODERATE)
219
+ if risk_level == RiskLevel.CONSERVATIVE and new_position_size_pct > 5.0:
220
+ warnings.append("Position size too large for conservative risk level")
221
+ elif risk_level == RiskLevel.AGGRESSIVE and new_position_size_pct > 20.0:
222
+ warnings.append("Position size too large for aggressive risk level")
223
+
224
+ return len(warnings) == 0, warnings
225
+
226
+ except Exception as e:
227
+ logger.error(f"Failed to check risk limits: {e}")
228
+ return False, [f"Risk check failed: {e}"]
229
+
230
+ def _estimate_order_value(self, symbol: str, quantity: int) -> float:
231
+ """Estimate the value of an order"""
232
+ try:
233
+ import yfinance as yf
234
+ ticker = yf.Ticker(symbol)
235
+ data = ticker.history(period="1d")
236
+ if not data.empty:
237
+ price = float(data["Close"].iloc[-1])
238
+ return price * quantity
239
+ return 0.0
240
+ except Exception as e:
241
+ logger.error(f"Failed to estimate order value for {symbol}: {e}")
242
+ return 0.0
243
+
244
+ def calculate_position_size(
245
+ self,
246
+ portfolio_id: UUID,
247
+ symbol: str,
248
+ signal_strength: float,
249
+ risk_level: RiskLevel = RiskLevel.MODERATE
250
+ ) -> float:
251
+ """Calculate recommended position size based on signal strength and risk level"""
252
+ try:
253
+ portfolio = self.trading_service.get_portfolio(portfolio_id)
254
+ if not portfolio:
255
+ return 0.0
256
+
257
+ # Base position size based on risk level
258
+ base_sizes = {
259
+ RiskLevel.CONSERVATIVE: 0.02, # 2%
260
+ RiskLevel.MODERATE: 0.05, # 5%
261
+ RiskLevel.AGGRESSIVE: 0.10, # 10%
262
+ }
263
+ base_size = base_sizes.get(risk_level, 0.05)
264
+
265
+ # Adjust based on signal strength
266
+ signal_multiplier = min(signal_strength * 2, 2.0) # Cap at 2x
267
+
268
+ # Calculate recommended position size
269
+ recommended_size = base_size * signal_multiplier
270
+
271
+ # Cap at maximum position size
272
+ max_position_size = 0.20 # 20% max
273
+ recommended_size = min(recommended_size, max_position_size)
274
+
275
+ # Convert to dollar amount
276
+ position_value = float(portfolio.current_value) * recommended_size
277
+
278
+ return position_value
279
+
280
+ except Exception as e:
281
+ logger.error(f"Failed to calculate position size: {e}")
282
+ return 0.0
283
+
284
+ def generate_risk_report(self, portfolio_id: UUID) -> Dict:
285
+ """Generate comprehensive risk report for a portfolio"""
286
+ try:
287
+ portfolio = self.trading_service.get_portfolio(portfolio_id)
288
+ if not portfolio:
289
+ return {}
290
+
291
+ # Get risk metrics
292
+ risk_metrics = self.calculate_portfolio_risk(portfolio_id)
293
+
294
+ # Get performance data
295
+ performance_df = self.trading_service.get_portfolio_performance(portfolio_id, days=30)
296
+
297
+ # Calculate additional metrics
298
+ max_drawdown = 0.0
299
+ if not performance_df.empty:
300
+ cumulative_returns = (1 + performance_df["daily_return_pct"] / 100).cumprod()
301
+ running_max = cumulative_returns.expanding().max()
302
+ drawdown = (cumulative_returns - running_max) / running_max
303
+ max_drawdown = abs(drawdown.min()) * 100
304
+
305
+ # Risk assessment
306
+ risk_level = "LOW"
307
+ if risk_metrics.get("risk_score", 0) > 70:
308
+ risk_level = "HIGH"
309
+ elif risk_metrics.get("risk_score", 0) > 40:
310
+ risk_level = "MEDIUM"
311
+
312
+ # Generate recommendations
313
+ recommendations = self._generate_risk_recommendations(risk_metrics, max_drawdown)
314
+
315
+ return {
316
+ "portfolio_id": str(portfolio_id),
317
+ "risk_level": risk_level,
318
+ "risk_score": risk_metrics.get("risk_score", 0),
319
+ "max_drawdown": max_drawdown,
320
+ "concentration_risk": risk_metrics.get("concentration_risk", 0),
321
+ "var_95": risk_metrics.get("var_95", 0),
322
+ "cvar_95": risk_metrics.get("cvar_95", 0),
323
+ "portfolio_beta": risk_metrics.get("portfolio_beta", 1.0),
324
+ "num_positions": risk_metrics.get("num_positions", 0),
325
+ "recommendations": recommendations,
326
+ "generated_at": datetime.utcnow().isoformat(),
327
+ }
328
+
329
+ except Exception as e:
330
+ logger.error(f"Failed to generate risk report: {e}")
331
+ return {}
332
+
333
+ def _generate_risk_recommendations(self, risk_metrics: Dict, max_drawdown: float) -> List[str]:
334
+ """Generate risk management recommendations"""
335
+ recommendations = []
336
+
337
+ # Concentration risk
338
+ if risk_metrics.get("concentration_risk", 0) > 50:
339
+ recommendations.append("Consider diversifying portfolio - concentration risk is high")
340
+
341
+ # Position count
342
+ if risk_metrics.get("num_positions", 0) < 5:
343
+ recommendations.append("Consider adding more positions for better diversification")
344
+ elif risk_metrics.get("num_positions", 0) > 15:
345
+ recommendations.append("Consider reducing number of positions for better focus")
346
+
347
+ # Risk score
348
+ if risk_metrics.get("risk_score", 0) > 70:
349
+ recommendations.append("Portfolio risk is high - consider reducing position sizes")
350
+
351
+ # Drawdown
352
+ if max_drawdown > 20:
353
+ recommendations.append("Maximum drawdown is high - consider risk management strategies")
354
+
355
+ # Beta
356
+ if risk_metrics.get("portfolio_beta", 1.0) > 1.5:
357
+ recommendations.append("Portfolio beta is high - consider adding defensive positions")
358
+ elif risk_metrics.get("portfolio_beta", 1.0) < 0.5:
359
+ recommendations.append("Portfolio beta is low - consider adding growth positions")
360
+
361
+ if not recommendations:
362
+ recommendations.append("Portfolio risk profile looks good - no immediate concerns")
363
+
364
+ return recommendations
365
+
366
+
367
+ def create_risk_manager(db_session: Session) -> RiskManager:
368
+ """Create a risk manager instance"""
369
+ trading_service = TradingService(db_session)
370
+ return RiskManager(trading_service)