fund-cli 2.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.
Files changed (66) hide show
  1. fund_cli/__init__.py +13 -0
  2. fund_cli/__main__.py +10 -0
  3. fund_cli/ai/__init__.py +21 -0
  4. fund_cli/ai/analyzer.py +360 -0
  5. fund_cli/ai/prompts.py +244 -0
  6. fund_cli/ai/providers.py +286 -0
  7. fund_cli/analysis/__init__.py +17 -0
  8. fund_cli/analysis/attribution.py +161 -0
  9. fund_cli/analysis/backtest.py +75 -0
  10. fund_cli/analysis/holding.py +217 -0
  11. fund_cli/analysis/manager.py +133 -0
  12. fund_cli/analysis/performance.py +440 -0
  13. fund_cli/analysis/portfolio.py +152 -0
  14. fund_cli/analysis/risk.py +300 -0
  15. fund_cli/cli.py +98 -0
  16. fund_cli/commands/__init__.py +9 -0
  17. fund_cli/commands/ai_cmd.py +464 -0
  18. fund_cli/commands/analyze_cmd.py +418 -0
  19. fund_cli/commands/compare_cmd.py +264 -0
  20. fund_cli/commands/config_cmd.py +97 -0
  21. fund_cli/commands/data_cmd.py +106 -0
  22. fund_cli/commands/filter_cmd.py +286 -0
  23. fund_cli/commands/holding_cmd.py +140 -0
  24. fund_cli/commands/interactive_cmd.py +84 -0
  25. fund_cli/commands/main.py +17 -0
  26. fund_cli/commands/manager_cmd.py +74 -0
  27. fund_cli/commands/monitor_cmd.py +113 -0
  28. fund_cli/commands/optimize_cmd.py +192 -0
  29. fund_cli/config.py +163 -0
  30. fund_cli/core/__init__.py +8 -0
  31. fund_cli/core/analyzer.py +46 -0
  32. fund_cli/core/data_manager.py +231 -0
  33. fund_cli/core/data_quality.py +162 -0
  34. fund_cli/core/monitor.py +230 -0
  35. fund_cli/core/optimizer.py +50 -0
  36. fund_cli/core/optimizers/__init__.py +13 -0
  37. fund_cli/core/optimizers/efficient_frontier.py +91 -0
  38. fund_cli/core/optimizers/max_sharpe.py +54 -0
  39. fund_cli/core/optimizers/mean_variance.py +84 -0
  40. fund_cli/core/optimizers/risk_parity.py +60 -0
  41. fund_cli/core/reporter.py +67 -0
  42. fund_cli/core/reporters/__init__.py +6 -0
  43. fund_cli/core/reporters/html_reporter.py +62 -0
  44. fund_cli/core/reporters/markdown_reporter.py +40 -0
  45. fund_cli/core/screener.py +142 -0
  46. fund_cli/data/__init__.py +6 -0
  47. fund_cli/data/adapters/__init__.py +7 -0
  48. fund_cli/data/adapters/akshare_adapter.py +442 -0
  49. fund_cli/data/adapters/tushare_adapter.py +254 -0
  50. fund_cli/data/adapters/wind_adapter.py +78 -0
  51. fund_cli/data/base.py +209 -0
  52. fund_cli/data/cache.py +192 -0
  53. fund_cli/data/models.py +248 -0
  54. fund_cli/utils/__init__.py +6 -0
  55. fund_cli/utils/decorators.py +88 -0
  56. fund_cli/utils/helpers.py +127 -0
  57. fund_cli/utils/validators.py +77 -0
  58. fund_cli/views/__init__.py +6 -0
  59. fund_cli/views/charts.py +120 -0
  60. fund_cli/views/reports.py +82 -0
  61. fund_cli/views/tables.py +124 -0
  62. fund_cli-2.0.0.dist-info/METADATA +183 -0
  63. fund_cli-2.0.0.dist-info/RECORD +66 -0
  64. fund_cli-2.0.0.dist-info/WHEEL +4 -0
  65. fund_cli-2.0.0.dist-info/entry_points.txt +3 -0
  66. fund_cli-2.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,440 @@
