mcli-framework 7.0.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.

Potentially problematic release.


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

Files changed (186) hide show
  1. mcli/app/chat_cmd.py +42 -0
  2. mcli/app/commands_cmd.py +226 -0
  3. mcli/app/completion_cmd.py +216 -0
  4. mcli/app/completion_helpers.py +288 -0
  5. mcli/app/cron_test_cmd.py +697 -0
  6. mcli/app/logs_cmd.py +419 -0
  7. mcli/app/main.py +492 -0
  8. mcli/app/model/model.py +1060 -0
  9. mcli/app/model_cmd.py +227 -0
  10. mcli/app/redis_cmd.py +269 -0
  11. mcli/app/video/video.py +1114 -0
  12. mcli/app/visual_cmd.py +303 -0
  13. mcli/chat/chat.py +2409 -0
  14. mcli/chat/command_rag.py +514 -0
  15. mcli/chat/enhanced_chat.py +652 -0
  16. mcli/chat/system_controller.py +1010 -0
  17. mcli/chat/system_integration.py +1016 -0
  18. mcli/cli.py +25 -0
  19. mcli/config.toml +20 -0
  20. mcli/lib/api/api.py +586 -0
  21. mcli/lib/api/daemon_client.py +203 -0
  22. mcli/lib/api/daemon_client_local.py +44 -0
  23. mcli/lib/api/daemon_decorator.py +217 -0
  24. mcli/lib/api/mcli_decorators.py +1032 -0
  25. mcli/lib/auth/auth.py +85 -0
  26. mcli/lib/auth/aws_manager.py +85 -0
  27. mcli/lib/auth/azure_manager.py +91 -0
  28. mcli/lib/auth/credential_manager.py +192 -0
  29. mcli/lib/auth/gcp_manager.py +93 -0
  30. mcli/lib/auth/key_manager.py +117 -0
  31. mcli/lib/auth/mcli_manager.py +93 -0
  32. mcli/lib/auth/token_manager.py +75 -0
  33. mcli/lib/auth/token_util.py +1011 -0
  34. mcli/lib/config/config.py +47 -0
  35. mcli/lib/discovery/__init__.py +1 -0
  36. mcli/lib/discovery/command_discovery.py +274 -0
  37. mcli/lib/erd/erd.py +1345 -0
  38. mcli/lib/erd/generate_graph.py +453 -0
  39. mcli/lib/files/files.py +76 -0
  40. mcli/lib/fs/fs.py +109 -0
  41. mcli/lib/lib.py +29 -0
  42. mcli/lib/logger/logger.py +611 -0
  43. mcli/lib/performance/optimizer.py +409 -0
  44. mcli/lib/performance/rust_bridge.py +502 -0
  45. mcli/lib/performance/uvloop_config.py +154 -0
  46. mcli/lib/pickles/pickles.py +50 -0
  47. mcli/lib/search/cached_vectorizer.py +479 -0
  48. mcli/lib/services/data_pipeline.py +460 -0
  49. mcli/lib/services/lsh_client.py +441 -0
  50. mcli/lib/services/redis_service.py +387 -0
  51. mcli/lib/shell/shell.py +137 -0
  52. mcli/lib/toml/toml.py +33 -0
  53. mcli/lib/ui/styling.py +47 -0
  54. mcli/lib/ui/visual_effects.py +634 -0
  55. mcli/lib/watcher/watcher.py +185 -0
  56. mcli/ml/api/app.py +215 -0
  57. mcli/ml/api/middleware.py +224 -0
  58. mcli/ml/api/routers/admin_router.py +12 -0
  59. mcli/ml/api/routers/auth_router.py +244 -0
  60. mcli/ml/api/routers/backtest_router.py +12 -0
  61. mcli/ml/api/routers/data_router.py +12 -0
  62. mcli/ml/api/routers/model_router.py +302 -0
  63. mcli/ml/api/routers/monitoring_router.py +12 -0
  64. mcli/ml/api/routers/portfolio_router.py +12 -0
  65. mcli/ml/api/routers/prediction_router.py +267 -0
  66. mcli/ml/api/routers/trade_router.py +12 -0
  67. mcli/ml/api/routers/websocket_router.py +76 -0
  68. mcli/ml/api/schemas.py +64 -0
  69. mcli/ml/auth/auth_manager.py +425 -0
  70. mcli/ml/auth/models.py +154 -0
  71. mcli/ml/auth/permissions.py +302 -0
  72. mcli/ml/backtesting/backtest_engine.py +502 -0
  73. mcli/ml/backtesting/performance_metrics.py +393 -0
  74. mcli/ml/cache.py +400 -0
  75. mcli/ml/cli/main.py +398 -0
  76. mcli/ml/config/settings.py +394 -0
  77. mcli/ml/configs/dvc_config.py +230 -0
  78. mcli/ml/configs/mlflow_config.py +131 -0
  79. mcli/ml/configs/mlops_manager.py +293 -0
  80. mcli/ml/dashboard/app.py +532 -0
  81. mcli/ml/dashboard/app_integrated.py +738 -0
  82. mcli/ml/dashboard/app_supabase.py +560 -0
  83. mcli/ml/dashboard/app_training.py +615 -0
  84. mcli/ml/dashboard/cli.py +51 -0
  85. mcli/ml/data_ingestion/api_connectors.py +501 -0
  86. mcli/ml/data_ingestion/data_pipeline.py +567 -0
  87. mcli/ml/data_ingestion/stream_processor.py +512 -0
  88. mcli/ml/database/migrations/env.py +94 -0
  89. mcli/ml/database/models.py +667 -0
  90. mcli/ml/database/session.py +200 -0
  91. mcli/ml/experimentation/ab_testing.py +845 -0
  92. mcli/ml/features/ensemble_features.py +607 -0
  93. mcli/ml/features/political_features.py +676 -0
  94. mcli/ml/features/recommendation_engine.py +809 -0
  95. mcli/ml/features/stock_features.py +573 -0
  96. mcli/ml/features/test_feature_engineering.py +346 -0
  97. mcli/ml/logging.py +85 -0
  98. mcli/ml/mlops/data_versioning.py +518 -0
  99. mcli/ml/mlops/experiment_tracker.py +377 -0
  100. mcli/ml/mlops/model_serving.py +481 -0
  101. mcli/ml/mlops/pipeline_orchestrator.py +614 -0
  102. mcli/ml/models/base_models.py +324 -0
  103. mcli/ml/models/ensemble_models.py +675 -0
  104. mcli/ml/models/recommendation_models.py +474 -0
  105. mcli/ml/models/test_models.py +487 -0
  106. mcli/ml/monitoring/drift_detection.py +676 -0
  107. mcli/ml/monitoring/metrics.py +45 -0
  108. mcli/ml/optimization/portfolio_optimizer.py +834 -0
  109. mcli/ml/preprocessing/data_cleaners.py +451 -0
  110. mcli/ml/preprocessing/feature_extractors.py +491 -0
  111. mcli/ml/preprocessing/ml_pipeline.py +382 -0
  112. mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
  113. mcli/ml/preprocessing/test_preprocessing.py +294 -0
  114. mcli/ml/scripts/populate_sample_data.py +200 -0
  115. mcli/ml/tasks.py +400 -0
  116. mcli/ml/tests/test_integration.py +429 -0
  117. mcli/ml/tests/test_training_dashboard.py +387 -0
  118. mcli/public/oi/oi.py +15 -0
  119. mcli/public/public.py +4 -0
  120. mcli/self/self_cmd.py +1246 -0
  121. mcli/workflow/daemon/api_daemon.py +800 -0
  122. mcli/workflow/daemon/async_command_database.py +681 -0
  123. mcli/workflow/daemon/async_process_manager.py +591 -0
  124. mcli/workflow/daemon/client.py +530 -0
  125. mcli/workflow/daemon/commands.py +1196 -0
  126. mcli/workflow/daemon/daemon.py +905 -0
  127. mcli/workflow/daemon/daemon_api.py +59 -0
  128. mcli/workflow/daemon/enhanced_daemon.py +571 -0
  129. mcli/workflow/daemon/process_cli.py +244 -0
  130. mcli/workflow/daemon/process_manager.py +439 -0
  131. mcli/workflow/daemon/test_daemon.py +275 -0
  132. mcli/workflow/dashboard/dashboard_cmd.py +113 -0
  133. mcli/workflow/docker/docker.py +0 -0
  134. mcli/workflow/file/file.py +100 -0
  135. mcli/workflow/gcloud/config.toml +21 -0
  136. mcli/workflow/gcloud/gcloud.py +58 -0
  137. mcli/workflow/git_commit/ai_service.py +328 -0
  138. mcli/workflow/git_commit/commands.py +430 -0
  139. mcli/workflow/lsh_integration.py +355 -0
  140. mcli/workflow/model_service/client.py +594 -0
  141. mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
  142. mcli/workflow/model_service/lightweight_embedder.py +397 -0
  143. mcli/workflow/model_service/lightweight_model_server.py +714 -0
  144. mcli/workflow/model_service/lightweight_test.py +241 -0
  145. mcli/workflow/model_service/model_service.py +1955 -0
  146. mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
  147. mcli/workflow/model_service/pdf_processor.py +386 -0
  148. mcli/workflow/model_service/test_efficient_runner.py +234 -0
  149. mcli/workflow/model_service/test_example.py +315 -0
  150. mcli/workflow/model_service/test_integration.py +131 -0
  151. mcli/workflow/model_service/test_new_features.py +149 -0
  152. mcli/workflow/openai/openai.py +99 -0
  153. mcli/workflow/politician_trading/commands.py +1790 -0
  154. mcli/workflow/politician_trading/config.py +134 -0
  155. mcli/workflow/politician_trading/connectivity.py +490 -0
  156. mcli/workflow/politician_trading/data_sources.py +395 -0
  157. mcli/workflow/politician_trading/database.py +410 -0
  158. mcli/workflow/politician_trading/demo.py +248 -0
  159. mcli/workflow/politician_trading/models.py +165 -0
  160. mcli/workflow/politician_trading/monitoring.py +413 -0
  161. mcli/workflow/politician_trading/scrapers.py +966 -0
  162. mcli/workflow/politician_trading/scrapers_california.py +412 -0
  163. mcli/workflow/politician_trading/scrapers_eu.py +377 -0
  164. mcli/workflow/politician_trading/scrapers_uk.py +350 -0
  165. mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
  166. mcli/workflow/politician_trading/supabase_functions.py +354 -0
  167. mcli/workflow/politician_trading/workflow.py +852 -0
  168. mcli/workflow/registry/registry.py +180 -0
  169. mcli/workflow/repo/repo.py +223 -0
  170. mcli/workflow/scheduler/commands.py +493 -0
  171. mcli/workflow/scheduler/cron_parser.py +238 -0
  172. mcli/workflow/scheduler/job.py +182 -0
  173. mcli/workflow/scheduler/monitor.py +139 -0
  174. mcli/workflow/scheduler/persistence.py +324 -0
  175. mcli/workflow/scheduler/scheduler.py +679 -0
  176. mcli/workflow/sync/sync_cmd.py +437 -0
  177. mcli/workflow/sync/test_cmd.py +314 -0
  178. mcli/workflow/videos/videos.py +242 -0
  179. mcli/workflow/wakatime/wakatime.py +11 -0
  180. mcli/workflow/workflow.py +37 -0
  181. mcli_framework-7.0.0.dist-info/METADATA +479 -0
  182. mcli_framework-7.0.0.dist-info/RECORD +186 -0
  183. mcli_framework-7.0.0.dist-info/WHEEL +5 -0
  184. mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
  185. mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
  186. mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,502 @@
