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,84 @@
1
+ """交互式模式"""
2
+
3
+ import typer
4
+ from rich import box
5
+ from rich.console import Console
6
+ from rich.panel import Panel
7
+
8
+ app = typer.Typer(help="交互式模式")
9
+ console = Console()
10
+
11
+
12
+ @app.command()
13
+ def interactive_mode():
14
+ """启动交互式REPL模式 (CLI-UX-006)"""
15
+ try:
16
+ from prompt_toolkit import PromptSession
17
+ from prompt_toolkit.completion import WordCompleter
18
+ from prompt_toolkit.history import InMemoryHistory
19
+ except ImportError:
20
+ console.print("[yellow]需要安装 prompt_toolkit: pip install prompt_toolkit[/yellow]")
21
+ raise typer.Exit(1) from None
22
+
23
+ commands = [
24
+ "info",
25
+ "filter",
26
+ "analyze",
27
+ "compare",
28
+ "optimize",
29
+ "monitor",
30
+ "holding",
31
+ "manager",
32
+ "data",
33
+ "config",
34
+ "help",
35
+ "exit",
36
+ "quit",
37
+ ]
38
+
39
+ completer = WordCompleter(commands, ignore_case=True)
40
+ session = PromptSession(completer=completer, history=InMemoryHistory())
41
+
42
+ console.print(
43
+ Panel(
44
+ "[bold]Fund CLI 交互式模式[/bold]\n"
45
+ "输入命令(如 info 000001)或 help 查看帮助\n"
46
+ "输入 exit 或 quit 退出",
47
+ box=box.ROUNDED,
48
+ border_style="blue",
49
+ )
50
+ )
51
+
52
+ while True:
53
+ try:
54
+ user_input = session.prompt("fund> ").strip()
55
+ except (EOFError, KeyboardInterrupt):
56
+ console.print("\n[yellow]再见![/yellow]")
57
+ break
58
+
59
+ if not user_input:
60
+ continue
61
+
62
+ if user_input.lower() in ("exit", "quit"):
63
+ console.print("[yellow]再见![/yellow]")
64
+ break
65
+
66
+ if user_input.lower() == "help":
67
+ console.print("可用命令: info, filter, analyze, compare, optimize,")
68
+ console.print(" monitor, holding, manager, data, config")
69
+ console.print("输入 exit 或 quit 退出")
70
+ continue
71
+
72
+ # 执行命令
73
+ try:
74
+ from typer.testing import CliRunner
75
+
76
+ from fund_cli.cli import app as main_app
77
+
78
+ runner = CliRunner()
79
+ args = user_input.split()
80
+ result = runner.invoke(main_app, args)
81
+ if result.output:
82
+ console.print(result.output)
83
+ except Exception as e:
84
+ console.print(f"[red]错误: {e}[/red]")
@@ -0,0 +1,17 @@
1
+ """
2
+ 命令主入口
3
+
4
+ CLI 命令模块的主入口,提供命令注册辅助功能。
5
+ """
6
+
7
+
8
+ def register_commands(app, commands: dict) -> None:
9
+ """
10
+ 批量注册子命令
11
+
12
+ Args:
13
+ app: Typer 主应用
14
+ commands: {名称: 子应用} 字典
15
+ """
16
+ for name, sub_app in commands.items():
17
+ app.add_typer(sub_app, name=name)
@@ -0,0 +1,74 @@
1
+ """
2
+ 基金经理命令
3
+
4
+ 提供基金经理信息查询、业绩统计和稳定性分析功能。
5
+ """
6
+
7
+ import typer
8
+ from rich.console import Console
9
+ from rich.table import Table
10
+
11
+ from fund_cli.analysis.manager import ManagerAnalyzer
12
+ from fund_cli.core.data_manager import DataManager
13
+
14
+ app = typer.Typer(help="基金经理命令")
15
+ console = Console()
16
+
17
+
18
+ @app.command("info")
19
+ def manager_info(fund_code: str = typer.Argument(..., help="基金代码")):
20
+ """查询基金经理信息 (FUND-MANAGER-001)"""
21
+ dm = DataManager()
22
+ try:
23
+ manager_data = dm.get_fund_manager(fund_code)
24
+ analyzer = ManagerAnalyzer()
25
+ info = analyzer.manager_info(manager_data)
26
+ console.print("\n[bold]基金经理信息[/bold]")
27
+ console.print(f" 姓名: {info['name']}")
28
+ console.print(f" 基金: {info['fund_name']}")
29
+ console.print(f" 公司: {info['company']}")
30
+ console.print(f" 任职日期: {info['start_date']}")
31
+ console.print(f" 任职天数: {info['tenure_days']}")
32
+ except Exception as e:
33
+ console.print(f"[red]获取经理信息失败: {e}[/red]")
34
+ raise typer.Exit(1) from None
35
+
36
+
37
+ @app.command("performance")
38
+ def manager_performance(fund_code: str = typer.Argument(..., help="基金代码")):
39
+ """经理业绩统计 (FUND-MANAGER-002)"""
40
+ dm = DataManager()
41
+ try:
42
+ manager_data = dm.get_fund_manager(fund_code)
43
+ analyzer = ManagerAnalyzer()
44
+ stats = analyzer.performance_stats(manager_data)
45
+ console.print(f"\n[bold]{manager_data.get('name', '')}[/bold] 业绩统计:")
46
+ table = Table(show_header=True, header_style="bold blue")
47
+ table.add_column("指标", style="cyan")
48
+ table.add_column("值", justify="right")
49
+ table.add_row("管理基金数", str(stats["total_funds"]))
50
+ table.add_row("平均收益率", f"{stats['avg_return']:.2f}%")
51
+ table.add_row("最佳基金", stats["best_fund"])
52
+ table.add_row("最佳收益率", f"{stats['best_return']:.2f}%")
53
+ console.print(table)
54
+ except Exception as e:
55
+ console.print(f"[red]业绩统计失败: {e}[/red]")
56
+ raise typer.Exit(1) from None
57
+
58
+
59
+ @app.command("stability")
60
+ def manager_stability(fund_code: str = typer.Argument(..., help="基金代码")):
61
+ """经理稳定性分析 (FUND-MANAGER-003)"""
62
+ dm = DataManager()
63
+ try:
64
+ manager_data = dm.get_fund_manager(fund_code)
65
+ analyzer = ManagerAnalyzer()
66
+ stability = analyzer.stability_analysis(manager_data)
67
+ console.print(f"\n[bold]{manager_data.get('name', '')}[/bold] 稳定性分析:")
68
+ console.print(f" 任职年限: {stability['tenure_years']}年")
69
+ console.print(f" 稳定性等级: [bold]{stability['stability_level']}[/bold]")
70
+ console.print(f" 稳定性评分: {stability['stability_score']}/5")
71
+ console.print(f" 多基金管理: {'是' if stability['multi_fund_manager'] else '否'}")
72
+ except Exception as e:
73
+ console.print(f"[red]稳定性分析失败: {e}[/red]")
74
+ raise typer.Exit(1) from None
@@ -0,0 +1,113 @@
1
+ """监控预警命令"""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+
7
+ app = typer.Typer(help="监控预警命令")
8
+ console = Console()
9
+
10
+
11
+ def _get_monitor(config_dir: str = "~/.fund_cli"):
12
+ from fund_cli.core.monitor import FundMonitor
13
+
14
+ return FundMonitor(config_dir=config_dir)
15
+
16
+
17
+ @app.command("add")
18
+ def add_to_pool(
19
+ fund_code: str = typer.Argument(..., help="基金代码"),
20
+ group: str = typer.Option("default", help="分组名称"),
21
+ ):
22
+ """添加基金到监控池 (FUND-MONITOR-001)"""
23
+ monitor = _get_monitor()
24
+ monitor.add_to_pool(fund_code, group)
25
+ console.print(f"[green]已添加 {fund_code} 到 {group} 监控池[/green]")
26
+
27
+
28
+ @app.command("remove")
29
+ def remove_from_pool(
30
+ fund_code: str = typer.Argument(..., help="基金代码"),
31
+ group: str = typer.Option(None, help="分组名称"),
32
+ ):
33
+ """从监控池移除基金"""
34
+ monitor = _get_monitor()
35
+ if monitor.remove_from_pool(fund_code, group):
36
+ console.print(f"[green]已从监控池移除 {fund_code}[/green]")
37
+ else:
38
+ console.print(f"[yellow]{fund_code} 不在监控池中[/yellow]")
39
+
40
+
41
+ @app.command("list")
42
+ def list_pool(
43
+ group: str = typer.Option(None, help="分组名称"),
44
+ ):
45
+ """列出监控池中的基金 (FUND-MONITOR-001)"""
46
+ monitor = _get_monitor()
47
+ funds = monitor.list_pool(group)
48
+ if not funds:
49
+ console.print("[yellow]监控池为空[/yellow]")
50
+ return
51
+
52
+ table = Table(show_header=True, header_style="bold blue")
53
+ table.add_column("基金代码", style="cyan")
54
+ table.add_column("分组")
55
+ table.add_column("添加时间")
56
+ for f in funds:
57
+ table.add_row(f["code"], f.get("group", "default"), f.get("added_at", ""))
58
+ console.print(table)
59
+ console.print(f"\n共 {len(funds)} 只基金")
60
+
61
+
62
+ @app.command("watch")
63
+ def watch_fund(
64
+ fund_code: str = typer.Argument(..., help="基金代码"),
65
+ threshold: float = typer.Option(-2.0, help="预警阈值(%)"),
66
+ ):
67
+ """监控基金净值变动 (FUND-MONITOR-002)"""
68
+ monitor = _get_monitor()
69
+ monitor.add_to_pool(fund_code)
70
+ monitor.add_rule(fund_code, "nav_change", threshold)
71
+ console.print(f"[green]已开始监控 {fund_code},阈值: {threshold}%[/green]")
72
+
73
+
74
+ @app.command("check")
75
+ def check_all():
76
+ """检查所有监控基金的净值变动"""
77
+ monitor = _get_monitor()
78
+ codes = monitor.get_all_fund_codes()
79
+ if not codes:
80
+ console.print("[yellow]监控池为空,请先添加基金[/yellow]")
81
+ return
82
+
83
+ console.print(f"正在检查 {len(codes)} 只基金...")
84
+ alerts = monitor.check_nav_changes(codes)
85
+ if alerts:
86
+ table = Table(show_header=True, header_style="bold red")
87
+ table.add_column("基金代码", style="cyan")
88
+ table.add_column("日收益率", justify="right", style="red")
89
+ table.add_column("阈值", justify="right")
90
+ for a in alerts:
91
+ table.add_row(a["fund_code"], f"{a['daily_return']:.2f}%", f"{a['threshold']:.2f}%")
92
+ console.print(table)
93
+ console.print(f"\n[bold red]{len(alerts)} 只基金触发预警[/bold red]")
94
+ else:
95
+ console.print("[green]所有基金正常,无预警[/green]")
96
+
97
+
98
+ @app.command("alert")
99
+ def alert_list():
100
+ """查看预警规则"""
101
+ monitor = _get_monitor()
102
+ rules = monitor.get_rules()
103
+ if not rules:
104
+ console.print("[yellow]暂无预警规则[/yellow]")
105
+ return
106
+
107
+ table = Table(show_header=True, header_style="bold blue")
108
+ table.add_column("基金代码", style="cyan")
109
+ table.add_column("规则类型")
110
+ table.add_column("阈值", justify="right")
111
+ for r in rules:
112
+ table.add_row(r["fund_code"], r["rule_type"], f"{r['threshold']:.2f}%")
113
+ console.print(table)
@@ -0,0 +1,192 @@
1
+ """组合优化命令"""
2
+
3
+ import typer
4
+ from rich.console import Console
5
+ from rich.table import Table
6
+
7
+ from fund_cli.data.models import OptimizationConstraint
8
+
9
+ app = typer.Typer(help="组合优化命令")
10
+ console = Console()
11
+
12
+
13
+ def _get_returns(fund_codes: list[str], period: str):
14
+ """获取多基金收益率数据"""
15
+ from datetime import date, timedelta
16
+
17
+ import pandas as pd
18
+
19
+ from fund_cli.core.data_manager import DataManager
20
+
21
+ dm = DataManager()
22
+ end = date.today()
23
+ days_map = {"1m": 30, "3m": 90, "6m": 180, "1y": 365, "2y": 730, "3y": 1095}
24
+ start = end - timedelta(days=days_map.get(period, 365))
25
+
26
+ all_nav = {}
27
+ for code in fund_codes:
28
+ try:
29
+ nav_df = dm.get_fund_nav(code, start_date=start, end_date=end)
30
+ if not nav_df.empty and "daily_return" in nav_df.columns:
31
+ nav_df = nav_df.dropna(subset=["daily_return"])
32
+ nav_df["daily_return"] = nav_df["daily_return"] / 100.0
33
+ all_nav[code] = nav_df.set_index("nav_date")["daily_return"]
34
+ except Exception:
35
+ continue
36
+
37
+ if not all_nav:
38
+ return None
39
+
40
+ return pd.DataFrame(all_nav)
41
+
42
+
43
+ @app.command("mean-variance")
44
+ def mean_variance(
45
+ fund_codes: str = typer.Argument(..., help="基金代码,逗号分隔"),
46
+ period: str = typer.Option("1y", help="分析周期"),
47
+ risk_free: float = typer.Option(0.03, help="无风险利率"),
48
+ min_weight: float = typer.Option(0.0, help="最小权重"),
49
+ max_weight: float = typer.Option(1.0, help="最大权重"),
50
+ ):
51
+ """均值-方差优化 (PORTFOLIO-OPT-001)"""
52
+ codes = [c.strip() for c in fund_codes.split(",")]
53
+ returns = _get_returns(codes, period)
54
+ if returns is None or returns.empty:
55
+ console.print("[red]无法获取收益率数据[/red]")
56
+ raise typer.Exit(1) from None
57
+
58
+ from fund_cli.core.optimizers.mean_variance import MeanVarianceOptimizer
59
+
60
+ constraints = OptimizationConstraint(min_weight=min_weight, max_weight=max_weight)
61
+ optimizer = MeanVarianceOptimizer(risk_free_rate=risk_free)
62
+ result = optimizer.optimize(returns, constraints)
63
+
64
+ console.print("\n[bold]均值-方差优化结果[/bold]")
65
+ _print_result(result)
66
+
67
+
68
+ @app.command("max-sharpe")
69
+ def max_sharpe(
70
+ fund_codes: str = typer.Argument(..., help="基金代码,逗号分隔"),
71
+ period: str = typer.Option("1y", help="分析周期"),
72
+ risk_free: float = typer.Option(0.03, help="无风险利率"),
73
+ min_weight: float = typer.Option(0.0, help="最小权重"),
74
+ max_weight: float = typer.Option(1.0, help="最大权重"),
75
+ ):
76
+ """最大夏普比率优化 (PORTFOLIO-OPT-002)"""
77
+ codes = [c.strip() for c in fund_codes.split(",")]
78
+ returns = _get_returns(codes, period)
79
+ if returns is None or returns.empty:
80
+ console.print("[red]无法获取收益率数据[/red]")
81
+ raise typer.Exit(1) from None
82
+
83
+ from fund_cli.core.optimizers.max_sharpe import MaxSharpeOptimizer
84
+
85
+ constraints = OptimizationConstraint(min_weight=min_weight, max_weight=max_weight)
86
+ optimizer = MaxSharpeOptimizer(risk_free_rate=risk_free)
87
+ result = optimizer.optimize(returns, constraints)
88
+
89
+ console.print("\n[bold]最大夏普比率优化结果[/bold]")
90
+ _print_result(result)
91
+
92
+
93
+ @app.command("risk-parity")
94
+ def risk_parity(
95
+ fund_codes: str = typer.Argument(..., help="基金代码,逗号分隔"),
96
+ period: str = typer.Option("1y", help="分析周期"),
97
+ ):
98
+ """风险平价优化 (PORTFOLIO-OPT-003)"""
99
+ codes = [c.strip() for c in fund_codes.split(",")]
100
+ returns = _get_returns(codes, period)
101
+ if returns is None or returns.empty:
102
+ console.print("[red]无法获取收益率数据[/red]")
103
+ raise typer.Exit(1) from None
104
+
105
+ from fund_cli.core.optimizers.risk_parity import RiskParityOptimizer
106
+
107
+ optimizer = RiskParityOptimizer()
108
+ result = optimizer.optimize(returns)
109
+
110
+ console.print("\n[bold]风险平价优化结果[/bold]")
111
+ _print_result(result)
112
+
113
+
114
+ @app.command("frontier")
115
+ def efficient_frontier(
116
+ fund_codes: str = typer.Argument(..., help="基金代码,逗号分隔"),
117
+ period: str = typer.Option("1y", help="分析周期"),
118
+ points: int = typer.Option(50, help="前沿点数"),
119
+ ):
120
+ """有效前沿计算 (PORTFOLIO-OPT-006)"""
121
+ codes = [c.strip() for c in fund_codes.split(",")]
122
+ returns = _get_returns(codes, period)
123
+ if returns is None or returns.empty:
124
+ console.print("[red]无法获取收益率数据[/red]")
125
+ raise typer.Exit(1) from None
126
+
127
+ from fund_cli.core.optimizers.efficient_frontier import EfficientFrontierCalculator
128
+
129
+ calc = EfficientFrontierCalculator()
130
+ result = calc.calculate(returns, n_points=points)
131
+
132
+ console.print(f"\n[bold]有效前沿[/bold] ({result['n_points']}个点)")
133
+ console.print(
134
+ f" 收益范围: {min(result['frontier_returns']):.2%} ~ {max(result['frontier_returns']):.2%}"
135
+ )
136
+ console.print(
137
+ f" 波动范围: {min(result['frontier_volatilities']):.2%} ~ {max(result['frontier_volatilities']):.2%}"
138
+ )
139
+
140
+
141
+ @app.command("backtest")
142
+ def backtest(
143
+ fund_codes: str = typer.Argument(..., help="基金代码,逗号分隔"),
144
+ weights: str = typer.Option(None, help="权重,逗号分隔(如0.4,0.3,0.3)"),
145
+ period: str = typer.Option("1y", help="分析周期"),
146
+ rebalance: str = typer.Option("monthly", help="再平衡频率"),
147
+ ):
148
+ """组合回测 (PORTFOLIO-OPT-007)"""
149
+ codes = [c.strip() for c in fund_codes.split(",")]
150
+ returns = _get_returns(codes, period)
151
+ if returns is None or returns.empty:
152
+ console.print("[red]无法获取收益率数据[/red]")
153
+ raise typer.Exit(1) from None
154
+
155
+ w = None
156
+ if weights:
157
+ w_list = [float(x.strip()) for x in weights.split(",")]
158
+ if len(w_list) == len(codes):
159
+ w = dict(zip(codes, w_list, strict=False))
160
+
161
+ from fund_cli.analysis.backtest import BacktestAnalyzer
162
+
163
+ analyzer = BacktestAnalyzer()
164
+ result = analyzer.run_backtest(returns, weights=w, rebalance_freq=rebalance)
165
+
166
+ console.print("\n[bold]组合回测结果[/bold]")
167
+ table = Table(show_header=True, header_style="bold blue")
168
+ table.add_column("指标", style="cyan")
169
+ table.add_column("值", justify="right")
170
+ table.add_row("累计收益率", f"{result['total_return']:.2f}%")
171
+ table.add_row("年化收益率", f"{result['annual_return']:.2f}%")
172
+ table.add_row("年化波动率", f"{result['annual_volatility']:.2f}%")
173
+ table.add_row("夏普比率", f"{result['sharpe_ratio']:.4f}")
174
+ table.add_row("最大回撤", f"{result['max_drawdown']:.2f}%")
175
+ table.add_row("日胜率", f"{result['win_rate']:.1f}%")
176
+ table.add_row("交易天数", str(result["trading_days"]))
177
+ console.print(table)
178
+
179
+
180
+ def _print_result(result: dict) -> None:
181
+ """打印优化结果"""
182
+ console.print(f" 方法: {result['method']}")
183
+ console.print(f" 预期收益: {result['expected_return']:.2%}")
184
+ console.print(f" 预期波动: {result['volatility']:.2%}")
185
+ console.print(f" 夏普比率: {result['sharpe_ratio']:.4f}")
186
+ console.print("\n [bold]权重分配:[/bold]")
187
+ table = Table(show_header=True, header_style="bold blue")
188
+ table.add_column("基金代码", style="cyan")
189
+ table.add_column("权重", justify="right")
190
+ for code, weight in sorted(result["weights"].items(), key=lambda x: x[1], reverse=True):
191
+ table.add_row(code, f"{weight:.2%}")
192
+ console.print(table)
fund_cli/config.py ADDED
@@ -0,0 +1,163 @@
1
+ """
2
+ Fund CLI 配置管理
3
+
4
+ 使用 Pydantic Settings 管理应用配置,支持环境变量和 .env 文件。
5
+ """
6
+
7
+ from pathlib import Path
8
+
9
+ from pydantic import Field, field_validator
10
+ from pydantic_settings import BaseSettings, SettingsConfigDict
11
+
12
+
13
+ class DataConfig(BaseSettings):
14
+ """数据源配置"""
15
+
16
+ model_config = SettingsConfigDict(env_prefix="FUND_DATA_")
17
+
18
+ # AKShare 配置
19
+ akshare_enabled: bool = Field(default=True, description="是否启用AKShare数据源")
20
+
21
+ # Tushare 配置
22
+ tushare_token: str | None = Field(default=None, description="Tushare API Token")
23
+
24
+ # Wind 配置
25
+ wind_enabled: bool = Field(default=False, description="是否启用Wind数据源")
26
+ wind_username: str | None = Field(default=None, description="Wind用户名")
27
+ wind_password: str | None = Field(default=None, description="Wind密码")
28
+
29
+ # 缓存配置
30
+ cache_ttl: int = Field(default=3600, description="缓存过期时间(秒)")
31
+ cache_dir: str = Field(default="~/.fund_cli/cache", description="缓存目录")
32
+
33
+ @field_validator("cache_dir")
34
+ @classmethod
35
+ def expand_cache_dir(cls, v: str) -> str:
36
+ """展开缓存目录路径"""
37
+ return str(Path(v).expanduser())
38
+
39
+
40
+ class AnalysisConfig(BaseSettings):
41
+ """分析配置"""
42
+
43
+ model_config = SettingsConfigDict(env_prefix="FUND_ANALYSIS_")
44
+
45
+ risk_free_rate: float = Field(default=0.03, description="无风险利率")
46
+ default_benchmark: str = Field(default="000300", description="默认基准指数代码")
47
+ default_period: str = Field(default="1y", description="默认分析周期")
48
+
49
+ @field_validator("risk_free_rate")
50
+ @classmethod
51
+ def validate_risk_free_rate(cls, v: float) -> float:
52
+ """验证无风险利率范围"""
53
+ if not 0 <= v <= 1:
54
+ raise ValueError("无风险利率必须在 0-1 之间")
55
+ return v
56
+
57
+
58
+ class AIConfig(BaseSettings):
59
+ """AI配置(V2.0功能)"""
60
+
61
+ model_config = SettingsConfigDict(env_prefix="FUND_AI_")
62
+
63
+ provider: str = Field(default="openai", description="LLM提供商: openai/qwen/litellm")
64
+ model: str = Field(default="gpt-4", description="模型名称")
65
+ api_key: str | None = Field(default=None, description="API密钥")
66
+ api_base: str | None = Field(default=None, description="API基础URL")
67
+ temperature: float = Field(default=0.7, description="温度参数(0-2)")
68
+ max_tokens: int = Field(default=2000, description="最大token数")
69
+ timeout: int = Field(default=30, description="请求超时(秒)")
70
+ retry_count: int = Field(default=3, description="重试次数")
71
+
72
+ # Qwen专用配置
73
+ qwen_api_key: str | None = Field(default=None, description="阿里云Qwen API Key")
74
+ qwen_base_url: str = Field(
75
+ default="https://dashscope.aliyuncs.com/api/v1", description="Qwen API地址"
76
+ )
77
+ qwen_model: str = Field(
78
+ default="qwen-max", description="Qwen模型: qwen-max/qwen-plus/qwen-turbo"
79
+ )
80
+
81
+ @field_validator("temperature")
82
+ @classmethod
83
+ def validate_temperature(cls, v: float) -> float:
84
+ """验证温度参数范围"""
85
+ if not 0 <= v <= 2:
86
+ raise ValueError("温度参数必须在 0-2 之间")
87
+ return v
88
+
89
+ @field_validator("retry_count")
90
+ @classmethod
91
+ def validate_retry_count(cls, v: int) -> int:
92
+ """验证重试次数"""
93
+ if not 0 <= v <= 10:
94
+ raise ValueError("重试次数必须在 0-10 之间")
95
+ return v
96
+
97
+
98
+ class LogConfig(BaseSettings):
99
+ """日志配置"""
100
+
101
+ model_config = SettingsConfigDict(env_prefix="FUND_LOG_")
102
+
103
+ level: str = Field(default="INFO", description="日志级别")
104
+ file: str | None = Field(default=None, description="日志文件路径")
105
+
106
+
107
+ class OutputConfig(BaseSettings):
108
+ """输出格式配置"""
109
+
110
+ model_config = SettingsConfigDict(env_prefix="FUND_OUTPUT_")
111
+
112
+ default_format: str = Field(default="table", description="默认输出格式")
113
+ csv_encoding: str = Field(default="utf-8-sig", description="CSV编码")
114
+ csv_delimiter: str = Field(default=",", description="CSV分隔符")
115
+ json_indent: int = Field(default=2, description="JSON缩进")
116
+ number_decimal: int = Field(default=2, description="数字小数位")
117
+ date_format: str = Field(default="%Y-%m-%d", description="日期格式")
118
+
119
+
120
+ class AppConfig(BaseSettings):
121
+ """应用主配置"""
122
+
123
+ model_config = SettingsConfigDict(
124
+ env_file=".env",
125
+ env_file_encoding="utf-8",
126
+ env_nested_delimiter="__",
127
+ extra="ignore",
128
+ )
129
+
130
+ # 应用信息
131
+ app_name: str = Field(default="Fund CLI", description="应用名称")
132
+ debug: bool = Field(default=False, description="调试模式")
133
+ dev_mode: bool = Field(default=False, description="开发模式")
134
+
135
+ # 嵌套配置
136
+ data: DataConfig = Field(default_factory=DataConfig)
137
+ analysis: AnalysisConfig = Field(default_factory=AnalysisConfig)
138
+ ai: AIConfig = Field(default_factory=AIConfig)
139
+ log: LogConfig = Field(default_factory=LogConfig)
140
+ output: OutputConfig = Field(default_factory=OutputConfig)
141
+
142
+
143
+ # 全局配置实例
144
+ _config: AppConfig | None = None
145
+
146
+
147
+ def get_config() -> AppConfig:
148
+ """获取配置实例(单例模式)"""
149
+ global _config
150
+ if _config is None:
151
+ _config = AppConfig()
152
+ return _config
153
+
154
+
155
+ def reload_config() -> AppConfig:
156
+ """重新加载配置"""
157
+ global _config
158
+ _config = AppConfig()
159
+ return _config
160
+
161
+
162
+ # 便捷访问
163
+ config = get_config()
@@ -0,0 +1,8 @@
1
+ """核心模块 - 数据管理器、分析引擎、优化引擎、报告生成器"""
2
+
3
+ from fund_cli.core.analyzer import Analyzer
4
+ from fund_cli.core.data_manager import DataManager
5
+ from fund_cli.core.optimizer import Optimizer
6
+ from fund_cli.core.reporter import Reporter
7
+
8
+ __all__ = ["DataManager", "Analyzer", "Optimizer", "Reporter"]