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,393 @@
1
+ """Performance metrics and analysis for backtesting"""
2
+
3
+ import pandas as pd
4
+ import numpy as np
5
+ from typing import Dict, Any, Optional, List, Tuple
6
+ from dataclasses import dataclass
7
+ import matplotlib.pyplot as plt
8
+ import seaborn as sns
9
+ from datetime import datetime
10
+ import logging
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ @dataclass
16
+ class PortfolioMetrics:
17
+ """Portfolio performance metrics"""
18
+ total_return: float
19
+ annualized_return: float
20
+ volatility: float
21
+ sharpe_ratio: float
22
+ sortino_ratio: float
23
+ calmar_ratio: float
24
+ max_drawdown: float
25
+ max_drawdown_duration: int
26
+ win_rate: float
27
+ profit_factor: float
28
+ avg_win: float
29
+ avg_loss: float
30
+ largest_win: float
31
+ largest_loss: float
32
+ consecutive_wins: int
33
+ consecutive_losses: int
34
+ recovery_factor: float
35
+ payoff_ratio: float
36
+
37
+
38
+ @dataclass
39
+ class RiskMetrics:
40
+ """Risk metrics"""
41
+ value_at_risk_95: float
42
+ conditional_var_95: float
43
+ value_at_risk_99: float
44
+ conditional_var_99: float
45
+ beta: float
46
+ alpha: float
47
+ correlation: float
48
+ information_ratio: float
49
+ treynor_ratio: float
50
+ downside_deviation: float
51
+ upside_capture: float
52
+ downside_capture: float
53
+
54
+
55
+ class PerformanceAnalyzer:
56
+ """Analyze backtest performance"""
57
+
58
+ def __init__(self, risk_free_rate: float = 0.02):
59
+ self.risk_free_rate = risk_free_rate
60
+
61
+ def calculate_metrics(self, returns: pd.Series,
62
+ benchmark_returns: Optional[pd.Series] = None,
63
+ trades: Optional[pd.DataFrame] = None) -> Tuple[PortfolioMetrics, RiskMetrics]:
64
+ """Calculate comprehensive performance metrics"""
65
+
66
+ # Portfolio metrics
67
+ portfolio_metrics = self._calculate_portfolio_metrics(returns, trades)
68
+
69
+ # Risk metrics
70
+ risk_metrics = self._calculate_risk_metrics(returns, benchmark_returns)
71
+
72
+ return portfolio_metrics, risk_metrics
73
+
74
+ def _calculate_portfolio_metrics(self, returns: pd.Series,
75
+ trades: Optional[pd.DataFrame] = None) -> PortfolioMetrics:
76
+ """Calculate portfolio performance metrics"""
77
+
78
+ # Basic returns
79
+ total_return = (1 + returns).prod() - 1
80
+ annualized_return = (1 + total_return) ** (252 / len(returns)) - 1
81
+
82
+ # Volatility
83
+ volatility = returns.std() * np.sqrt(252)
84
+
85
+ # Sharpe ratio
86
+ excess_returns = returns - self.risk_free_rate / 252
87
+ sharpe_ratio = excess_returns.mean() / returns.std() * np.sqrt(252) if returns.std() > 0 else 0
88
+
89
+ # Sortino ratio (downside deviation)
90
+ downside_returns = returns[returns < 0]
91
+ downside_std = downside_returns.std() * np.sqrt(252)
92
+ sortino_ratio = (annualized_return - self.risk_free_rate) / downside_std if downside_std > 0 else 0
93
+
94
+ # Drawdown analysis
95
+ cumulative = (1 + returns).cumprod()
96
+ running_max = cumulative.expanding().max()
97
+ drawdown = (cumulative - running_max) / running_max
98
+
99
+ max_drawdown = drawdown.min()
100
+ max_dd_duration = self._calculate_max_drawdown_duration(drawdown)
101
+
102
+ # Calmar ratio
103
+ calmar_ratio = annualized_return / abs(max_drawdown) if max_drawdown != 0 else 0
104
+
105
+ # Trade analysis
106
+ if trades is not None and len(trades) > 0:
107
+ trade_metrics = self._analyze_trades(trades)
108
+ else:
109
+ trade_metrics = {
110
+ 'win_rate': 0.5,
111
+ 'profit_factor': 1.0,
112
+ 'avg_win': 0,
113
+ 'avg_loss': 0,
114
+ 'largest_win': 0,
115
+ 'largest_loss': 0,
116
+ 'consecutive_wins': 0,
117
+ 'consecutive_losses': 0,
118
+ 'payoff_ratio': 1.0
119
+ }
120
+
121
+ # Recovery factor
122
+ recovery_factor = total_return / abs(max_drawdown) if max_drawdown != 0 else 0
123
+
124
+ return PortfolioMetrics(
125
+ total_return=total_return,
126
+ annualized_return=annualized_return,
127
+ volatility=volatility,
128
+ sharpe_ratio=sharpe_ratio,
129
+ sortino_ratio=sortino_ratio,
130
+ calmar_ratio=calmar_ratio,
131
+ max_drawdown=max_drawdown,
132
+ max_drawdown_duration=max_dd_duration,
133
+ recovery_factor=recovery_factor,
134
+ **trade_metrics
135
+ )
136
+
137
+ def _calculate_risk_metrics(self, returns: pd.Series,
138
+ benchmark_returns: Optional[pd.Series] = None) -> RiskMetrics:
139
+ """Calculate risk metrics"""
140
+
141
+ # Value at Risk (VaR)
142
+ var_95 = np.percentile(returns, 5)
143
+ var_99 = np.percentile(returns, 1)
144
+
145
+ # Conditional VaR (CVaR) or Expected Shortfall
146
+ cvar_95 = returns[returns <= var_95].mean()
147
+ cvar_99 = returns[returns <= var_99].mean()
148
+
149
+ # Market risk metrics
150
+ if benchmark_returns is not None and len(benchmark_returns) > 0:
151
+ # Align series
152
+ aligned = pd.DataFrame({'returns': returns, 'benchmark': benchmark_returns}).dropna()
153
+
154
+ if len(aligned) > 1:
155
+ # Beta
156
+ covariance = aligned.cov()
157
+ beta = covariance.loc['returns', 'benchmark'] / aligned['benchmark'].var()
158
+
159
+ # Alpha
160
+ alpha = aligned['returns'].mean() - beta * aligned['benchmark'].mean()
161
+ alpha = alpha * 252 # Annualize
162
+
163
+ # Correlation
164
+ correlation = aligned.corr().loc['returns', 'benchmark']
165
+
166
+ # Information ratio
167
+ active_returns = aligned['returns'] - aligned['benchmark']
168
+ tracking_error = active_returns.std() * np.sqrt(252)
169
+ information_ratio = active_returns.mean() * 252 / tracking_error if tracking_error > 0 else 0
170
+
171
+ # Treynor ratio
172
+ treynor_ratio = (aligned['returns'].mean() * 252 - self.risk_free_rate) / beta if beta != 0 else 0
173
+
174
+ # Capture ratios
175
+ up_market = aligned[aligned['benchmark'] > 0]
176
+ down_market = aligned[aligned['benchmark'] < 0]
177
+
178
+ upside_capture = (up_market['returns'].mean() / up_market['benchmark'].mean()
179
+ if len(up_market) > 0 and up_market['benchmark'].mean() != 0 else 1.0)
180
+
181
+ downside_capture = (down_market['returns'].mean() / down_market['benchmark'].mean()
182
+ if len(down_market) > 0 and down_market['benchmark'].mean() != 0 else 1.0)
183
+ else:
184
+ beta = alpha = correlation = information_ratio = treynor_ratio = 0
185
+ upside_capture = downside_capture = 1.0
186
+ else:
187
+ beta = alpha = correlation = information_ratio = treynor_ratio = 0
188
+ upside_capture = downside_capture = 1.0
189
+
190
+ # Downside deviation
191
+ downside_returns = returns[returns < 0]
192
+ downside_deviation = downside_returns.std() * np.sqrt(252)
193
+
194
+ return RiskMetrics(
195
+ value_at_risk_95=var_95,
196
+ conditional_var_95=cvar_95,
197
+ value_at_risk_99=var_99,
198
+ conditional_var_99=cvar_99,
199
+ beta=beta,
200
+ alpha=alpha,
201
+ correlation=correlation,
202
+ information_ratio=information_ratio,
203
+ treynor_ratio=treynor_ratio,
204
+ downside_deviation=downside_deviation,
205
+ upside_capture=upside_capture,
206
+ downside_capture=downside_capture
207
+ )
208
+
209
+ def _calculate_max_drawdown_duration(self, drawdown: pd.Series) -> int:
210
+ """Calculate maximum drawdown duration in days"""
211
+ is_drawdown = drawdown < 0
212
+ drawdown_periods = []
213
+ current_duration = 0
214
+
215
+ for is_dd in is_drawdown:
216
+ if is_dd:
217
+ current_duration += 1
218
+ else:
219
+ if current_duration > 0:
220
+ drawdown_periods.append(current_duration)
221
+ current_duration = 0
222
+
223
+ if current_duration > 0:
224
+ drawdown_periods.append(current_duration)
225
+
226
+ return max(drawdown_periods) if drawdown_periods else 0
227
+
228
+ def _analyze_trades(self, trades: pd.DataFrame) -> Dict[str, float]:
229
+ """Analyze trade statistics"""
230
+ # Filter for trades with PnL
231
+ pnl_trades = trades[trades['pnl'].notna()].copy()
232
+
233
+ if len(pnl_trades) == 0:
234
+ return {
235
+ 'win_rate': 0.5,
236
+ 'profit_factor': 1.0,
237
+ 'avg_win': 0,
238
+ 'avg_loss': 0,
239
+ 'largest_win': 0,
240
+ 'largest_loss': 0,
241
+ 'consecutive_wins': 0,
242
+ 'consecutive_losses': 0,
243
+ 'payoff_ratio': 1.0
244
+ }
245
+
246
+ # Winning and losing trades
247
+ winning_trades = pnl_trades[pnl_trades['pnl'] > 0]
248
+ losing_trades = pnl_trades[pnl_trades['pnl'] < 0]
249
+
250
+ # Win rate
251
+ win_rate = len(winning_trades) / len(pnl_trades)
252
+
253
+ # Average win/loss
254
+ avg_win = winning_trades['pnl'].mean() if len(winning_trades) > 0 else 0
255
+ avg_loss = abs(losing_trades['pnl'].mean()) if len(losing_trades) > 0 else 0
256
+
257
+ # Profit factor
258
+ gross_profit = winning_trades['pnl'].sum() if len(winning_trades) > 0 else 0
259
+ gross_loss = abs(losing_trades['pnl'].sum()) if len(losing_trades) > 0 else 1
260
+ profit_factor = gross_profit / gross_loss if gross_loss != 0 else 0
261
+
262
+ # Largest win/loss
263
+ largest_win = winning_trades['pnl'].max() if len(winning_trades) > 0 else 0
264
+ largest_loss = abs(losing_trades['pnl'].min()) if len(losing_trades) > 0 else 0
265
+
266
+ # Consecutive wins/losses
267
+ pnl_trades['is_win'] = pnl_trades['pnl'] > 0
268
+ consecutive_wins = self._max_consecutive(pnl_trades['is_win'].values, True)
269
+ consecutive_losses = self._max_consecutive(pnl_trades['is_win'].values, False)
270
+
271
+ # Payoff ratio
272
+ payoff_ratio = avg_win / avg_loss if avg_loss > 0 else 0
273
+
274
+ return {
275
+ 'win_rate': win_rate,
276
+ 'profit_factor': profit_factor,
277
+ 'avg_win': avg_win,
278
+ 'avg_loss': avg_loss,
279
+ 'largest_win': largest_win,
280
+ 'largest_loss': largest_loss,
281
+ 'consecutive_wins': consecutive_wins,
282
+ 'consecutive_losses': consecutive_losses,
283
+ 'payoff_ratio': payoff_ratio
284
+ }
285
+
286
+ def _max_consecutive(self, arr: np.ndarray, value: bool) -> int:
287
+ """Calculate maximum consecutive occurrences of value"""
288
+ max_count = 0
289
+ current_count = 0
290
+
291
+ for val in arr:
292
+ if val == value:
293
+ current_count += 1
294
+ max_count = max(max_count, current_count)
295
+ else:
296
+ current_count = 0
297
+
298
+ return max_count
299
+
300
+
301
+ def plot_performance(backtest_result, save_path: Optional[str] = None):
302
+ """Plot backtest performance charts"""
303
+ fig, axes = plt.subplots(3, 2, figsize=(15, 12))
304
+
305
+ # Portfolio value
306
+ ax = axes[0, 0]
307
+ ax.plot(backtest_result.portfolio_value.index,
308
+ backtest_result.portfolio_value.values, label='Portfolio')
309
+ if backtest_result.benchmark_returns is not None:
310
+ benchmark_cumulative = (1 + backtest_result.benchmark_returns).cumprod()
311
+ benchmark_value = benchmark_cumulative * backtest_result.portfolio_value.iloc[0]
312
+ ax.plot(benchmark_value.index, benchmark_value.values,
313
+ label='Benchmark', alpha=0.7)
314
+ ax.set_title('Portfolio Value')
315
+ ax.set_xlabel('Date')
316
+ ax.set_ylabel('Value ($)')
317
+ ax.legend()
318
+ ax.grid(True, alpha=0.3)
319
+
320
+ # Returns distribution
321
+ ax = axes[0, 1]
322
+ ax.hist(backtest_result.returns.values * 100, bins=50, edgecolor='black')
323
+ ax.set_title('Returns Distribution')
324
+ ax.set_xlabel('Daily Return (%)')
325
+ ax.set_ylabel('Frequency')
326
+ ax.axvline(x=0, color='red', linestyle='--', alpha=0.5)
327
+ ax.grid(True, alpha=0.3)
328
+
329
+ # Drawdown
330
+ ax = axes[1, 0]
331
+ cumulative = (1 + backtest_result.returns).cumprod()
332
+ running_max = cumulative.expanding().max()
333
+ drawdown = ((cumulative - running_max) / running_max) * 100
334
+ ax.fill_between(drawdown.index, drawdown.values, 0, color='red', alpha=0.3)
335
+ ax.set_title('Drawdown')
336
+ ax.set_xlabel('Date')
337
+ ax.set_ylabel('Drawdown (%)')
338
+ ax.grid(True, alpha=0.3)
339
+
340
+ # Rolling Sharpe Ratio
341
+ ax = axes[1, 1]
342
+ rolling_sharpe = (
343
+ backtest_result.returns.rolling(window=60).mean() /
344
+ backtest_result.returns.rolling(window=60).std() * np.sqrt(252)
345
+ )
346
+ ax.plot(rolling_sharpe.index, rolling_sharpe.values)
347
+ ax.set_title('Rolling Sharpe Ratio (60 days)')
348
+ ax.set_xlabel('Date')
349
+ ax.set_ylabel('Sharpe Ratio')
350
+ ax.axhline(y=0, color='red', linestyle='--', alpha=0.5)
351
+ ax.grid(True, alpha=0.3)
352
+
353
+ # Trade analysis
354
+ ax = axes[2, 0]
355
+ if not backtest_result.trades.empty and 'pnl' in backtest_result.trades.columns:
356
+ pnl_trades = backtest_result.trades[backtest_result.trades['pnl'].notna()]
357
+ if not pnl_trades.empty:
358
+ colors = ['green' if pnl > 0 else 'red' for pnl in pnl_trades['pnl']]
359
+ ax.bar(range(len(pnl_trades)), pnl_trades['pnl'].values, color=colors, alpha=0.6)
360
+ ax.set_title('Trade PnL')
361
+ ax.set_xlabel('Trade Number')
362
+ ax.set_ylabel('PnL ($)')
363
+ ax.axhline(y=0, color='black', linestyle='-', alpha=0.3)
364
+ else:
365
+ ax.text(0.5, 0.5, 'No trades', ha='center', va='center')
366
+ ax.set_title('Trade PnL')
367
+ ax.grid(True, alpha=0.3)
368
+
369
+ # Metrics summary
370
+ ax = axes[2, 1]
371
+ ax.axis('off')
372
+ metrics_text = f"""
373
+ Performance Metrics:
374
+ ─────────────────
375
+ Total Return: {backtest_result.metrics['total_return']:.2%}
376
+ Annual Return: {backtest_result.metrics['annualized_return']:.2%}
377
+ Volatility: {backtest_result.metrics['volatility']:.2%}
378
+ Sharpe Ratio: {backtest_result.metrics['sharpe_ratio']:.2f}
379
+ Max Drawdown: {backtest_result.metrics['max_drawdown']:.2%}
380
+ Win Rate: {backtest_result.metrics['win_rate']:.2%}
381
+ Total Trades: {backtest_result.metrics['total_trades']}
382
+ """
383
+ ax.text(0.1, 0.9, metrics_text, transform=ax.transAxes,
384
+ fontsize=10, verticalalignment='top', fontfamily='monospace')
385
+
386
+ plt.suptitle(f'Backtest Results - {backtest_result.strategy_name}', fontsize=14)
387
+ plt.tight_layout()
388
+
389
+ if save_path:
390
+ plt.savefig(save_path, dpi=100, bbox_inches='tight')
391
+ logger.info(f"Performance chart saved to {save_path}")
392
+
393
+ return fig