1
+ """Backtesting engine for trading strategies"""
2
+
3
+ import pandas as pd
4
+ import numpy as np
5
+ from typing import Dict, Any, Optional, List, Callable, Union, Tuple
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime, timedelta
8
+ from enum import Enum
9
+ import logging
10
+ from pathlib import Path
11
+ import json
12
+
13
+ import sys
14
+ import os
15
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "../.."))
16
+
17
+ from ml.models.recommendation_models import StockRecommendationModel, PortfolioRecommendation
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ class OrderType(Enum):
23
+ """Order types"""
24
+ MARKET = "market"
25
+ LIMIT = "limit"
26
+ STOP = "stop"
27
+ STOP_LIMIT = "stop_limit"
28
+
29
+
30
+ class OrderSide(Enum):
31
+ """Order side"""
32
+ BUY = "buy"
33
+ SELL = "sell"
34
+
35
+
36
+ @dataclass
37
+ class BacktestConfig:
38
+ """Backtesting configuration"""
39
+ initial_capital: float = 100000.0
40
+ start_date: Optional[datetime] = None
41
+ end_date: Optional[datetime] = None
42
+ commission: float = 0.001 # 0.1%
43
+ slippage: float = 0.001 # 0.1%
44
+ max_positions: int = 20
45
+ position_sizing: str = "equal" # equal, kelly, risk_parity
46
+ rebalance_frequency: str = "weekly" # daily, weekly, monthly
47
+ stop_loss: Optional[float] = 0.1 # 10% stop loss
48
+ take_profit: Optional[float] = 0.2 # 20% take profit
49
+ benchmark: str = "SPY"
50
+ risk_free_rate: float = 0.02 # 2% annual
51
+ verbose: bool = True
52
+
53
+
54
+ @dataclass
55
+ class BacktestResult:
56
+ """Backtesting results"""
57
+ portfolio_value: pd.Series
58
+ returns: pd.Series
59
+ positions: pd.DataFrame
60
+ trades: pd.DataFrame
61
+ metrics: Dict[str, float]
62
+ benchmark_returns: Optional[pd.Series] = None
63
+ strategy_name: str = "strategy"
64
+
65
+
66
+ class TradingStrategy:
67
+ """Base trading strategy class"""
68
+
69
+ def __init__(self, model: Optional[StockRecommendationModel] = None):
70
+ self.model = model
71
+ self.current_positions = {}
72
+ self.pending_orders = []
73
+
74
+ def generate_signals(self, data: pd.DataFrame,
75
+ current_date: datetime,
76
+ portfolio_value: float) -> List[Dict[str, Any]]:
77
+ """Generate trading signals"""
78
+ signals = []
79
+
80
+ if self.model:
81
+ # Use ML model for predictions
82
+ recommendations = self._get_model_recommendations(data, current_date)
83
+
84
+ for rec in recommendations:
85
+ signal = {
86
+ "ticker": rec.ticker,
87
+ "action": "buy" if rec.recommendation_score > 0.6 else "sell",
88
+ "confidence": rec.confidence,
89
+ "position_size": rec.position_size,
90
+ "entry_price": rec.entry_price,
91
+ "target_price": rec.target_price,
92
+ "stop_loss": rec.stop_loss
93
+ }
94
+ signals.append(signal)
95
+ else:
96
+ # Simple momentum strategy as fallback
97
+ signals = self._momentum_strategy(data, current_date)
98
+
99
+ return signals
100
+
101
+ def _get_model_recommendations(self, data: pd.DataFrame,
102
+ current_date: datetime) -> List[PortfolioRecommendation]:
103
+ """Get recommendations from ML model"""
104
+ # Filter data up to current date
105
+ historical_data = data[data['date'] <= current_date]
106
+
107
+ # Extract features (simplified)
108
+ features = historical_data.select_dtypes(include=[np.number]).values[-1:, :]
109
+
110
+ # Get unique tickers
111
+ tickers = historical_data['symbol'].unique()[:5] # Limit to 5 for speed
112
+
113
+ # Generate recommendations
114
+ try:
115
+ recommendations = self.model.generate_recommendations(features, tickers.tolist())
116
+ return recommendations
117
+ except Exception as e:
118
+ logger.warning(f"Model prediction failed: {e}")
119
+ return []
120
+
121
+ def _momentum_strategy(self, data: pd.DataFrame,
122
+ current_date: datetime) -> List[Dict[str, Any]]:
123
+ """Simple momentum strategy"""
124
+ signals = []
125
+
126
+ # Get recent data
127
+ recent_data = data[data['date'] <= current_date].tail(20)
128
+
129
+ if len(recent_data) < 20:
130
+ return signals
131
+
132
+ # Calculate momentum for each ticker
133
+ for ticker in recent_data['symbol'].unique():
134
+ ticker_data = recent_data[recent_data['symbol'] == ticker]
135
+
136
+ if len(ticker_data) < 2:
137
+ continue
138
+
139
+ # Simple momentum: compare current price to 20-day average
140
+ current_price = ticker_data['close'].iloc[-1]
141
+ avg_price = ticker_data['close'].mean()
142
+
143
+ if current_price > avg_price * 1.05: # 5% above average
144
+ signals.append({
145
+ "ticker": ticker,
146
+ "action": "buy",
147
+ "confidence": 0.6,
148
+ "position_size": 0.05 # 5% position
149
+ })
150
+ elif current_price < avg_price * 0.95: # 5% below average
151
+ signals.append({
152
+ "ticker": ticker,
153
+ "action": "sell",
154
+ "confidence": 0.6,
155
+ "position_size": 0.05
156
+ })
157
+
158
+ return signals
159
+
160
+
161
+ class PositionManager:
162
+ """Manage portfolio positions"""
163
+
164
+ def __init__(self, config: BacktestConfig):
165
+ self.config = config
166
+ self.positions: Dict[str, Dict[str, Any]] = {}
167
+ self.cash = config.initial_capital
168
+ self.portfolio_value = config.initial_capital
169
+
170
+ def open_position(self, ticker: str, quantity: int, price: float,
171
+ date: datetime, signal: Dict[str, Any]):
172
+ """Open a new position"""
173
+ cost = quantity * price * (1 + self.config.commission + self.config.slippage)
174
+
175
+ if cost > self.cash:
176
+ logger.warning(f"Insufficient cash to open position in {ticker}")
177
+ return False
178
+
179
+ self.cash -= cost
180
+ self.positions[ticker] = {
181
+ "quantity": quantity,
182
+ "entry_price": price,
183
+ "entry_date": date,
184
+ "current_price": price,
185
+ "stop_loss": signal.get("stop_loss"),
186
+ "take_profit": signal.get("target_price"),
187
+ "unrealized_pnl": 0,
188
+ "realized_pnl": 0
189
+ }
190
+
191
+ logger.debug(f"Opened position: {ticker} - {quantity} shares @ ${price:.2f}")
192
+ return True
193
+
194
+ def close_position(self, ticker: str, price: float, date: datetime) -> float:
195
+ """Close a position"""
196
+ if ticker not in self.positions:
197
+ return 0
198
+
199
+ position = self.positions[ticker]
200
+ quantity = position["quantity"]
201
+
202
+ # Calculate PnL
203
+ gross_proceeds = quantity * price
204
+ net_proceeds = gross_proceeds * (1 - self.config.commission - self.config.slippage)
205
+ entry_cost = quantity * position["entry_price"]
206
+
207
+ realized_pnl = net_proceeds - entry_cost
208
+ self.cash += net_proceeds
209
+
210
+ logger.debug(f"Closed position: {ticker} - PnL: ${realized_pnl:.2f}")
211
+
212
+ del self.positions[ticker]
213
+ return realized_pnl
214
+
215
+ def update_positions(self, price_data: Dict[str, float]):
216
+ """Update position prices and calculate unrealized PnL"""
217
+ for ticker, position in self.positions.items():
218
+ if ticker in price_data:
219
+ current_price = price_data[ticker]
220
+ position["current_price"] = current_price
221
+
222
+ # Calculate unrealized PnL
223
+ entry_cost = position["quantity"] * position["entry_price"]
224
+ current_value = position["quantity"] * current_price
225
+ position["unrealized_pnl"] = current_value - entry_cost
226
+
227
+ # Check stop loss
228
+ if position.get("stop_loss") and current_price <= position["stop_loss"]:
229
+ logger.info(f"Stop loss triggered for {ticker}")
230
+ return ticker, "stop_loss"
231
+
232
+ # Check take profit
233
+ if position.get("take_profit") and current_price >= position["take_profit"]:
234
+ logger.info(f"Take profit triggered for {ticker}")
235
+ return ticker, "take_profit"
236
+
237
+ return None, None
238
+
239
+ def get_portfolio_value(self, price_data: Dict[str, float]) -> float:
240
+ """Calculate total portfolio value"""
241
+ positions_value = sum(
242
+ pos["quantity"] * price_data.get(ticker, pos["current_price"])
243
+ for ticker, pos in self.positions.items()
244
+ )
245
+ return self.cash + positions_value
246
+
247
+ def get_position_weights(self, price_data: Dict[str, float]) -> Dict[str, float]:
248
+ """Get position weights"""
249
+ portfolio_value = self.get_portfolio_value(price_data)
250
+ weights = {}
251
+
252
+ for ticker, pos in self.positions.items():
253
+ position_value = pos["quantity"] * price_data.get(ticker, pos["current_price"])
254
+ weights[ticker] = position_value / portfolio_value
255
+
256
+ weights["cash"] = self.cash / portfolio_value
257
+ return weights
258
+
259
+
260
+ class BacktestEngine:
261
+ """Main backtesting engine"""
262
+
263
+ def __init__(self, config: BacktestConfig):
264
+ self.config = config
265
+ self.position_manager = PositionManager(config)
266
+ self.trades = []
267
+ self.portfolio_history = []
268
+ self.strategy = None
269
+
270
+ def set_strategy(self, strategy: TradingStrategy):
271
+ """Set trading strategy"""
272
+ self.strategy = strategy
273
+
274
+ def run(self, price_data: pd.DataFrame,
275
+ trading_data: Optional[pd.DataFrame] = None) -> BacktestResult:
276
+ """Run backtest"""
277
+ logger.info("Starting backtest...")
278
+
279
+ # Prepare data
280
+ price_data = price_data.sort_values('date')
281
+
282
+ if self.config.start_date:
283
+ price_data = price_data[price_data['date'] >= self.config.start_date]
284
+ if self.config.end_date:
285
+ price_data = price_data[price_data['date'] <= self.config.end_date]
286
+
287
+ # Get unique dates
288
+ dates = price_data['date'].unique()
289
+
290
+ # Initialize results
291
+ portfolio_values = []
292
+ daily_positions = []
293
+
294
+ # Main backtest loop
295
+ for i, current_date in enumerate(dates):
296
+ # Get current prices
297
+ current_prices = self._get_current_prices(price_data, current_date)
298
+
299
+ # Update positions with current prices
300
+ trigger_ticker, trigger_type = self.position_manager.update_positions(current_prices)
301
+
302
+ # Handle stop loss or take profit triggers
303
+ if trigger_ticker:
304
+ self._execute_exit(trigger_ticker, current_prices[trigger_ticker],
305
+ current_date, trigger_type)
306
+
307
+ # Generate signals (e.g., weekly rebalancing)
308
+ if self._should_rebalance(i, current_date):
309
+ signals = self.strategy.generate_signals(
310
+ price_data[price_data['date'] <= current_date],
311
+ current_date,
312
+ self.position_manager.get_portfolio_value(current_prices)
313
+ )
314
+
315
+ # Execute signals
316
+ self._execute_signals(signals, current_prices, current_date)
317
+
318
+ # Record portfolio value
319
+ portfolio_value = self.position_manager.get_portfolio_value(current_prices)
320
+ portfolio_values.append({
321
+ "date": current_date,
322
+ "value": portfolio_value,
323
+ "cash": self.position_manager.cash,
324
+ "positions_value": portfolio_value - self.position_manager.cash
325
+ })
326
+
327
+ # Record positions
328
+ position_snapshot = self._get_position_snapshot(current_date, current_prices)
329
+ daily_positions.append(position_snapshot)
330
+
331
+ # Create results
332
+ portfolio_df = pd.DataFrame(portfolio_values)
333
+ portfolio_df.set_index('date', inplace=True)
334
+
335
+ # Calculate returns
336
+ portfolio_df['returns'] = portfolio_df['value'].pct_change().fillna(0)
337
+
338
+ # Calculate metrics
339
+ metrics = self._calculate_metrics(portfolio_df)
340
+
341
+ # Get benchmark returns if available
342
+ benchmark_returns = self._get_benchmark_returns(price_data, dates)
343
+
344
+ result = BacktestResult(
345
+ portfolio_value=portfolio_df['value'],
346
+ returns=portfolio_df['returns'],
347
+ positions=pd.DataFrame(daily_positions),
348
+ trades=pd.DataFrame(self.trades),
349
+ metrics=metrics,
350
+ benchmark_returns=benchmark_returns,
351
+ strategy_name=self.strategy.__class__.__name__
352
+ )
353
+
354
+ logger.info(f"Backtest complete. Total return: {metrics['total_return']:.2%}")
355
+
356
+ return result
357
+
358
+ def _get_current_prices(self, price_data: pd.DataFrame,
359
+ current_date: datetime) -> Dict[str, float]:
360
+ """Get current prices for all tickers"""
361
+ current_data = price_data[price_data['date'] == current_date]
362
+ return dict(zip(current_data['symbol'], current_data['close']))
363
+
364
+ def _should_rebalance(self, day_index: int, current_date: datetime) -> bool:
365
+ """Check if should rebalance portfolio"""
366
+ if self.config.rebalance_frequency == "daily":
367
+ return True
368
+ elif self.config.rebalance_frequency == "weekly":
369
+ return day_index % 5 == 0
370
+ elif self.config.rebalance_frequency == "monthly":
371
+ return day_index % 21 == 0
372
+ return False
373
+
374
+ def _execute_signals(self, signals: List[Dict[str, Any]],
375
+ current_prices: Dict[str, float],
376
+ current_date: datetime):
377
+ """Execute trading signals"""
378
+ for signal in signals:
379
+ ticker = signal["ticker"]
380
+
381
+ if ticker not in current_prices:
382
+ continue
383
+
384
+ price = current_prices[ticker]
385
+
386
+ if signal["action"] == "buy":
387
+ # Calculate position size
388
+ portfolio_value = self.position_manager.get_portfolio_value(current_prices)
389
+ position_value = portfolio_value * signal.get("position_size", 0.05)
390
+ quantity = int(position_value / price)
391
+
392
+ if quantity > 0:
393
+ # Check if already have position
394
+ if ticker not in self.position_manager.positions:
395
+ # Check max positions
396
+ if len(self.position_manager.positions) < self.config.max_positions:
397
+ success = self.position_manager.open_position(
398
+ ticker, quantity, price, current_date, signal
399
+ )
400
+
401
+ if success:
402
+ self.trades.append({
403
+ "date": current_date,
404
+ "ticker": ticker,
405
+ "action": "buy",
406
+ "quantity": quantity,
407
+ "price": price,
408
+ "value": quantity * price
409
+ })
410
+
411
+ elif signal["action"] == "sell":
412
+ if ticker in self.position_manager.positions:
413
+ pnl = self.position_manager.close_position(ticker, price, current_date)
414
+
415
+ self.trades.append({
416
+ "date": current_date,
417
+ "ticker": ticker,
418
+ "action": "sell",
419
+ "quantity": self.position_manager.positions.get(ticker, {}).get("quantity", 0),
420
+ "price": price,
421
+ "pnl": pnl
422
+ })
423
+
424
+ def _execute_exit(self, ticker: str, price: float,
425
+ current_date: datetime, exit_type: str):
426
+ """Execute position exit"""
427
+ if ticker in self.position_manager.positions:
428
+ position = self.position_manager.positions[ticker]
429
+ pnl = self.position_manager.close_position(ticker, price, current_date)
430
+
431
+ self.trades.append({
432
+ "date": current_date,
433
+ "ticker": ticker,
434
+ "action": f"sell_{exit_type}",
435
+ "quantity": position["quantity"],
436
+ "price": price,
437
+ "pnl": pnl
438
+ })
439
+
440
+ def _get_position_snapshot(self, date: datetime,
441
+ current_prices: Dict[str, float]) -> Dict[str, Any]:
442
+ """Get current position snapshot"""
443
+ snapshot = {
444
+ "date": date,
445
+ "num_positions": len(self.position_manager.positions),
446
+ "cash": self.position_manager.cash,
447
+ "portfolio_value": self.position_manager.get_portfolio_value(current_prices)
448
+ }
449
+
450
+ # Add position weights
451
+ weights = self.position_manager.get_position_weights(current_prices)
452
+ for ticker, weight in weights.items():
453
+ snapshot[f"weight_{ticker}"] = weight
454
+
455
+ return snapshot
456
+
457
+ def _calculate_metrics(self, portfolio_df: pd.DataFrame) -> Dict[str, float]:
458
+ """Calculate performance metrics"""
459
+ returns = portfolio_df['returns']
460
+
461
+ # Basic metrics
462
+ total_return = (portfolio_df['value'].iloc[-1] / portfolio_df['value'].iloc[0]) - 1
463
+ annualized_return = (1 + total_return) ** (252 / len(returns)) - 1
464
+
465
+ # Risk metrics
466
+ volatility = returns.std() * np.sqrt(252)
467
+ sharpe_ratio = (annualized_return - self.config.risk_free_rate) / volatility if volatility > 0 else 0
468
+
469
+ # Drawdown
470
+ cumulative = (1 + returns).cumprod()
471
+ running_max = cumulative.expanding().max()
472
+ drawdown = (cumulative - running_max) / running_max
473
+ max_drawdown = drawdown.min()
474
+
475
+ # Win rate
476
+ winning_trades = len([t for t in self.trades if t.get("pnl", 0) > 0])
477
+ total_trades = len([t for t in self.trades if "pnl" in t])
478
+ win_rate = winning_trades / total_trades if total_trades > 0 else 0
479
+
480
+ return {
481
+ "total_return": total_return,
482
+ "annualized_return": annualized_return,
483
+ "volatility": volatility,
484
+ "sharpe_ratio": sharpe_ratio,
485
+ "max_drawdown": max_drawdown,
486
+ "win_rate": win_rate,
487
+ "total_trades": len(self.trades),
488
+ "final_value": portfolio_df['value'].iloc[-1],
489
+ "initial_value": self.config.initial_capital
490
+ }
491
+
492
+ def _get_benchmark_returns(self, price_data: pd.DataFrame,
493
+ dates: np.ndarray) -> Optional[pd.Series]:
494
+ """Get benchmark returns"""
495
+ if self.config.benchmark not in price_data['symbol'].unique():
496
+ return None
497
+
498
+ benchmark_data = price_data[price_data['symbol'] == self.config.benchmark]
499
+ benchmark_data = benchmark_data.set_index('date')['close']
500
+ benchmark_returns = benchmark_data.pct_change().fillna(0)
501
+
502
+ return benchmark_returns[benchmark_returns.index.isin(dates)]