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,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"]
|