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.
- mcli/app/chat_cmd.py +42 -0
- mcli/app/commands_cmd.py +226 -0
- mcli/app/completion_cmd.py +216 -0
- mcli/app/completion_helpers.py +288 -0
- mcli/app/cron_test_cmd.py +697 -0
- mcli/app/logs_cmd.py +419 -0
- mcli/app/main.py +492 -0
- mcli/app/model/model.py +1060 -0
- mcli/app/model_cmd.py +227 -0
- mcli/app/redis_cmd.py +269 -0
- mcli/app/video/video.py +1114 -0
- mcli/app/visual_cmd.py +303 -0
- mcli/chat/chat.py +2409 -0
- mcli/chat/command_rag.py +514 -0
- mcli/chat/enhanced_chat.py +652 -0
- mcli/chat/system_controller.py +1010 -0
- mcli/chat/system_integration.py +1016 -0
- mcli/cli.py +25 -0
- mcli/config.toml +20 -0
- mcli/lib/api/api.py +586 -0
- mcli/lib/api/daemon_client.py +203 -0
- mcli/lib/api/daemon_client_local.py +44 -0
- mcli/lib/api/daemon_decorator.py +217 -0
- mcli/lib/api/mcli_decorators.py +1032 -0
- mcli/lib/auth/auth.py +85 -0
- mcli/lib/auth/aws_manager.py +85 -0
- mcli/lib/auth/azure_manager.py +91 -0
- mcli/lib/auth/credential_manager.py +192 -0
- mcli/lib/auth/gcp_manager.py +93 -0
- mcli/lib/auth/key_manager.py +117 -0
- mcli/lib/auth/mcli_manager.py +93 -0
- mcli/lib/auth/token_manager.py +75 -0
- mcli/lib/auth/token_util.py +1011 -0
- mcli/lib/config/config.py +47 -0
- mcli/lib/discovery/__init__.py +1 -0
- mcli/lib/discovery/command_discovery.py +274 -0
- mcli/lib/erd/erd.py +1345 -0
- mcli/lib/erd/generate_graph.py +453 -0
- mcli/lib/files/files.py +76 -0
- mcli/lib/fs/fs.py +109 -0
- mcli/lib/lib.py +29 -0
- mcli/lib/logger/logger.py +611 -0
- mcli/lib/performance/optimizer.py +409 -0
- mcli/lib/performance/rust_bridge.py +502 -0
- mcli/lib/performance/uvloop_config.py +154 -0
- mcli/lib/pickles/pickles.py +50 -0
- mcli/lib/search/cached_vectorizer.py +479 -0
- mcli/lib/services/data_pipeline.py +460 -0
- mcli/lib/services/lsh_client.py +441 -0
- mcli/lib/services/redis_service.py +387 -0
- mcli/lib/shell/shell.py +137 -0
- mcli/lib/toml/toml.py +33 -0
- mcli/lib/ui/styling.py +47 -0
- mcli/lib/ui/visual_effects.py +634 -0
- mcli/lib/watcher/watcher.py +185 -0
- mcli/ml/api/app.py +215 -0
- mcli/ml/api/middleware.py +224 -0
- mcli/ml/api/routers/admin_router.py +12 -0
- mcli/ml/api/routers/auth_router.py +244 -0
- mcli/ml/api/routers/backtest_router.py +12 -0
- mcli/ml/api/routers/data_router.py +12 -0
- mcli/ml/api/routers/model_router.py +302 -0
- mcli/ml/api/routers/monitoring_router.py +12 -0
- mcli/ml/api/routers/portfolio_router.py +12 -0
- mcli/ml/api/routers/prediction_router.py +267 -0
- mcli/ml/api/routers/trade_router.py +12 -0
- mcli/ml/api/routers/websocket_router.py +76 -0
- mcli/ml/api/schemas.py +64 -0
- mcli/ml/auth/auth_manager.py +425 -0
- mcli/ml/auth/models.py +154 -0
- mcli/ml/auth/permissions.py +302 -0
- mcli/ml/backtesting/backtest_engine.py +502 -0
- mcli/ml/backtesting/performance_metrics.py +393 -0
- mcli/ml/cache.py +400 -0
- mcli/ml/cli/main.py +398 -0
- mcli/ml/config/settings.py +394 -0
- mcli/ml/configs/dvc_config.py +230 -0
- mcli/ml/configs/mlflow_config.py +131 -0
- mcli/ml/configs/mlops_manager.py +293 -0
- mcli/ml/dashboard/app.py +532 -0
- mcli/ml/dashboard/app_integrated.py +738 -0
- mcli/ml/dashboard/app_supabase.py +560 -0
- mcli/ml/dashboard/app_training.py +615 -0
- mcli/ml/dashboard/cli.py +51 -0
- mcli/ml/data_ingestion/api_connectors.py +501 -0
- mcli/ml/data_ingestion/data_pipeline.py +567 -0
- mcli/ml/data_ingestion/stream_processor.py +512 -0
- mcli/ml/database/migrations/env.py +94 -0
- mcli/ml/database/models.py +667 -0
- mcli/ml/database/session.py +200 -0
- mcli/ml/experimentation/ab_testing.py +845 -0
- mcli/ml/features/ensemble_features.py +607 -0
- mcli/ml/features/political_features.py +676 -0
- mcli/ml/features/recommendation_engine.py +809 -0
- mcli/ml/features/stock_features.py +573 -0
- mcli/ml/features/test_feature_engineering.py +346 -0
- mcli/ml/logging.py +85 -0
- mcli/ml/mlops/data_versioning.py +518 -0
- mcli/ml/mlops/experiment_tracker.py +377 -0
- mcli/ml/mlops/model_serving.py +481 -0
- mcli/ml/mlops/pipeline_orchestrator.py +614 -0
- mcli/ml/models/base_models.py +324 -0
- mcli/ml/models/ensemble_models.py +675 -0
- mcli/ml/models/recommendation_models.py +474 -0
- mcli/ml/models/test_models.py +487 -0
- mcli/ml/monitoring/drift_detection.py +676 -0
- mcli/ml/monitoring/metrics.py +45 -0
- mcli/ml/optimization/portfolio_optimizer.py +834 -0
- mcli/ml/preprocessing/data_cleaners.py +451 -0
- mcli/ml/preprocessing/feature_extractors.py +491 -0
- mcli/ml/preprocessing/ml_pipeline.py +382 -0
- mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
- mcli/ml/preprocessing/test_preprocessing.py +294 -0
- mcli/ml/scripts/populate_sample_data.py +200 -0
- mcli/ml/tasks.py +400 -0
- mcli/ml/tests/test_integration.py +429 -0
- mcli/ml/tests/test_training_dashboard.py +387 -0
- mcli/public/oi/oi.py +15 -0
- mcli/public/public.py +4 -0
- mcli/self/self_cmd.py +1246 -0
- mcli/workflow/daemon/api_daemon.py +800 -0
- mcli/workflow/daemon/async_command_database.py +681 -0
- mcli/workflow/daemon/async_process_manager.py +591 -0
- mcli/workflow/daemon/client.py +530 -0
- mcli/workflow/daemon/commands.py +1196 -0
- mcli/workflow/daemon/daemon.py +905 -0
- mcli/workflow/daemon/daemon_api.py +59 -0
- mcli/workflow/daemon/enhanced_daemon.py +571 -0
- mcli/workflow/daemon/process_cli.py +244 -0
- mcli/workflow/daemon/process_manager.py +439 -0
- mcli/workflow/daemon/test_daemon.py +275 -0
- mcli/workflow/dashboard/dashboard_cmd.py +113 -0
- mcli/workflow/docker/docker.py +0 -0
- mcli/workflow/file/file.py +100 -0
- mcli/workflow/gcloud/config.toml +21 -0
- mcli/workflow/gcloud/gcloud.py +58 -0
- mcli/workflow/git_commit/ai_service.py +328 -0
- mcli/workflow/git_commit/commands.py +430 -0
- mcli/workflow/lsh_integration.py +355 -0
- mcli/workflow/model_service/client.py +594 -0
- mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
- mcli/workflow/model_service/lightweight_embedder.py +397 -0
- mcli/workflow/model_service/lightweight_model_server.py +714 -0
- mcli/workflow/model_service/lightweight_test.py +241 -0
- mcli/workflow/model_service/model_service.py +1955 -0
- mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
- mcli/workflow/model_service/pdf_processor.py +386 -0
- mcli/workflow/model_service/test_efficient_runner.py +234 -0
- mcli/workflow/model_service/test_example.py +315 -0
- mcli/workflow/model_service/test_integration.py +131 -0
- mcli/workflow/model_service/test_new_features.py +149 -0
- mcli/workflow/openai/openai.py +99 -0
- mcli/workflow/politician_trading/commands.py +1790 -0
- mcli/workflow/politician_trading/config.py +134 -0
- mcli/workflow/politician_trading/connectivity.py +490 -0
- mcli/workflow/politician_trading/data_sources.py +395 -0
- mcli/workflow/politician_trading/database.py +410 -0
- mcli/workflow/politician_trading/demo.py +248 -0
- mcli/workflow/politician_trading/models.py +165 -0
- mcli/workflow/politician_trading/monitoring.py +413 -0
- mcli/workflow/politician_trading/scrapers.py +966 -0
- mcli/workflow/politician_trading/scrapers_california.py +412 -0
- mcli/workflow/politician_trading/scrapers_eu.py +377 -0
- mcli/workflow/politician_trading/scrapers_uk.py +350 -0
- mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
- mcli/workflow/politician_trading/supabase_functions.py +354 -0
- mcli/workflow/politician_trading/workflow.py +852 -0
- mcli/workflow/registry/registry.py +180 -0
- mcli/workflow/repo/repo.py +223 -0
- mcli/workflow/scheduler/commands.py +493 -0
- mcli/workflow/scheduler/cron_parser.py +238 -0
- mcli/workflow/scheduler/job.py +182 -0
- mcli/workflow/scheduler/monitor.py +139 -0
- mcli/workflow/scheduler/persistence.py +324 -0
- mcli/workflow/scheduler/scheduler.py +679 -0
- mcli/workflow/sync/sync_cmd.py +437 -0
- mcli/workflow/sync/test_cmd.py +314 -0
- mcli/workflow/videos/videos.py +242 -0
- mcli/workflow/wakatime/wakatime.py +11 -0
- mcli/workflow/workflow.py +37 -0
- mcli_framework-7.0.0.dist-info/METADATA +479 -0
- mcli_framework-7.0.0.dist-info/RECORD +186 -0
- mcli_framework-7.0.0.dist-info/WHEEL +5 -0
- mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
- mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
- 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
|