1
+ """
2
+ 业绩分析引擎
3
+
4
+ 基于 QuantStats 实现专业的业绩分析功能。
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ from fund_cli.config import get_config
13
+ from fund_cli.core.analyzer import Analyzer
14
+
15
+
16
+ class PerformanceAnalyzer(Analyzer):
17
+ """
18
+ 业绩分析引擎
19
+
20
+ 使用 QuantStats 库计算专业业绩指标,包括:
21
+ - 收益指标:总收益、年化收益、累计收益
22
+ - 风险指标:波动率、VaR、CVaR
23
+ - 风险调整收益:夏普比率、索提诺比率、卡玛比率
24
+ - 相对指标:Alpha、Beta、信息比率、跟踪误差
25
+ """
26
+
27
+ def __init__(self, risk_free_rate: float | None = None):
28
+ """
29
+ 初始化业绩分析引擎
30
+
31
+ Args:
32
+ risk_free_rate: 无风险利率,默认从配置读取
33
+ """
34
+ config = get_config()
35
+ self.risk_free_rate = risk_free_rate or config.analysis.risk_free_rate
36
+ self._qs = None
37
+
38
+ def _get_quantstats(self):
39
+ """延迟加载 QuantStats"""
40
+ if self._qs is None:
41
+ try:
42
+ import quantstats as qs
43
+
44
+ self._qs = qs
45
+ except ImportError as e:
46
+ raise ImportError("QuantStats 未安装,请运行: pip install quantstats") from e
47
+ return self._qs
48
+
49
+ def analyze(
50
+ self,
51
+ returns: pd.Series,
52
+ benchmark: pd.Series | None = None,
53
+ **kwargs,
54
+ ) -> dict[str, Any]:
55
+ """
56
+ 执行业绩分析
57
+
58
+ Args:
59
+ returns: 收益率序列(日频)
60
+ benchmark: 基准收益率序列(可选)
61
+ **kwargs: 额外参数
62
+
63
+ Returns:
64
+ 分析结果字典
65
+ """
66
+ qs = self._get_quantstats()
67
+
68
+ # 确保输入为 Series
69
+ if isinstance(returns, pd.DataFrame):
70
+ returns = returns.iloc[:, 0]
71
+
72
+ # 清理数据
73
+ returns = returns.dropna()
74
+
75
+ # 基础收益指标
76
+ metrics = {
77
+ # 收益指标
78
+ "total_return": self._safe_calc(qs.stats.comp, returns) * 100,
79
+ "cagr": self._safe_calc(qs.stats.cagr, returns) * 100,
80
+ "mean_return": returns.mean() * 252 * 100,
81
+ # 风险指标
82
+ "volatility": self._safe_calc(qs.stats.volatility, returns) * 100,
83
+ "max_drawdown": self._safe_calc(qs.stats.max_drawdown, returns) * 100,
84
+ "var_95": self._safe_calc(qs.stats.var, returns) * 100,
85
+ "cvar_95": self._safe_calc(qs.stats.cvar, returns) * 100,
86
+ # 风险调整收益
87
+ "sharpe": self._safe_calc(qs.stats.sharpe, returns, rf=self.risk_free_rate),
88
+ "sortino": self._safe_calc(qs.stats.sortino, returns),
89
+ "calmar": self._safe_calc(qs.stats.calmar, returns),
90
+ # 其他指标
91
+ "skew": self._safe_calc(qs.stats.skew, returns),
92
+ "kurtosis": self._safe_calc(qs.stats.kurtosis, returns),
93
+ "best_day": returns.max() * 100 if not returns.empty else 0,
94
+ "worst_day": returns.min() * 100 if not returns.empty else 0,
95
+ "avg_win": self._safe_calc(qs.stats.avg_win, returns) * 100,
96
+ "avg_loss": self._safe_calc(qs.stats.avg_loss, returns) * 100,
97
+ "win_rate": self._safe_calc(qs.stats.win_rate, returns) * 100,
98
+ }
99
+
100
+ # 相对指标(如果有基准)
101
+ if benchmark is not None:
102
+ if isinstance(benchmark, pd.DataFrame):
103
+ benchmark = benchmark.iloc[:, 0]
104
+ benchmark = benchmark.dropna()
105
+
106
+ # 对齐日期
107
+ common_dates = returns.index.intersection(benchmark.index)
108
+ if len(common_dates) > 0:
109
+ returns_aligned = returns.loc[common_dates]
110
+ benchmark_aligned = benchmark.loc[common_dates]
111
+
112
+ try:
113
+ greeks = qs.stats.greeks(returns_aligned, benchmark_aligned)
114
+ if greeks is not None:
115
+ metrics["alpha"] = float(greeks.iloc[0])
116
+ metrics["beta"] = float(greeks.iloc[1])
117
+ else:
118
+ metrics["alpha"] = None
119
+ metrics["beta"] = None
120
+ except Exception:
121
+ metrics["alpha"] = None
122
+ metrics["beta"] = None
123
+
124
+ # tracking_error / information_ratio / r_squared 在部分版本不可用
125
+ # 使用 hasattr 检查,因为属性访问时会抛出 AttributeError
126
+ if hasattr(qs.stats, "tracking_error"):
127
+ metrics["tracking_error"] = self._safe_calc(
128
+ qs.stats.tracking_error, returns_aligned, benchmark_aligned
129
+ )
130
+ if metrics["tracking_error"] is not None and not (
131
+ isinstance(metrics["tracking_error"], float)
132
+ and metrics["tracking_error"] != metrics["tracking_error"]
133
+ ):
134
+ metrics["tracking_error"] = metrics["tracking_error"] * 100
135
+ else:
136
+ metrics["tracking_error"] = None
137
+ else:
138
+ metrics["tracking_error"] = None
139
+
140
+ if metrics["tracking_error"] is None:
141
+ # 手动计算 tracking_error
142
+ excess = returns_aligned - benchmark_aligned
143
+ metrics["tracking_error"] = float(excess.std() * np.sqrt(252)) * 100
144
+
145
+ if hasattr(qs.stats, "information_ratio"):
146
+ metrics["information_ratio"] = self._safe_calc(
147
+ qs.stats.information_ratio, returns_aligned, benchmark_aligned
148
+ )
149
+ else:
150
+ metrics["information_ratio"] = None
151
+
152
+ if metrics["information_ratio"] is None or (
153
+ isinstance(metrics["information_ratio"], float)
154
+ and metrics["information_ratio"] != metrics["information_ratio"]
155
+ ):
156
+ # 手动计算 information_ratio
157
+ excess = returns_aligned - benchmark_aligned
158
+ te = excess.std() * np.sqrt(252)
159
+ metrics["information_ratio"] = (
160
+ float(excess.mean() * 252 / te) if te > 0 else None
161
+ )
162
+
163
+ if hasattr(qs.stats, "r_squared"):
164
+ metrics["r_squared"] = self._safe_calc(
165
+ qs.stats.r_squared, returns_aligned, benchmark_aligned
166
+ )
167
+ else:
168
+ metrics["r_squared"] = None
169
+
170
+ if metrics["r_squared"] is None or (
171
+ isinstance(metrics["r_squared"], float)
172
+ and metrics["r_squared"] != metrics["r_squared"]
173
+ ):
174
+ metrics["r_squared"] = float(returns_aligned.corr(benchmark_aligned) ** 2)
175
+
176
+ return metrics
177
+
178
+ def _safe_calc(self, func, *args, **kwargs) -> Any:
179
+ """安全计算,捕获异常"""
180
+ try:
181
+ result = func(*args, **kwargs)
182
+ if result is None:
183
+ return float("nan")
184
+ return result
185
+ except Exception:
186
+ return float("nan")
187
+
188
+ def get_metrics(self) -> list[str]:
189
+ """
190
+ 获取可计算的指标列表
191
+
192
+ Returns:
193
+ 指标名称列表
194
+ """
195
+ return [
196
+ "total_return",
197
+ "cagr",
198
+ "volatility",
199
+ "max_drawdown",
200
+ "sharpe",
201
+ "sortino",
202
+ "calmar",
203
+ "var_95",
204
+ "cvar_95",
205
+ "alpha",
206
+ "beta",
207
+ "tracking_error",
208
+ "information_ratio",
209
+ ]
210
+
211
+ def calculate_returns(
212
+ self,
213
+ nav_data: pd.DataFrame,
214
+ nav_column: str = "unit_nav",
215
+ ) -> pd.Series:
216
+ """
217
+ 从净值数据计算收益率
218
+
219
+ Args:
220
+ nav_data: 净值数据 DataFrame
221
+ nav_column: 净值列名
222
+
223
+ Returns:
224
+ 日收益率序列
225
+ """
226
+ nav = nav_data.set_index("nav_date")[nav_column]
227
+ returns = nav.pct_change().dropna()
228
+ returns.name = "daily_return"
229
+ return returns
230
+
231
+ def calculate_cumulative_return(
232
+ self,
233
+ returns: pd.Series,
234
+ ) -> pd.Series:
235
+ """
236
+ 计算累计收益率
237
+
238
+ Args:
239
+ returns: 日收益率序列
240
+
241
+ Returns:
242
+ 累计收益率序列
243
+ """
244
+ return (1 + returns).cumprod() - 1
245
+
246
+ def calculate_drawdown(
247
+ self,
248
+ returns: pd.Series,
249
+ ) -> pd.Series:
250
+ """
251
+ 计算回撤序列
252
+
253
+ Args:
254
+ returns: 日收益率序列
255
+
256
+ Returns:
257
+ 回撤序列
258
+ """
259
+ wealth = (1 + returns).cumprod()
260
+ rolling_max = wealth.cummax()
261
+ drawdown = (wealth - rolling_max) / rolling_max
262
+ return drawdown
263
+
264
+ def rolling_performance(self, returns: pd.Series, window: int = 60) -> pd.DataFrame:
265
+ """
266
+ 滚动业绩分析 (FUND-ANALYZE-006)
267
+
268
+ Args:
269
+ returns: 日收益率序列
270
+ window: 滚动窗口(交易日)
271
+
272
+ Returns:
273
+ 滚动指标 DataFrame,包含 rolling_return, rolling_sharpe, rolling_volatility, rolling_max_drawdown
274
+ """
275
+ if len(returns) < window:
276
+ return pd.DataFrame()
277
+
278
+ rolling_ret = returns.rolling(window=window).apply(lambda x: (1 + x).prod() - 1) * 100
279
+ rolling_vol = returns.rolling(window=window).std() * np.sqrt(252) * 100
280
+ rolling_sharpe = (
281
+ (rolling_ret / rolling_vol) if rolling_vol.notna().any() else pd.Series(dtype=float)
282
+ )
283
+
284
+ def _rolling_mdd(x):
285
+ if len(x) == 0:
286
+ return 0
287
+ cumprod = (1 + x).cumprod()
288
+ cummax = cumprod.cummax()
289
+ return ((cummax - cumprod) / cummax).min() * 100
290
+
291
+ rolling_mdd = returns.rolling(window=window).apply(_rolling_mdd)
292
+
293
+ return pd.DataFrame(
294
+ {
295
+ "rolling_return": rolling_ret,
296
+ "rolling_volatility": rolling_vol,
297
+ "rolling_sharpe": rolling_sharpe,
298
+ "rolling_max_drawdown": rolling_mdd,
299
+ }
300
+ ).dropna()
301
+
302
+ def monthly_return_distribution(self, returns: pd.Series) -> dict[str, Any]:
303
+ """
304
+ 月度收益分布 (FUND-ANALYZE-008)
305
+
306
+ Args:
307
+ returns: 日收益率序列
308
+
309
+ Returns:
310
+ 月度分布统计字典
311
+ """
312
+ if returns.empty:
313
+ return {
314
+ "monthly_returns": [],
315
+ "positive_months": 0,
316
+ "negative_months": 0,
317
+ "avg_monthly_return": 0,
318
+ "max_month": 0,
319
+ "min_month": 0,
320
+ }
321
+
322
+ monthly = returns.resample("ME").apply(lambda x: (1 + x).prod() - 1) * 100
323
+ monthly = monthly.dropna()
324
+
325
+ positive = (monthly > 0).sum()
326
+ negative = (monthly < 0).sum()
327
+
328
+ return {
329
+ "total_months": len(monthly),
330
+ "positive_months": int(positive),
331
+ "negative_months": int(negative),
332
+ "win_rate": round(positive / len(monthly) * 100, 1) if len(monthly) > 0 else 0,
333
+ "avg_monthly_return": round(monthly.mean(), 4),
334
+ "std_monthly_return": round(monthly.std(), 4),
335
+ "max_month": round(monthly.max(), 4),
336
+ "min_month": round(monthly.min(), 4),
337
+ "monthly_returns": monthly.to_dict(),
338
+ }
339
+
340
+ def scenario_analysis(
341
+ self, returns: pd.Series, scenarios: dict[str, Any] | None = None
342
+ ) -> dict[str, Any]:
343
+ """
344
+ 情景分析 (FUND-ANALYZE-009)
345
+
346
+ 预定义情景:牛市(+20%年化)、熊市(-20%年化)、震荡市(0%年化)
347
+
348
+ Args:
349
+ returns: 日收益率序列
350
+ scenarios: 自定义情景 {名称: 年化收益率}
351
+
352
+ Returns:
353
+ 情景分析结果
354
+ """
355
+ if scenarios is None:
356
+ scenarios = {
357
+ "牛市": 0.20,
358
+ "温和牛市": 0.10,
359
+ "震荡市": 0.0,
360
+ "温和熊市": -0.10,
361
+ "熊市": -0.20,
362
+ }
363
+
364
+ results = {}
365
+ for name, annual_return in scenarios.items():
366
+ daily_return = (1 + annual_return) ** (1 / 252) - 1
367
+ n_days = len(returns)
368
+ simulated = np.random.normal(daily_return, returns.std(), n_days)
369
+ total = (1 + pd.Series(simulated)).prod() - 1
370
+ vol = pd.Series(simulated).std() * np.sqrt(252)
371
+ results[name] = {
372
+ "annual_return": round(annual_return * 100, 2),
373
+ "simulated_total_return": round(total * 100, 2),
374
+ "simulated_volatility": round(vol * 100, 2),
375
+ }
376
+
377
+ return results
378
+
379
+ def performance_persistence(
380
+ self, returns: pd.Series, periods_per_year: int = 12
381
+ ) -> dict[str, Any]:
382
+ """
383
+ 业绩持续性分析 (FUND-ANALYZE-010)
384
+
385
+ Args:
386
+ returns: 日收益率序列
387
+ periods_per_year: 每年周期数
388
+
389
+ Returns:
390
+ 持续性分析结果
391
+ """
392
+ if len(returns) < periods_per_year * 2:
393
+ return {"persistence_score": 0, "message": "数据不足"}
394
+
395
+ monthly = returns.resample("ME").apply(lambda x: (1 + x).prod() - 1)
396
+ monthly = monthly.dropna()
397
+
398
+ if len(monthly) < 6:
399
+ return {"persistence_score": 0, "message": "月度数据不足"}
400
+
401
+ # 计算排名相关性(相邻周期排名的相关系数)
402
+ half = len(monthly) // 2
403
+ first_half = monthly.iloc[:half]
404
+ second_half = monthly.iloc[half : 2 * half]
405
+
406
+ if len(first_half) > 1 and len(second_half) > 1:
407
+ rank_corr = first_half.rank().corr(second_half.rank())
408
+ else:
409
+ rank_corr = 0
410
+
411
+ # 胜率
412
+ win_rate = (monthly > 0).sum() / len(monthly) * 100
413
+
414
+ # 连续正/负月数
415
+ max_positive_streak = 0
416
+ max_negative_streak = 0
417
+ current_streak = 0
418
+ current_sign = 1
419
+ for val in monthly:
420
+ sign = 1 if val > 0 else -1
421
+ if sign == current_sign:
422
+ current_streak += 1
423
+ else:
424
+ current_streak = 1
425
+ current_sign = sign
426
+ if sign == 1:
427
+ max_positive_streak = max(max_positive_streak, current_streak)
428
+ else:
429
+ max_negative_streak = max(max_negative_streak, current_streak)
430
+
431
+ persistence_score = max(0, min(100, (rank_corr + 1) * 50))
432
+
433
+ return {
434
+ "persistence_score": round(persistence_score, 1),
435
+ "rank_correlation": round(rank_corr, 4),
436
+ "monthly_win_rate": round(win_rate, 1),
437
+ "max_positive_streak": max_positive_streak,
438
+ "max_negative_streak": max_negative_streak,
439
+ "total_months": len(monthly),
440
+ }
@@ -0,0 +1,152 @@
1
+ """
2
+ 投资组合分析引擎
3
+
4
+ 实现组合层面的分析功能,包括组合收益、风险分散度等。
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ from fund_cli.core.analyzer import Analyzer
13
+
14
+
15
+ class PortfolioAnalyzer(Analyzer):
16
+ """
17
+ 投资组合分析引擎
18
+
19
+ 支持:
20
+ - 组合权重分析
21
+ - 组合风险分散度
22
+ - 资产相关性分析
23
+ - 组合收益贡献分析
24
+ """
25
+
26
+ def analyze(
27
+ self,
28
+ data: pd.DataFrame,
29
+ weights: dict[str, float] | None = None,
30
+ **kwargs,
31
+ ) -> dict[str, Any]:
32
+ """
33
+ 执行组合分析
34
+
35
+ Args:
36
+ data: 多资产收益率 DataFrame,每列一个资产
37
+ weights: 各资产权重字典
38
+ **kwargs: 额外参数
39
+
40
+ Returns:
41
+ 组合分析结果字典
42
+ """
43
+ if isinstance(data, pd.Series):
44
+ data = data.to_frame("asset")
45
+
46
+ if weights is None:
47
+ # 等权配置
48
+ n = data.shape[1]
49
+ weights = dict.fromkeys(data.columns, 1.0 / n)
50
+
51
+ result = {
52
+ "asset_count": len(weights),
53
+ "weights": weights,
54
+ }
55
+
56
+ # 组合收益率
57
+ portfolio_returns = self._calculate_portfolio_returns(data, weights)
58
+ result["portfolio_return"] = float(portfolio_returns.mean() * 252 * 100)
59
+ result["portfolio_volatility"] = float(portfolio_returns.std() * np.sqrt(252) * 100)
60
+ result["portfolio_sharpe"] = float(
61
+ portfolio_returns.mean() / portfolio_returns.std() * np.sqrt(252)
62
+ if portfolio_returns.std() > 0
63
+ else 0
64
+ )
65
+
66
+ # 相关性分析
67
+ result["correlation_matrix"] = data.corr().to_dict()
68
+ avg_correlation = self._average_correlation(data.corr())
69
+ result["average_correlation"] = float(avg_correlation)
70
+
71
+ # 风险分散度(DR)
72
+ result["diversification_ratio"] = float(self._diversification_ratio(data, weights))
73
+
74
+ # 各资产贡献
75
+ result["contribution"] = self._return_contribution(data, weights)
76
+
77
+ return result
78
+
79
+ def _calculate_portfolio_returns(
80
+ self,
81
+ returns: pd.DataFrame,
82
+ weights: dict[str, float],
83
+ ) -> pd.Series:
84
+ """计算组合收益率序列"""
85
+ valid_cols = [c for c in weights.keys() if c in returns.columns]
86
+ w = np.array([weights[c] for c in valid_cols])
87
+ return returns[valid_cols].dot(w)
88
+
89
+ def _average_correlation(self, corr_matrix: pd.DataFrame) -> float:
90
+ """计算平均相关系数"""
91
+ n = len(corr_matrix)
92
+ if n <= 1:
93
+ return 1.0
94
+
95
+ # 取上三角(不含对角线)
96
+ mask = np.triu(np.ones((n, n), dtype=bool), k=1)
97
+ values = corr_matrix.values[mask]
98
+
99
+ return float(np.mean(values)) if len(values) > 0 else 1.0
100
+
101
+ def _diversification_ratio(
102
+ self,
103
+ returns: pd.DataFrame,
104
+ weights: dict[str, float],
105
+ ) -> float:
106
+ """
107
+ 计算风险分散度(Diversification Ratio)
108
+
109
+ DR = 加权平均波动率 / 组合波动率
110
+ DR > 1 表示组合具有分散化效果
111
+ """
112
+ valid_cols = [c for c in weights.keys() if c in returns.columns]
113
+ if not valid_cols:
114
+ return 1.0
115
+
116
+ w = np.array([weights[c] for c in valid_cols])
117
+ vols = returns[valid_cols].std() * np.sqrt(252)
118
+
119
+ weighted_vol = float(np.dot(w, vols))
120
+ portfolio_vol = float(returns[valid_cols].dot(w).std() * np.sqrt(252))
121
+
122
+ if portfolio_vol == 0:
123
+ return 1.0
124
+
125
+ return weighted_vol / portfolio_vol
126
+
127
+ def _return_contribution(
128
+ self,
129
+ returns: pd.DataFrame,
130
+ weights: dict[str, float],
131
+ ) -> dict[str, float]:
132
+ """计算各资产收益贡献"""
133
+ contribution = {}
134
+ for asset, weight in weights.items():
135
+ if asset in returns.columns:
136
+ annual_return = returns[asset].mean() * 252 * 100
137
+ contribution[asset] = {
138
+ "weight": weight,
139
+ "return": float(annual_return),
140
+ "contribution": float(weight * annual_return),
141
+ }
142
+ return contribution
143
+
144
+ def get_metrics(self) -> list[str]:
145
+ """获取可计算的指标列表"""
146
+ return [
147
+ "portfolio_return",
148
+ "portfolio_volatility",
149
+ "portfolio_sharpe",
150
+ "average_correlation",
151
+ "diversification_ratio",
152
+ ]