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,418 @@
1
+ """
2
+ 基金分析命令
3
+
4
+ 提供基金信息查询、业绩分析、报告生成等功能。
5
+ """
6
+
7
+ from datetime import date, datetime, timedelta
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ from fund_cli.analysis.performance import PerformanceAnalyzer
15
+ from fund_cli.analysis.risk import RiskAnalyzer
16
+ from fund_cli.core.data_manager import get_data_manager
17
+
18
+ app = typer.Typer(help="基金分析命令")
19
+ console = Console()
20
+
21
+
22
+ @app.command("info")
23
+ def info_fund(
24
+ fund_code: str = typer.Argument(..., help="基金代码(6位数字)"),
25
+ ) -> None:
26
+ """
27
+ 查看基金基础信息。
28
+
29
+ 示例:
30
+ fund analyze info 000001
31
+ """
32
+ try:
33
+ dm = get_data_manager()
34
+
35
+ console.print(f"[bold blue]获取基金 {fund_code} 信息...[/bold blue]")
36
+
37
+ info = dm.get_fund_info(fund_code)
38
+
39
+ # 显示基金信息
40
+ info_table = Table(show_header=False, box=None)
41
+ info_table.add_column("字段", style="cyan")
42
+ info_table.add_column("值", style="white")
43
+
44
+ field_names = {
45
+ "code": "基金代码",
46
+ "name": "基金名称",
47
+ "type": "基金类型",
48
+ "establish_date": "成立日期",
49
+ "manager": "基金经理",
50
+ "company": "基金公司",
51
+ "scale": "规模(亿元)",
52
+ }
53
+
54
+ for key, label in field_names.items():
55
+ value = info.get(key, "-")
56
+ if value is None:
57
+ value = "-"
58
+ info_table.add_row(label, str(value))
59
+
60
+ console.print(
61
+ Panel(
62
+ info_table, title=f"[bold]{info.get('name', fund_code)}[/bold]", border_style="blue"
63
+ )
64
+ )
65
+
66
+ except Exception as e:
67
+ console.print(f"[red]获取信息失败: {e}[/red]")
68
+ raise typer.Exit(1) from None
69
+
70
+
71
+ @app.command("nav")
72
+ def nav_history(
73
+ fund_code: str = typer.Argument(..., help="基金代码"),
74
+ start_date: str | None = typer.Option(None, "--start", "-s", help="开始日期 (YYYY-MM-DD)"),
75
+ end_date: str | None = typer.Option(None, "--end", "-e", help="结束日期 (YYYY-MM-DD)"),
76
+ limit: int = typer.Option(30, "--limit", "-l", help="显示条数"),
77
+ ) -> None:
78
+ """
79
+ 查看基金净值历史。
80
+
81
+ 示例:
82
+ fund analyze nav 000001
83
+ fund analyze nav 000001 --start 2023-01-01 --end 2023-12-31
84
+ """
85
+ try:
86
+ dm = get_data_manager()
87
+
88
+ # 解析日期
89
+ start = datetime.strptime(start_date, "%Y-%m-%d").date() if start_date else None
90
+ end = datetime.strptime(end_date, "%Y-%m-%d").date() if end_date else None
91
+
92
+ console.print(f"[bold blue]获取基金 {fund_code} 净值数据...[/bold blue]")
93
+
94
+ df = dm.get_fund_nav(fund_code, start_date=start, end_date=end)
95
+
96
+ if df.empty:
97
+ console.print("[yellow]未找到净值数据[/yellow]")
98
+ return
99
+
100
+ # 显示最近的数据
101
+ df_display = df.tail(limit).copy()
102
+ df_display["nav_date"] = df_display["nav_date"].dt.strftime("%Y-%m-%d")
103
+
104
+ table = Table(title=f"基金 {fund_code} 净值历史")
105
+ table.add_column("日期", style="cyan")
106
+ table.add_column("单位净值", style="green")
107
+ table.add_column("累计净值", style="blue")
108
+ table.add_column("日涨跌(%)", style="yellow")
109
+
110
+ for _, row in df_display.iterrows():
111
+ daily_return = row.get("daily_return", "")
112
+ if daily_return is not None:
113
+ daily_return = f"{daily_return:.2f}"
114
+ else:
115
+ daily_return = "-"
116
+
117
+ table.add_row(
118
+ str(row["nav_date"]),
119
+ f"{row['unit_nav']:.4f}",
120
+ f"{row['accumulated_nav']:.4f}" if row.get("accumulated_nav") else "-",
121
+ daily_return,
122
+ )
123
+
124
+ console.print(table)
125
+ console.print(f"\n[green]共 {len(df)} 条记录,显示最近 {len(df_display)} 条[/green]")
126
+
127
+ except Exception as e:
128
+ console.print(f"[red]获取净值失败: {e}[/red]")
129
+ raise typer.Exit(1) from None
130
+
131
+
132
+ @app.command("metrics")
133
+ def analyze_metrics(
134
+ fund_code: str = typer.Argument(..., help="基金代码"),
135
+ start_date: str | None = typer.Option(None, "--start", "-s", help="开始日期"),
136
+ end_date: str | None = typer.Option(None, "--end", "-e", help="结束日期"),
137
+ benchmark: str | None = typer.Option(None, "-b", "--benchmark", help="基准指数代码"),
138
+ ) -> None:
139
+ """
140
+ 分析基金业绩指标。
141
+
142
+ 示例:
143
+ fund analyze metrics 000001
144
+ fund analyze metrics 000001 -b 000300
145
+ """
146
+ try:
147
+ dm = get_data_manager()
148
+
149
+ # 解析日期
150
+ start = (
151
+ datetime.strptime(start_date, "%Y-%m-%d").date()
152
+ if start_date
153
+ else date.today() - timedelta(days=365)
154
+ )
155
+ end = datetime.strptime(end_date, "%Y-%m-%d").date() if end_date else date.today()
156
+
157
+ console.print(f"[bold blue]分析基金 {fund_code} 业绩指标...[/bold blue]")
158
+
159
+ # 获取净值数据
160
+ nav_df = dm.get_fund_nav(fund_code, start_date=start, end_date=end)
161
+
162
+ if nav_df.empty:
163
+ console.print("[yellow]未找到净值数据[/yellow]")
164
+ return
165
+
166
+ # 计算收益率
167
+ nav_series = nav_df.set_index("nav_date")["unit_nav"]
168
+ returns = nav_series.pct_change().dropna()
169
+
170
+ # 获取基准数据
171
+ benchmark_returns = None
172
+ if benchmark:
173
+ try:
174
+ benchmark_df = dm.get_benchmark_nav(benchmark, start_date=start, end_date=end)
175
+ if not benchmark_df.empty:
176
+ benchmark_series = benchmark_df.set_index("nav_date")["unit_nav"]
177
+ benchmark_returns = benchmark_series.pct_change().dropna()
178
+ except Exception:
179
+ console.print(f"[yellow]无法获取基准 {benchmark} 数据[/yellow]")
180
+
181
+ # 执行分析
182
+ perf_analyzer = PerformanceAnalyzer()
183
+ risk_analyzer = RiskAnalyzer()
184
+
185
+ perf_metrics = perf_analyzer.analyze(returns, benchmark=benchmark_returns)
186
+ risk_metrics = risk_analyzer.analyze(returns, benchmark=benchmark_returns)
187
+
188
+ # 显示结果
189
+ metrics_table = Table(title=f"基金 {fund_code} 分析结果", show_header=True)
190
+ metrics_table.add_column("指标", style="cyan")
191
+ metrics_table.add_column("业绩指标", style="green")
192
+ metrics_table.add_column("风险指标", style="yellow")
193
+
194
+ # 收益指标
195
+ metrics_table.add_row("总收益率", f"{perf_metrics.get('total_return', 0):.2f}%", "-")
196
+ metrics_table.add_row("年化收益率", f"{perf_metrics.get('cagr', 0):.2f}%", "-")
197
+ metrics_table.add_row(
198
+ "年化波动率",
199
+ f"{perf_metrics.get('volatility', 0):.2f}%",
200
+ f"{risk_metrics.get('volatility_annual', 0):.2f}%",
201
+ )
202
+ metrics_table.add_row(
203
+ "最大回撤",
204
+ f"{perf_metrics.get('max_drawdown', 0):.2f}%",
205
+ f"{risk_metrics.get('max_drawdown', 0):.2f}%",
206
+ )
207
+ metrics_table.add_row("夏普比率", f"{perf_metrics.get('sharpe', 0):.2f}", "-")
208
+ metrics_table.add_row("索提诺比率", f"{perf_metrics.get('sortino', 0):.2f}", "-")
209
+ metrics_table.add_row(
210
+ "VaR(95%)",
211
+ f"{perf_metrics.get('var_95', 0):.2f}%",
212
+ f"{risk_metrics.get('var_95', 0):.2f}%",
213
+ )
214
+
215
+ # 相对指标
216
+ if benchmark:
217
+ metrics_table.add_row("Alpha", f"{perf_metrics.get('alpha', '-') or '-'}", "-")
218
+ metrics_table.add_row(
219
+ "Beta",
220
+ f"{perf_metrics.get('beta', '-') or '-'}",
221
+ f"{risk_metrics.get('beta', '-') or '-'}",
222
+ )
223
+ metrics_table.add_row(
224
+ "信息比率", f"{perf_metrics.get('information_ratio', '-') or '-'}", "-"
225
+ )
226
+ metrics_table.add_row(
227
+ "跟踪误差",
228
+ f"{perf_metrics.get('tracking_error', '-') or '-'}%",
229
+ f"{risk_metrics.get('tracking_error', '-') or '-'}%",
230
+ )
231
+
232
+ console.print(metrics_table)
233
+ console.print(f"\n[dim]分析区间: {start} 至 {end}[/dim]")
234
+
235
+ except Exception as e:
236
+ console.print(f"[red]分析失败: {e}[/red]")
237
+ raise typer.Exit(1) from None
238
+
239
+
240
+ @app.command("report")
241
+ def generate_report(
242
+ fund_code: str = typer.Argument(..., help="基金代码"),
243
+ output: str = typer.Option(None, help="输出文件路径"),
244
+ format: str = typer.Option("html", help="报告格式: html/markdown"),
245
+ ):
246
+ """生成分析报告 (FUND-ANALYZE-011)"""
247
+ from fund_cli.analysis.performance import PerformanceAnalyzer
248
+ from fund_cli.core.data_manager import DataManager
249
+
250
+ dm = DataManager()
251
+ try:
252
+ nav_df = dm.get_fund_nav(fund_code)
253
+ if nav_df.empty:
254
+ console.print("[yellow]无净值数据[/yellow]")
255
+ return
256
+ returns = nav_df["daily_return"].dropna() / 100.0
257
+ analyzer = PerformanceAnalyzer()
258
+ metrics = analyzer.analyze(returns)
259
+
260
+ if format == "html":
261
+ from fund_cli.core.reporters.html_reporter import HtmlReporter
262
+
263
+ reporter = HtmlReporter()
264
+ ext = ".html"
265
+ elif format == "markdown":
266
+ from fund_cli.core.reporters.markdown_reporter import MarkdownReporter
267
+
268
+ reporter = MarkdownReporter()
269
+ ext = ".md"
270
+ else:
271
+ console.print(f"[red]不支持的格式: {format}[/red]")
272
+ raise typer.Exit(1) from None
273
+
274
+ content = reporter.generate(fund_code, metrics, nav_data=nav_df)
275
+ out_path = output or f"{fund_code}_report{ext}"
276
+ reporter.save(content, out_path)
277
+ console.print(f"[green]报告已生成: {out_path}[/green]")
278
+ except Exception as e:
279
+ console.print(f"[red]报告生成失败: {e}[/red]")
280
+ raise typer.Exit(1) from None
281
+
282
+
283
+ @app.command("rolling")
284
+ def rolling_performance(
285
+ fund_code: str = typer.Argument(..., help="基金代码"),
286
+ window: int = typer.Option(60, help="滚动窗口(交易日)"),
287
+ start_date: str = typer.Option(None, help="开始日期"),
288
+ end_date: str = typer.Option(None, help="结束日期"),
289
+ ):
290
+ """滚动业绩分析 (FUND-ANALYZE-006)"""
291
+ from fund_cli.analysis.performance import PerformanceAnalyzer
292
+ from fund_cli.core.data_manager import DataManager
293
+
294
+ dm = DataManager()
295
+ try:
296
+ nav_df = dm.get_fund_nav(
297
+ fund_code,
298
+ start_date=date.fromisoformat(start_date) if start_date else None,
299
+ end_date=date.fromisoformat(end_date) if end_date else None,
300
+ )
301
+ if nav_df.empty:
302
+ console.print("[yellow]无净值数据[/yellow]")
303
+ return
304
+ returns = nav_df["daily_return"].dropna() / 100.0
305
+ analyzer = PerformanceAnalyzer()
306
+ result = analyzer.rolling_performance(returns, window)
307
+ if result.empty:
308
+ console.print("[yellow]数据不足以计算滚动指标[/yellow]")
309
+ return
310
+ console.print(f"\n[bold]{fund_code}[/bold] 滚动业绩 (窗口={window}日):")
311
+ table = Table(show_header=True, header_style="bold blue")
312
+ table.add_column("日期")
313
+ table.add_column("滚动收益", justify="right")
314
+ table.add_column("滚动夏普", justify="right")
315
+ table.add_column("滚动波动", justify="right")
316
+ for idx, row in result.tail(10).iterrows():
317
+ table.add_row(
318
+ str(idx.date()),
319
+ f"{row['rolling_return']:.2f}%",
320
+ f"{row['rolling_sharpe']:.4f}",
321
+ f"{row['rolling_volatility']:.2f}%",
322
+ )
323
+ console.print(table)
324
+ except Exception as e:
325
+ console.print(f"[red]分析失败: {e}[/red]")
326
+ raise typer.Exit(1) from None
327
+
328
+
329
+ @app.command("monthly")
330
+ def monthly_distribution(fund_code: str = typer.Argument(..., help="基金代码")):
331
+ """月度收益分布 (FUND-ANALYZE-008)"""
332
+ from fund_cli.analysis.performance import PerformanceAnalyzer
333
+ from fund_cli.core.data_manager import DataManager
334
+
335
+ dm = DataManager()
336
+ try:
337
+ nav_df = dm.get_fund_nav(fund_code)
338
+ if nav_df.empty:
339
+ console.print("[yellow]无净值数据[/yellow]")
340
+ return
341
+ returns = nav_df["daily_return"].dropna() / 100.0
342
+ analyzer = PerformanceAnalyzer()
343
+ result = analyzer.monthly_return_distribution(returns)
344
+ console.print(f"\n[bold]{fund_code}[/bold] 月度收益分布:")
345
+ console.print(f" 总月数: {result['total_months']}")
346
+ console.print(f" 正收益月: [green]{result['positive_months']}[/green]")
347
+ console.print(f" 负收益月: [red]{result['negative_months']}[/red]")
348
+ console.print(f" 月胜率: {result['win_rate']:.1f}%")
349
+ console.print(f" 平均月收益: {result['avg_monthly_return']:.4f}%")
350
+ console.print(f" 最佳月: [green]{result['max_month']:.2f}%[/green]")
351
+ console.print(f" 最差月: [red]{result['min_month']:.2f}%[/red]")
352
+ except Exception as e:
353
+ console.print(f"[red]分析失败: {e}[/red]")
354
+ raise typer.Exit(1) from None
355
+
356
+
357
+ @app.command("scenario")
358
+ def scenario_analysis(fund_code: str = typer.Argument(..., help="基金代码")):
359
+ """情景分析 (FUND-ANALYZE-009)"""
360
+ from fund_cli.analysis.performance import PerformanceAnalyzer
361
+ from fund_cli.core.data_manager import DataManager
362
+
363
+ dm = DataManager()
364
+ try:
365
+ nav_df = dm.get_fund_nav(fund_code)
366
+ if nav_df.empty:
367
+ console.print("[yellow]无净值数据[/yellow]")
368
+ return
369
+ returns = nav_df["daily_return"].dropna() / 100.0
370
+ analyzer = PerformanceAnalyzer()
371
+ result = analyzer.scenario_analysis(returns)
372
+ console.print(f"\n[bold]{fund_code}[/bold] 情景分析:")
373
+ table = Table(show_header=True, header_style="bold blue")
374
+ table.add_column("情景", style="cyan")
375
+ table.add_column("年化假设", justify="right")
376
+ table.add_column("模拟总收益", justify="right")
377
+ table.add_column("模拟波动率", justify="right")
378
+ for name, data in result.items():
379
+ table.add_row(
380
+ name,
381
+ f"{data['annual_return']:.1f}%",
382
+ f"{data['simulated_total_return']:.2f}%",
383
+ f"{data['simulated_volatility']:.2f}%",
384
+ )
385
+ console.print(table)
386
+ except Exception as e:
387
+ console.print(f"[red]分析失败: {e}[/red]")
388
+ raise typer.Exit(1) from None
389
+
390
+
391
+ @app.command("persistence")
392
+ def performance_persistence(fund_code: str = typer.Argument(..., help="基金代码")):
393
+ """业绩持续性分析 (FUND-ANALYZE-010)"""
394
+ from fund_cli.analysis.performance import PerformanceAnalyzer
395
+ from fund_cli.core.data_manager import DataManager
396
+
397
+ dm = DataManager()
398
+ try:
399
+ nav_df = dm.get_fund_nav(fund_code)
400
+ if nav_df.empty:
401
+ console.print("[yellow]无净值数据[/yellow]")
402
+ return
403
+ returns = nav_df["daily_return"].dropna() / 100.0
404
+ analyzer = PerformanceAnalyzer()
405
+ result = analyzer.performance_persistence(returns)
406
+ console.print(f"\n[bold]{fund_code}[/bold] 业绩持续性分析:")
407
+ console.print(f" 持续性评分: {result['persistence_score']}/100")
408
+ console.print(f" 排名相关性: {result['rank_correlation']:.4f}")
409
+ console.print(f" 月胜率: {result['monthly_win_rate']:.1f}%")
410
+ console.print(f" 最长连续正收益: {result['max_positive_streak']}月")
411
+ console.print(f" 最长连续负收益: {result['max_negative_streak']}月")
412
+ except Exception as e:
413
+ console.print(f"[red]分析失败: {e}[/red]")
414
+ raise typer.Exit(1) from None
415
+
416
+
417
+ if __name__ == "__main__":
418
+ app()
@@ -0,0 +1,264 @@
1
+ """
2
+ 基金对比命令
3
+
4
+ 提供多基金对比分析功能。
5
+ """
6
+
7
+ from datetime import date, timedelta
8
+
9
+ import typer
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+
13
+ from fund_cli.analysis.performance import PerformanceAnalyzer
14
+ from fund_cli.core.data_manager import get_data_manager
15
+
16
+ app = typer.Typer(help="基金对比命令")
17
+ console = Console()
18
+
19
+
20
+ @app.command("funds")
21
+ def compare_funds(
22
+ fund_codes: list[str] = typer.Argument(..., help="基金代码列表(至少2只)"),
23
+ period: str = typer.Option("1y", "--period", "-p", help="对比周期: 1m, 3m, 6m, 1y, 3y"),
24
+ ) -> None:
25
+ """
26
+ 对比多只基金的业绩表现。
27
+
28
+ 示例:
29
+ fund compare funds 000001 000002 000003
30
+ fund compare funds 000001 000002 --period 3y
31
+ """
32
+ if len(fund_codes) < 2:
33
+ console.print("[red]请至少输入2只基金代码[/red]")
34
+ raise typer.Exit(1) from None
35
+
36
+ try:
37
+ dm = get_data_manager()
38
+ analyzer = PerformanceAnalyzer()
39
+
40
+ # 计算日期范围
41
+ period_map = {
42
+ "1m": 30,
43
+ "3m": 90,
44
+ "6m": 180,
45
+ "1y": 365,
46
+ "3y": 1095,
47
+ }
48
+ days = period_map.get(period, 365)
49
+ start_date = date.today() - timedelta(days=days)
50
+
51
+ console.print(f"[bold blue]对比分析 {len(fund_codes)} 只基金...[/bold blue]")
52
+
53
+ # 收集数据
54
+ results = []
55
+ for code in fund_codes:
56
+ try:
57
+ nav_df = dm.get_fund_nav(code, start_date=start_date)
58
+ if not nav_df.empty:
59
+ nav_series = nav_df.set_index("nav_date")["unit_nav"]
60
+ returns = nav_series.pct_change().dropna()
61
+ metrics = analyzer.analyze(returns)
62
+
63
+ info = dm.get_fund_info(code)
64
+ results.append(
65
+ {
66
+ "code": code,
67
+ "name": info.get("name", "-"),
68
+ "total_return": metrics.get("total_return", 0),
69
+ "cagr": metrics.get("cagr", 0),
70
+ "volatility": metrics.get("volatility", 0),
71
+ "max_drawdown": metrics.get("max_drawdown", 0),
72
+ "sharpe": metrics.get("sharpe", 0),
73
+ }
74
+ )
75
+ except Exception as e:
76
+ console.print(f"[yellow]获取 {code} 数据失败: {e}[/yellow]")
77
+
78
+ if not results:
79
+ console.print("[red]未能获取任何基金数据[/red]")
80
+ return
81
+
82
+ # 显示对比结果
83
+ table = Table(title="基金对比结果")
84
+ table.add_column("基金代码", style="cyan")
85
+ table.add_column("基金名称", style="white")
86
+ table.add_column("总收益率", style="green")
87
+ table.add_column("年化收益", style="green")
88
+ table.add_column("年化波动", style="yellow")
89
+ table.add_column("最大回撤", style="red")
90
+ table.add_column("夏普比率", style="blue")
91
+
92
+ for r in results:
93
+ table.add_row(
94
+ r["code"],
95
+ r["name"][:10] if r["name"] else "-",
96
+ f"{r['total_return']:.2f}%",
97
+ f"{r['cagr']:.2f}%",
98
+ f"{r['volatility']:.2f}%",
99
+ f"{r['max_drawdown']:.2f}%",
100
+ f"{r['sharpe']:.2f}",
101
+ )
102
+
103
+ console.print(table)
104
+
105
+ except Exception as e:
106
+ console.print(f"[red]对比分析失败: {e}[/red]")
107
+ raise typer.Exit(1) from None
108
+
109
+
110
+ @app.command("rolling-win")
111
+ def rolling_win_rate(
112
+ fund_codes: str = typer.Argument(..., help="基金代码,逗号分隔"),
113
+ window: int = typer.Option(60, help="滚动窗口(交易日)"),
114
+ period: str = typer.Option("1y", help="分析周期"),
115
+ ):
116
+ """滚动胜率对比 (FUND-COMPARE-005)"""
117
+ from datetime import date, timedelta
118
+
119
+ from fund_cli.core.data_manager import DataManager
120
+
121
+ dm = DataManager()
122
+ codes = [c.strip() for c in fund_codes.split(",")]
123
+ end = date.today()
124
+ start = end - timedelta(
125
+ days={"1m": 30, "3m": 90, "6m": 180, "1y": 365, "2y": 730}.get(period, 365)
126
+ )
127
+
128
+ all_returns = {}
129
+ for code in codes:
130
+ try:
131
+ nav_df = dm.get_fund_nav(code, start_date=start, end_date=end)
132
+ if not nav_df.empty and "daily_return" in nav_df.columns:
133
+ all_returns[code] = nav_df["daily_return"].dropna() / 100.0
134
+ except Exception:
135
+ continue
136
+
137
+ if len(all_returns) < 2:
138
+ console.print("[yellow]至少需要2只基金的有效数据[/yellow]")
139
+ return
140
+
141
+ import pandas as pd
142
+
143
+ returns_df = pd.DataFrame(all_returns)
144
+ rolling = returns_df.rolling(window=window).apply(lambda x: (1 + x).prod() - 1)
145
+ win_counts = {}
146
+ for code in codes:
147
+ if code in rolling.columns:
148
+ others = [c for c in codes if c != code and c in rolling.columns]
149
+ if others:
150
+ wins = (rolling[code] > rolling[others].T).sum(axis=1).sum()
151
+ total = rolling[code].notna().sum()
152
+ win_counts[code] = round(wins / total * 100, 1) if total > 0 else 0
153
+
154
+ console.print(f"\n[bold]滚动胜率对比[/bold] (窗口={window}日):")
155
+ table = Table(show_header=True, header_style="bold blue")
156
+ table.add_column("基金代码", style="cyan")
157
+ table.add_column("胜率", justify="right")
158
+ for code, rate in sorted(win_counts.items(), key=lambda x: x[1], reverse=True):
159
+ table.add_row(code, f"{rate:.1f}%")
160
+ console.print(table)
161
+
162
+
163
+ @app.command("correlation")
164
+ def correlation_analysis(
165
+ fund_codes: str = typer.Argument(..., help="基金代码,逗号分隔"),
166
+ period: str = typer.Option("1y", help="分析周期"),
167
+ ):
168
+ """相关性分析 (FUND-COMPARE-006)"""
169
+ from datetime import date, timedelta
170
+
171
+ from fund_cli.core.data_manager import DataManager
172
+
173
+ dm = DataManager()
174
+ codes = [c.strip() for c in fund_codes.split(",")]
175
+ end = date.today()
176
+ start = end - timedelta(days=365)
177
+
178
+ all_returns = {}
179
+ for code in codes:
180
+ try:
181
+ nav_df = dm.get_fund_nav(code, start_date=start, end_date=end)
182
+ if not nav_df.empty and "daily_return" in nav_df.columns:
183
+ all_returns[code] = nav_df["daily_return"].dropna() / 100.0
184
+ except Exception:
185
+ continue
186
+
187
+ if len(all_returns) < 2:
188
+ console.print("[yellow]至少需要2只基金的有效数据[/yellow]")
189
+ return
190
+
191
+ import pandas as pd
192
+
193
+ returns_df = pd.DataFrame(all_returns).dropna()
194
+ corr = returns_df.corr()
195
+
196
+ console.print("\n[bold]相关性矩阵[/bold]:")
197
+ table = Table(show_header=True, header_style="bold blue")
198
+ table.add_column("", style="cyan")
199
+ for code in corr.columns:
200
+ table.add_column(code, justify="right")
201
+ for code in corr.index:
202
+ row = [f"[bold]{code}[/bold]"]
203
+ for val in corr[code]:
204
+ color = "green" if val > 0.5 else "red" if val < -0.3 else ""
205
+ row.append(f"[{color}]{val:.4f}[/{color}]")
206
+ table.add_row(*row)
207
+ console.print(table)
208
+
209
+
210
+ @app.command("report")
211
+ def compare_report(
212
+ fund_codes: str = typer.Argument(..., help="基金代码,逗号分隔"),
213
+ period: str = typer.Option("1y", help="分析周期"),
214
+ output: str = typer.Option(None, help="输出文件路径"),
215
+ ):
216
+ """对比报告生成 (FUND-COMPARE-007)"""
217
+ from datetime import date, timedelta
218
+
219
+ from fund_cli.analysis.performance import PerformanceAnalyzer
220
+ from fund_cli.core.data_manager import DataManager
221
+
222
+ dm = DataManager()
223
+ codes = [c.strip() for c in fund_codes.split(",")]
224
+ end = date.today()
225
+ start = end - timedelta(days=365)
226
+
227
+ analyzer = PerformanceAnalyzer()
228
+ results = []
229
+ for code in codes:
230
+ try:
231
+ nav_df = dm.get_fund_nav(code, start_date=start, end_date=end)
232
+ if nav_df.empty:
233
+ continue
234
+ returns = nav_df["daily_return"].dropna() / 100.0
235
+ metrics = analyzer.analyze(returns)
236
+ metrics["fund_code"] = code
237
+ results.append(metrics)
238
+ except Exception:
239
+ continue
240
+
241
+ if not results:
242
+ console.print("[yellow]无有效数据[/yellow]")
243
+ return
244
+
245
+ from fund_cli.core.reporters.markdown_reporter import MarkdownReporter
246
+
247
+ md = f"# 基金对比报告\n\n对比周期: {period}\n\n"
248
+ md += "| 指标 | " + " | ".join(r["fund_code"] for r in results) + " |\n"
249
+ md += "|------|" + "|".join(["------"] * len(results)) + "|\n"
250
+ for key in ["total_return", "volatility", "sharpe_ratio", "max_drawdown"]:
251
+ row = [key]
252
+ for r in results:
253
+ v = r.get(key, "N/A")
254
+ row.append(f"{v:.4f}" if isinstance(v, float) else str(v))
255
+ md += "|" + "|".join(row) + "|\n"
256
+
257
+ reporter = MarkdownReporter()
258
+ out = output or "comparison_report.md"
259
+ reporter.save(md, out)
260
+ console.print(f"[green]对比报告已生成: {out}[/green]")
261
+
262
+
263
+ if __name__ == "__main__":
264
+ app()