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.
- fund_cli/__init__.py +13 -0
- fund_cli/__main__.py +10 -0
- fund_cli/ai/__init__.py +21 -0
- fund_cli/ai/analyzer.py +360 -0
- fund_cli/ai/prompts.py +244 -0
- fund_cli/ai/providers.py +286 -0
- fund_cli/analysis/__init__.py +17 -0
- fund_cli/analysis/attribution.py +161 -0
- fund_cli/analysis/backtest.py +75 -0
- fund_cli/analysis/holding.py +217 -0
- fund_cli/analysis/manager.py +133 -0
- fund_cli/analysis/performance.py +440 -0
- fund_cli/analysis/portfolio.py +152 -0
- fund_cli/analysis/risk.py +300 -0
- fund_cli/cli.py +98 -0
- fund_cli/commands/__init__.py +9 -0
- fund_cli/commands/ai_cmd.py +464 -0
- fund_cli/commands/analyze_cmd.py +418 -0
- fund_cli/commands/compare_cmd.py +264 -0
- fund_cli/commands/config_cmd.py +97 -0
- fund_cli/commands/data_cmd.py +106 -0
- fund_cli/commands/filter_cmd.py +286 -0
- fund_cli/commands/holding_cmd.py +140 -0
- fund_cli/commands/interactive_cmd.py +84 -0
- fund_cli/commands/main.py +17 -0
- fund_cli/commands/manager_cmd.py +74 -0
- fund_cli/commands/monitor_cmd.py +113 -0
- fund_cli/commands/optimize_cmd.py +192 -0
- fund_cli/config.py +163 -0
- fund_cli/core/__init__.py +8 -0
- fund_cli/core/analyzer.py +46 -0
- fund_cli/core/data_manager.py +231 -0
- fund_cli/core/data_quality.py +162 -0
- fund_cli/core/monitor.py +230 -0
- fund_cli/core/optimizer.py +50 -0
- fund_cli/core/optimizers/__init__.py +13 -0
- fund_cli/core/optimizers/efficient_frontier.py +91 -0
- fund_cli/core/optimizers/max_sharpe.py +54 -0
- fund_cli/core/optimizers/mean_variance.py +84 -0
- fund_cli/core/optimizers/risk_parity.py +60 -0
- fund_cli/core/reporter.py +67 -0
- fund_cli/core/reporters/__init__.py +6 -0
- fund_cli/core/reporters/html_reporter.py +62 -0
- fund_cli/core/reporters/markdown_reporter.py +40 -0
- fund_cli/core/screener.py +142 -0
- fund_cli/data/__init__.py +6 -0
- fund_cli/data/adapters/__init__.py +7 -0
- fund_cli/data/adapters/akshare_adapter.py +442 -0
- fund_cli/data/adapters/tushare_adapter.py +254 -0
- fund_cli/data/adapters/wind_adapter.py +78 -0
- fund_cli/data/base.py +209 -0
- fund_cli/data/cache.py +192 -0
- fund_cli/data/models.py +248 -0
- fund_cli/utils/__init__.py +6 -0
- fund_cli/utils/decorators.py +88 -0
- fund_cli/utils/helpers.py +127 -0
- fund_cli/utils/validators.py +77 -0
- fund_cli/views/__init__.py +6 -0
- fund_cli/views/charts.py +120 -0
- fund_cli/views/reports.py +82 -0
- fund_cli/views/tables.py +124 -0
- fund_cli-2.0.0.dist-info/METADATA +183 -0
- fund_cli-2.0.0.dist-info/RECORD +66 -0
- fund_cli-2.0.0.dist-info/WHEEL +4 -0
- fund_cli-2.0.0.dist-info/entry_points.txt +3 -0
- 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()
|