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,464 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI 分析命令(V2.0 实现)
|
|
3
|
+
|
|
4
|
+
提供 AI 辅助分析功能,支持基金摘要生成、对比分析、投资建议等。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import typer
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.panel import Panel
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="AI分析命令(V2.0功能)")
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command("config")
|
|
21
|
+
def ai_config(
|
|
22
|
+
action: str = typer.Argument("show", help="操作: show/set-provider/test"),
|
|
23
|
+
provider: str = typer.Option(None, "--provider", "-p", help="提供商名称"),
|
|
24
|
+
model: str = typer.Option(None, "--model", "-m", help="模型名称"),
|
|
25
|
+
) -> None:
|
|
26
|
+
"""
|
|
27
|
+
AI配置管理
|
|
28
|
+
|
|
29
|
+
示例:
|
|
30
|
+
fund ai config show # 显示当前配置
|
|
31
|
+
fund ai config test # 测试AI连接
|
|
32
|
+
fund ai config set-provider --provider qwen # 设置提供商
|
|
33
|
+
"""
|
|
34
|
+
from fund_cli.ai.providers import get_provider
|
|
35
|
+
from fund_cli.config import get_config
|
|
36
|
+
|
|
37
|
+
if action == "show":
|
|
38
|
+
config = get_config().ai
|
|
39
|
+
table = Table(title="AI配置")
|
|
40
|
+
table.add_column("配置项", style="cyan")
|
|
41
|
+
table.add_column("值", style="green")
|
|
42
|
+
|
|
43
|
+
table.add_row("提供商", config.provider)
|
|
44
|
+
table.add_row("模型", config.model)
|
|
45
|
+
table.add_row("Qwen模型", config.qwen_model)
|
|
46
|
+
table.add_row("温度参数", str(config.temperature))
|
|
47
|
+
table.add_row("最大Token", str(config.max_tokens))
|
|
48
|
+
table.add_row("超时(秒)", str(config.timeout))
|
|
49
|
+
table.add_row("重试次数", str(config.retry_count))
|
|
50
|
+
table.add_row("API Key", "已设置" if config.api_key else "未设置")
|
|
51
|
+
table.add_row("Qwen API Key", "已设置" if config.qwen_api_key else "未设置")
|
|
52
|
+
|
|
53
|
+
console.print(table)
|
|
54
|
+
|
|
55
|
+
elif action == "test":
|
|
56
|
+
config = get_config().ai
|
|
57
|
+
|
|
58
|
+
if not config.validate_config():
|
|
59
|
+
console.print("[red]配置无效,请检查API Key和模型设置[/red]")
|
|
60
|
+
raise typer.Exit(1) from None
|
|
61
|
+
|
|
62
|
+
console.print(f"[dim]测试 {config.provider} 提供商连接...[/dim]")
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
provider_instance = get_provider(config)
|
|
66
|
+
if provider_instance.is_available():
|
|
67
|
+
console.print(f"[green]✓ {config.provider} 连接成功[/green]")
|
|
68
|
+
else:
|
|
69
|
+
console.print(f"[red]✗ {config.provider} 连接失败[/red]")
|
|
70
|
+
raise typer.Exit(1) from None
|
|
71
|
+
except Exception as e:
|
|
72
|
+
console.print(f"[red]连接测试失败: {e}[/red]")
|
|
73
|
+
raise typer.Exit(1) from None
|
|
74
|
+
|
|
75
|
+
elif action == "set-provider":
|
|
76
|
+
if not provider:
|
|
77
|
+
console.print("[red]请使用 --provider 指定提供商[/red]")
|
|
78
|
+
raise typer.Exit(1) from None
|
|
79
|
+
|
|
80
|
+
# 更新配置(仅内存中,不持久化到文件)
|
|
81
|
+
config = get_config().ai
|
|
82
|
+
config.provider = provider
|
|
83
|
+
if model:
|
|
84
|
+
if provider == "qwen":
|
|
85
|
+
config.qwen_model = model
|
|
86
|
+
else:
|
|
87
|
+
config.model = model
|
|
88
|
+
|
|
89
|
+
console.print(f"[green]提供商已设置为: {provider}[/green]")
|
|
90
|
+
if model:
|
|
91
|
+
console.print(f"[green]模型已设置为: {model}[/green]")
|
|
92
|
+
|
|
93
|
+
else:
|
|
94
|
+
console.print(f"[red]未知操作: {action}[/red]")
|
|
95
|
+
console.print("支持的操作: show, test, set-provider")
|
|
96
|
+
raise typer.Exit(1) from None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@app.command("summarize")
|
|
100
|
+
def ai_summarize(
|
|
101
|
+
fund_code: str = typer.Argument(..., help="基金代码"),
|
|
102
|
+
output: str = typer.Option(None, "--output", "-o", help="输出文件路径"),
|
|
103
|
+
) -> None:
|
|
104
|
+
"""
|
|
105
|
+
AI生成基金分析摘要
|
|
106
|
+
|
|
107
|
+
示例:
|
|
108
|
+
fund ai summarize 000001
|
|
109
|
+
fund ai summarize 000001 --output summary.txt
|
|
110
|
+
"""
|
|
111
|
+
from fund_cli.ai.analyzer import AIAnalyzer
|
|
112
|
+
from fund_cli.analysis.performance import PerformanceAnalyzer
|
|
113
|
+
from fund_cli.data.adapters import get_adapter
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
# 获取基金数据
|
|
117
|
+
adapter = get_adapter()
|
|
118
|
+
fund_info = adapter.get_fund_info(fund_code)
|
|
119
|
+
nav_data = adapter.get_fund_nav(fund_code, period="1y")
|
|
120
|
+
|
|
121
|
+
# 计算业绩指标
|
|
122
|
+
perf_analyzer = PerformanceAnalyzer()
|
|
123
|
+
metrics = perf_analyzer.calculate_metrics(nav_data)
|
|
124
|
+
|
|
125
|
+
# 生成AI摘要
|
|
126
|
+
ai_analyzer = AIAnalyzer()
|
|
127
|
+
fund_data = {
|
|
128
|
+
"info": fund_info,
|
|
129
|
+
"nav": nav_data,
|
|
130
|
+
"metrics": metrics,
|
|
131
|
+
}
|
|
132
|
+
summary = ai_analyzer.summarize_fund(fund_code, fund_data)
|
|
133
|
+
|
|
134
|
+
# 输出结果
|
|
135
|
+
console.print(Panel(summary, title=f"AI分析摘要 - {fund_code}"))
|
|
136
|
+
|
|
137
|
+
if output:
|
|
138
|
+
Path(output).write_text(summary, encoding="utf-8")
|
|
139
|
+
console.print(f"[green]结果已保存至: {output}[/green]")
|
|
140
|
+
|
|
141
|
+
except Exception as e:
|
|
142
|
+
console.print(f"[red]分析失败: {e}[/red]")
|
|
143
|
+
raise typer.Exit(1) from None
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@app.command("compare")
|
|
147
|
+
def ai_compare(
|
|
148
|
+
fund_codes: str = typer.Argument(..., help="基金代码列表(逗号分隔)"),
|
|
149
|
+
output: str = typer.Option(None, "--output", "-o", help="输出文件路径"),
|
|
150
|
+
) -> None:
|
|
151
|
+
"""
|
|
152
|
+
AI对比分析多只基金
|
|
153
|
+
|
|
154
|
+
示例:
|
|
155
|
+
fund ai compare 000001,000002,000003
|
|
156
|
+
fund ai compare 000001,000002 --output compare.txt
|
|
157
|
+
"""
|
|
158
|
+
from fund_cli.ai.analyzer import AIAnalyzer
|
|
159
|
+
from fund_cli.analysis.performance import PerformanceAnalyzer
|
|
160
|
+
from fund_cli.data.adapters import get_adapter
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
codes = [c.strip() for c in fund_codes.split(",")]
|
|
164
|
+
if len(codes) < 2:
|
|
165
|
+
console.print("[red]请至少提供2只基金进行对比[/red]")
|
|
166
|
+
raise typer.Exit(1) from None
|
|
167
|
+
|
|
168
|
+
# 获取基金数据
|
|
169
|
+
adapter = get_adapter()
|
|
170
|
+
perf_analyzer = PerformanceAnalyzer()
|
|
171
|
+
|
|
172
|
+
funds_data = []
|
|
173
|
+
for code in codes:
|
|
174
|
+
fund_info = adapter.get_fund_info(code)
|
|
175
|
+
nav_data = adapter.get_fund_nav(code, period="1y")
|
|
176
|
+
metrics = perf_analyzer.calculate_metrics(nav_data)
|
|
177
|
+
funds_data.append(
|
|
178
|
+
{
|
|
179
|
+
"code": code,
|
|
180
|
+
"info": fund_info,
|
|
181
|
+
"nav": nav_data,
|
|
182
|
+
"metrics": metrics,
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
# 生成AI对比分析
|
|
187
|
+
ai_analyzer = AIAnalyzer()
|
|
188
|
+
comparison = ai_analyzer.compare_funds(codes, funds_data)
|
|
189
|
+
|
|
190
|
+
# 输出结果
|
|
191
|
+
console.print(Panel(comparison, title=f"AI对比分析 - {', '.join(codes)}"))
|
|
192
|
+
|
|
193
|
+
if output:
|
|
194
|
+
Path(output).write_text(comparison, encoding="utf-8")
|
|
195
|
+
console.print(f"[green]结果已保存至: {output}[/green]")
|
|
196
|
+
|
|
197
|
+
except Exception as e:
|
|
198
|
+
console.print(f"[red]对比分析失败: {e}[/red]")
|
|
199
|
+
raise typer.Exit(1) from None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@app.command("advice")
|
|
203
|
+
def ai_advice(
|
|
204
|
+
fund_code: str = typer.Argument(..., help="基金代码"),
|
|
205
|
+
risk_profile: str = typer.Option(
|
|
206
|
+
"moderate", "--risk", "-r", help="风险偏好: conservative/moderate/aggressive"
|
|
207
|
+
),
|
|
208
|
+
output: str = typer.Option(None, "--output", "-o", help="输出文件路径"),
|
|
209
|
+
) -> None:
|
|
210
|
+
"""
|
|
211
|
+
AI生成投资建议
|
|
212
|
+
|
|
213
|
+
示例:
|
|
214
|
+
fund ai advice 000001
|
|
215
|
+
fund ai advice 000001 --risk conservative
|
|
216
|
+
fund ai advice 000001 --risk aggressive --output advice.txt
|
|
217
|
+
"""
|
|
218
|
+
from fund_cli.ai.analyzer import AIAnalyzer
|
|
219
|
+
from fund_cli.analysis.performance import PerformanceAnalyzer
|
|
220
|
+
from fund_cli.data.adapters import get_adapter
|
|
221
|
+
|
|
222
|
+
try:
|
|
223
|
+
# 验证风险偏好
|
|
224
|
+
valid_profiles = ["conservative", "moderate", "aggressive"]
|
|
225
|
+
if risk_profile not in valid_profiles:
|
|
226
|
+
console.print(f"[red]无效的风险偏好,请选择: {', '.join(valid_profiles)}[/red]")
|
|
227
|
+
raise typer.Exit(1) from None
|
|
228
|
+
|
|
229
|
+
# 获取基金数据
|
|
230
|
+
adapter = get_adapter()
|
|
231
|
+
fund_info = adapter.get_fund_info(fund_code)
|
|
232
|
+
nav_data = adapter.get_fund_nav(fund_code, period="1y")
|
|
233
|
+
|
|
234
|
+
# 计算业绩指标
|
|
235
|
+
perf_analyzer = PerformanceAnalyzer()
|
|
236
|
+
metrics = perf_analyzer.calculate_metrics(nav_data)
|
|
237
|
+
|
|
238
|
+
# 生成投资建议
|
|
239
|
+
ai_analyzer = AIAnalyzer()
|
|
240
|
+
fund_data = {
|
|
241
|
+
"info": fund_info,
|
|
242
|
+
"nav": nav_data,
|
|
243
|
+
"metrics": metrics,
|
|
244
|
+
}
|
|
245
|
+
advice = ai_analyzer.investment_advice(fund_code, fund_data, risk_profile)
|
|
246
|
+
|
|
247
|
+
# 格式化输出
|
|
248
|
+
table = Table(title=f"AI投资建议 - {fund_code} ({risk_profile})")
|
|
249
|
+
table.add_column("项目", style="cyan")
|
|
250
|
+
table.add_column("内容", style="green")
|
|
251
|
+
|
|
252
|
+
table.add_row("适合性", advice.get("suitability", "未知"))
|
|
253
|
+
table.add_row("配置比例", advice.get("allocation", "未知"))
|
|
254
|
+
table.add_row("风险提示", advice.get("risk_warning", "未知"))
|
|
255
|
+
table.add_row("持有建议", advice.get("holding_period", "未知"))
|
|
256
|
+
|
|
257
|
+
console.print(table)
|
|
258
|
+
|
|
259
|
+
if output:
|
|
260
|
+
import json
|
|
261
|
+
|
|
262
|
+
Path(output).write_text(
|
|
263
|
+
json.dumps(advice, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
264
|
+
)
|
|
265
|
+
console.print(f"[green]结果已保存至: {output}[/green]")
|
|
266
|
+
|
|
267
|
+
except Exception as e:
|
|
268
|
+
console.print(f"[red]生成建议失败: {e}[/red]")
|
|
269
|
+
raise typer.Exit(1) from None
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@app.command("risk")
|
|
273
|
+
def ai_risk(
|
|
274
|
+
fund_code: str = typer.Argument(..., help="基金代码"),
|
|
275
|
+
detailed: bool = typer.Option(False, "--detailed", "-d", help="详细分析"),
|
|
276
|
+
output: str = typer.Option(None, "--output", "-o", help="输出文件路径"),
|
|
277
|
+
) -> None:
|
|
278
|
+
"""
|
|
279
|
+
AI深度风险评估
|
|
280
|
+
|
|
281
|
+
示例:
|
|
282
|
+
fund ai risk 000001
|
|
283
|
+
fund ai risk 000001 --detailed
|
|
284
|
+
fund ai risk 000001 --detailed --output risk.txt
|
|
285
|
+
"""
|
|
286
|
+
from fund_cli.ai.analyzer import AIAnalyzer
|
|
287
|
+
from fund_cli.data.adapters import get_adapter
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
# 获取基金数据
|
|
291
|
+
adapter = get_adapter()
|
|
292
|
+
fund_info = adapter.get_fund_info(fund_code)
|
|
293
|
+
nav_data = adapter.get_fund_nav(fund_code, period="1y")
|
|
294
|
+
|
|
295
|
+
# 生成风险评估
|
|
296
|
+
ai_analyzer = AIAnalyzer()
|
|
297
|
+
fund_data = {
|
|
298
|
+
"code": fund_code,
|
|
299
|
+
"info": fund_info,
|
|
300
|
+
"nav": nav_data,
|
|
301
|
+
}
|
|
302
|
+
risk_assessment = ai_analyzer.risk_assessment(fund_code, fund_data, detailed)
|
|
303
|
+
|
|
304
|
+
# 格式化输出
|
|
305
|
+
table = Table(title=f"AI风险评估 - {fund_code}")
|
|
306
|
+
table.add_column("项目", style="cyan")
|
|
307
|
+
table.add_column("内容", style="green")
|
|
308
|
+
|
|
309
|
+
table.add_row("风险等级", risk_assessment.get("risk_level", "未知"))
|
|
310
|
+
table.add_row("主要风险", risk_assessment.get("main_risks", "未知"))
|
|
311
|
+
table.add_row("风险预警", risk_assessment.get("warnings", "无"))
|
|
312
|
+
table.add_row("控制建议", risk_assessment.get("control_suggestions", "未知"))
|
|
313
|
+
|
|
314
|
+
if detailed:
|
|
315
|
+
table.add_row("详细分析", risk_assessment.get("detailed_analysis", "未知"))
|
|
316
|
+
|
|
317
|
+
console.print(table)
|
|
318
|
+
|
|
319
|
+
if output:
|
|
320
|
+
import json
|
|
321
|
+
|
|
322
|
+
Path(output).write_text(
|
|
323
|
+
json.dumps(risk_assessment, ensure_ascii=False, indent=2),
|
|
324
|
+
encoding="utf-8",
|
|
325
|
+
)
|
|
326
|
+
console.print(f"[green]结果已保存至: {output}[/green]")
|
|
327
|
+
|
|
328
|
+
except Exception as e:
|
|
329
|
+
console.print(f"[red]风险评估失败: {e}[/red]")
|
|
330
|
+
raise typer.Exit(1) from None
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@app.command("insight")
|
|
334
|
+
def ai_insight(
|
|
335
|
+
fund_code: str = typer.Argument(..., help="基金代码"),
|
|
336
|
+
market_context: str = typer.Option(None, "--context", "-c", help="市场环境描述"),
|
|
337
|
+
output: str = typer.Option(None, "--output", "-o", help="输出文件路径"),
|
|
338
|
+
) -> None:
|
|
339
|
+
"""
|
|
340
|
+
AI市场解读
|
|
341
|
+
|
|
342
|
+
示例:
|
|
343
|
+
fund ai insight 000001
|
|
344
|
+
fund ai insight 000001 --context "当前市场处于震荡期"
|
|
345
|
+
fund ai insight 000001 --output insight.txt
|
|
346
|
+
"""
|
|
347
|
+
from fund_cli.ai.analyzer import AIAnalyzer
|
|
348
|
+
from fund_cli.data.adapters import get_adapter
|
|
349
|
+
|
|
350
|
+
try:
|
|
351
|
+
# 获取基金数据
|
|
352
|
+
adapter = get_adapter()
|
|
353
|
+
fund_info = adapter.get_fund_info(fund_code)
|
|
354
|
+
nav_data = adapter.get_fund_nav(fund_code, period="1y")
|
|
355
|
+
|
|
356
|
+
# 生成市场解读
|
|
357
|
+
ai_analyzer = AIAnalyzer()
|
|
358
|
+
fund_data = {
|
|
359
|
+
"code": fund_code,
|
|
360
|
+
"info": fund_info,
|
|
361
|
+
"nav": nav_data,
|
|
362
|
+
}
|
|
363
|
+
insight = ai_analyzer.market_insight(fund_code, fund_data, market_context)
|
|
364
|
+
|
|
365
|
+
# 输出结果
|
|
366
|
+
console.print(Panel(insight, title=f"AI市场解读 - {fund_code}"))
|
|
367
|
+
|
|
368
|
+
if output:
|
|
369
|
+
Path(output).write_text(insight, encoding="utf-8")
|
|
370
|
+
console.print(f"[green]结果已保存至: {output}[/green]")
|
|
371
|
+
|
|
372
|
+
except Exception as e:
|
|
373
|
+
console.print(f"[red]市场解读失败: {e}[/red]")
|
|
374
|
+
raise typer.Exit(1) from None
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
@app.command("portfolio")
|
|
378
|
+
def ai_portfolio(
|
|
379
|
+
fund_codes: str = typer.Argument(..., help="基金代码列表(逗号分隔)"),
|
|
380
|
+
weights: str = typer.Option(
|
|
381
|
+
None, "--weights", "-w", help="权重配置(逗号分隔,如0.5,0.3,0.2)"
|
|
382
|
+
),
|
|
383
|
+
output: str = typer.Option(None, "--output", "-o", help="输出文件路径"),
|
|
384
|
+
) -> None:
|
|
385
|
+
"""
|
|
386
|
+
AI组合分析
|
|
387
|
+
|
|
388
|
+
示例:
|
|
389
|
+
fund ai portfolio 000001,000002
|
|
390
|
+
fund ai portfolio 000001,000002 --weights 0.6,0.4
|
|
391
|
+
fund ai portfolio 000001,000002,000003 --weights 0.5,0.3,0.2
|
|
392
|
+
"""
|
|
393
|
+
from fund_cli.ai.analyzer import AIAnalyzer
|
|
394
|
+
from fund_cli.analysis.performance import PerformanceAnalyzer
|
|
395
|
+
from fund_cli.data.adapters import get_adapter
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
codes = [c.strip() for c in fund_codes.split(",")]
|
|
399
|
+
if len(codes) < 2:
|
|
400
|
+
console.print("[red]请至少提供2只基金进行组合分析[/red]")
|
|
401
|
+
raise typer.Exit(1) from None
|
|
402
|
+
|
|
403
|
+
# 解析权重
|
|
404
|
+
weight_list = None
|
|
405
|
+
if weights:
|
|
406
|
+
weight_list = [float(w.strip()) for w in weights.split(",")]
|
|
407
|
+
if len(weight_list) != len(codes):
|
|
408
|
+
console.print("[red]权重数量必须与基金数量一致[/red]")
|
|
409
|
+
raise typer.Exit(1) from None
|
|
410
|
+
if abs(sum(weight_list) - 1.0) > 0.01:
|
|
411
|
+
console.print("[red]权重总和必须等于1[/red]")
|
|
412
|
+
raise typer.Exit(1) from None
|
|
413
|
+
|
|
414
|
+
# 获取基金数据
|
|
415
|
+
adapter = get_adapter()
|
|
416
|
+
perf_analyzer = PerformanceAnalyzer()
|
|
417
|
+
|
|
418
|
+
portfolio_data = {
|
|
419
|
+
"funds": [],
|
|
420
|
+
"weights": weight_list or [1.0 / len(codes)] * len(codes),
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
for code in codes:
|
|
424
|
+
fund_info = adapter.get_fund_info(code)
|
|
425
|
+
nav_data = adapter.get_fund_nav(code, period="1y")
|
|
426
|
+
metrics = perf_analyzer.calculate_metrics(nav_data)
|
|
427
|
+
portfolio_data["funds"].append(
|
|
428
|
+
{
|
|
429
|
+
"code": code,
|
|
430
|
+
"info": fund_info,
|
|
431
|
+
"metrics": metrics,
|
|
432
|
+
}
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
# 生成组合分析
|
|
436
|
+
ai_analyzer = AIAnalyzer()
|
|
437
|
+
review = ai_analyzer.portfolio_review(portfolio_data)
|
|
438
|
+
|
|
439
|
+
# 格式化输出
|
|
440
|
+
table = Table(title=f"AI组合分析 - {', '.join(codes)}")
|
|
441
|
+
table.add_column("项目", style="cyan")
|
|
442
|
+
table.add_column("内容", style="green")
|
|
443
|
+
|
|
444
|
+
table.add_row("组合评价", review.get("overall_assessment", "未知"))
|
|
445
|
+
table.add_row("优化建议", review.get("optimization_suggestions", "未知"))
|
|
446
|
+
table.add_row("风险分散度", review.get("diversification", "未知"))
|
|
447
|
+
|
|
448
|
+
console.print(table)
|
|
449
|
+
|
|
450
|
+
if output:
|
|
451
|
+
import json
|
|
452
|
+
|
|
453
|
+
Path(output).write_text(
|
|
454
|
+
json.dumps(review, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
455
|
+
)
|
|
456
|
+
console.print(f"[green]结果已保存至: {output}[/green]")
|
|
457
|
+
|
|
458
|
+
except Exception as e:
|
|
459
|
+
console.print(f"[red]组合分析失败: {e}[/red]")
|
|
460
|
+
raise typer.Exit(1) from None
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
if __name__ == "__main__":
|
|
464
|
+
app()
|