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,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()