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,97 @@
|
|
|
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.config import get_config
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="配置管理命令")
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("show")
|
|
18
|
+
def show_config() -> None:
|
|
19
|
+
"""显示当前配置。"""
|
|
20
|
+
try:
|
|
21
|
+
config = get_config()
|
|
22
|
+
|
|
23
|
+
table = Table(title="当前配置")
|
|
24
|
+
table.add_column("配置项", style="cyan")
|
|
25
|
+
table.add_column("值", style="green")
|
|
26
|
+
|
|
27
|
+
# 应用配置
|
|
28
|
+
table.add_row("应用名称", config.app_name)
|
|
29
|
+
table.add_row("调试模式", str(config.debug))
|
|
30
|
+
|
|
31
|
+
# 数据配置
|
|
32
|
+
table.add_row("AKShare启用", str(config.data.akshare_enabled))
|
|
33
|
+
table.add_row("缓存TTL", f"{config.data.cache_ttl}秒")
|
|
34
|
+
table.add_row("缓存目录", config.data.cache_dir)
|
|
35
|
+
|
|
36
|
+
# 分析配置
|
|
37
|
+
table.add_row("无风险利率", f"{config.analysis.risk_free_rate * 100}%")
|
|
38
|
+
table.add_row("默认基准", config.analysis.default_benchmark)
|
|
39
|
+
|
|
40
|
+
console.print(table)
|
|
41
|
+
|
|
42
|
+
except Exception as e:
|
|
43
|
+
console.print(f"[red]获取配置失败: {e}[/red]")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.command("output")
|
|
47
|
+
def output_config(
|
|
48
|
+
format: str = typer.Option(None, help="默认输出格式: table/csv/json"),
|
|
49
|
+
encoding: str = typer.Option(None, help="CSV编码"),
|
|
50
|
+
decimal: int = typer.Option(None, help="数字小数位"),
|
|
51
|
+
):
|
|
52
|
+
"""输出格式配置 (FUND-CONFIG-004)"""
|
|
53
|
+
from fund_cli.config import AppConfig
|
|
54
|
+
|
|
55
|
+
config = AppConfig()
|
|
56
|
+
if format:
|
|
57
|
+
config.output.default_format = format
|
|
58
|
+
if encoding:
|
|
59
|
+
config.output.csv_encoding = encoding
|
|
60
|
+
if decimal is not None:
|
|
61
|
+
config.output.number_decimal = decimal
|
|
62
|
+
|
|
63
|
+
console.print("\n[bold]输出格式配置[/bold]")
|
|
64
|
+
table = Table(show_header=True, header_style="bold blue")
|
|
65
|
+
table.add_column("配置项", style="cyan")
|
|
66
|
+
table.add_column("当前值")
|
|
67
|
+
table.add_row("默认格式", config.output.default_format)
|
|
68
|
+
table.add_row("CSV编码", config.output.csv_encoding)
|
|
69
|
+
table.add_row("CSV分隔符", config.output.csv_delimiter)
|
|
70
|
+
table.add_row("JSON缩进", str(config.output.json_indent))
|
|
71
|
+
table.add_row("数字小数位", str(config.output.number_decimal))
|
|
72
|
+
table.add_row("日期格式", config.output.date_format)
|
|
73
|
+
console.print(table)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@app.command("set")
|
|
77
|
+
def set_config(
|
|
78
|
+
key: str = typer.Argument(..., help="配置键"),
|
|
79
|
+
value: str = typer.Argument(..., help="配置值"),
|
|
80
|
+
):
|
|
81
|
+
"""设置配置项"""
|
|
82
|
+
from fund_cli.config import AppConfig
|
|
83
|
+
|
|
84
|
+
AppConfig()
|
|
85
|
+
console.print(f"[green]配置 {key} = {value} 已设置[/green]")
|
|
86
|
+
console.print("[yellow]注意: 当前仅显示,持久化配置请编辑 .env 文件[/yellow]")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.callback(invoke_without_command=True)
|
|
90
|
+
def default(ctx: typer.Context) -> None:
|
|
91
|
+
"""默认显示配置"""
|
|
92
|
+
if ctx.invoked_subcommand is None:
|
|
93
|
+
show_config()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
if __name__ == "__main__":
|
|
97
|
+
app()
|
|
@@ -0,0 +1,106 @@
|
|
|
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.core.data_manager import get_data_manager
|
|
12
|
+
|
|
13
|
+
app = typer.Typer(help="数据管理命令")
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@app.command("stats")
|
|
18
|
+
def cache_stats() -> None:
|
|
19
|
+
"""查看数据缓存统计信息。"""
|
|
20
|
+
try:
|
|
21
|
+
dm = get_data_manager()
|
|
22
|
+
stats = dm.get_cache_stats()
|
|
23
|
+
|
|
24
|
+
table = Table(title="缓存统计")
|
|
25
|
+
table.add_column("指标", style="cyan")
|
|
26
|
+
table.add_column("值", style="green")
|
|
27
|
+
|
|
28
|
+
table.add_row("缓存条目数", str(stats.get("size", 0)))
|
|
29
|
+
table.add_row("缓存大小", f"{stats.get('volume', 0) / 1024 / 1024:.2f} MB")
|
|
30
|
+
table.add_row("缓存目录", stats.get("directory", "-"))
|
|
31
|
+
|
|
32
|
+
console.print(table)
|
|
33
|
+
|
|
34
|
+
except Exception as e:
|
|
35
|
+
console.print(f"[red]获取缓存统计失败: {e}[/red]")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.command("clear")
|
|
39
|
+
def clear_cache() -> None:
|
|
40
|
+
"""清空数据缓存。"""
|
|
41
|
+
try:
|
|
42
|
+
dm = get_data_manager()
|
|
43
|
+
dm.clear_cache()
|
|
44
|
+
console.print("[green]缓存已清空[/green]")
|
|
45
|
+
except Exception as e:
|
|
46
|
+
console.print(f"[red]清空缓存失败: {e}[/red]")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@app.command("quality")
|
|
50
|
+
def data_quality(fund_code: str = typer.Argument(..., help="基金代码")):
|
|
51
|
+
"""数据质量检查 (FUND-DATA-005)"""
|
|
52
|
+
from fund_cli.core.data_quality import DataQualityChecker
|
|
53
|
+
|
|
54
|
+
checker = DataQualityChecker()
|
|
55
|
+
result = checker.check(fund_code)
|
|
56
|
+
console.print(f"\n[bold]{fund_code}[/bold] 数据质量报告:")
|
|
57
|
+
console.print(f" 整体状态: [bold]{result.get('overall_status', 'unknown')}[/bold]")
|
|
58
|
+
if "completeness" in result:
|
|
59
|
+
c = result["completeness"]
|
|
60
|
+
console.print(
|
|
61
|
+
f" 完整性评分: {c['score']}/100 (共{c['total_rows']}行, 缺失{sum(c['missing_values'].values())}个)"
|
|
62
|
+
)
|
|
63
|
+
if "accuracy" in result:
|
|
64
|
+
a = result["accuracy"]
|
|
65
|
+
console.print(f" 准确性评分: {a['score']}/100 (异常值{a['anomaly_count']}个)")
|
|
66
|
+
if "timeliness" in result:
|
|
67
|
+
t = result["timeliness"]
|
|
68
|
+
console.print(f" 时效性: {t['status']} (最后更新: {t.get('last_date', 'N/A')})")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@app.command("update")
|
|
72
|
+
def incremental_update(fund_code: str = typer.Argument(..., help="基金代码")):
|
|
73
|
+
"""增量更新 (FUND-DATA-006)"""
|
|
74
|
+
from fund_cli.core.data_quality import DataQualityChecker
|
|
75
|
+
|
|
76
|
+
checker = DataQualityChecker()
|
|
77
|
+
result = checker.incremental_update(fund_code)
|
|
78
|
+
if result["status"] == "success":
|
|
79
|
+
console.print(f"[green]{fund_code}: 新增 {result['new_records']} 条记录[/green]")
|
|
80
|
+
else:
|
|
81
|
+
console.print(f"[red]{fund_code}: 更新失败 - {result.get('message', '')}[/red]")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@app.command("batch-download")
|
|
85
|
+
def batch_download(
|
|
86
|
+
fund_codes: str = typer.Argument(..., help="基金代码,逗号分隔"),
|
|
87
|
+
output: str = typer.Option(None, help="输出目录"),
|
|
88
|
+
):
|
|
89
|
+
"""批量下载 (FUND-DATA-007)"""
|
|
90
|
+
from fund_cli.core.data_quality import DataQualityChecker
|
|
91
|
+
|
|
92
|
+
codes = [c.strip() for c in fund_codes.split(",")]
|
|
93
|
+
checker = DataQualityChecker()
|
|
94
|
+
with console.status("正在批量下载..."):
|
|
95
|
+
result = checker.batch_download(codes)
|
|
96
|
+
console.print("\n[bold]批量下载完成[/bold]")
|
|
97
|
+
console.print(f" 总计: {result['total']} 只基金")
|
|
98
|
+
console.print(f" 成功: [green]{result['success']}[/green]")
|
|
99
|
+
console.print(f" 失败: [red]{result['failed']}[/red]")
|
|
100
|
+
for code, detail in result["details"].items():
|
|
101
|
+
if detail["status"] == "error":
|
|
102
|
+
console.print(f" {code}: {detail.get('message', '失败')}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
app()
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
基金筛选命令
|
|
3
|
+
|
|
4
|
+
提供基金筛选、排序、导出等功能。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
|
|
12
|
+
from fund_cli.core.data_manager import get_data_manager
|
|
13
|
+
from fund_cli.views.tables import TableRenderer
|
|
14
|
+
|
|
15
|
+
app = typer.Typer(help="基金筛选命令")
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class SortField(str, Enum):
|
|
20
|
+
"""排序字段"""
|
|
21
|
+
|
|
22
|
+
return_1y = "return_1y"
|
|
23
|
+
return_3y = "return_3y"
|
|
24
|
+
sharpe = "sharpe"
|
|
25
|
+
max_drawdown = "max_drawdown"
|
|
26
|
+
scale = "scale"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SortOrder(str, Enum):
|
|
30
|
+
"""排序方向"""
|
|
31
|
+
|
|
32
|
+
asc = "asc"
|
|
33
|
+
desc = "desc"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _render_fund_table(df) -> None:
|
|
37
|
+
"""渲染基金列表表格"""
|
|
38
|
+
renderer = TableRenderer()
|
|
39
|
+
table = renderer.render_fund_list(df)
|
|
40
|
+
console.print(table)
|
|
41
|
+
console.print(f"\n[green]共找到 {len(df)} 只基金[/green]")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@app.command("basic")
|
|
45
|
+
def filter_basic(
|
|
46
|
+
fund_type: str | None = typer.Option(None, "--type", "-t", help="基金类型"),
|
|
47
|
+
company: str | None = typer.Option(None, "--company", "-c", help="基金公司"),
|
|
48
|
+
min_scale: float | None = typer.Option(None, "--min-scale", help="最小规模(亿)"),
|
|
49
|
+
max_scale: float | None = typer.Option(None, "--max-scale", help="最大规模(亿)"),
|
|
50
|
+
keyword: str | None = typer.Option(None, "--keyword", "-k", help="关键词搜索"),
|
|
51
|
+
limit: int = typer.Option(20, "--limit", "-l", help="返回数量"),
|
|
52
|
+
output: str | None = typer.Option(None, "--output", "-o", help="输出文件路径"),
|
|
53
|
+
) -> None:
|
|
54
|
+
"""
|
|
55
|
+
基础筛选 - 按基金类型、公司、规模等条件筛选基金。
|
|
56
|
+
|
|
57
|
+
示例:
|
|
58
|
+
fund filter basic --type 股票型 --min-scale 10
|
|
59
|
+
fund filter basic -c 华夏基金 -l 50
|
|
60
|
+
fund filter basic -k 成长
|
|
61
|
+
"""
|
|
62
|
+
try:
|
|
63
|
+
dm = get_data_manager()
|
|
64
|
+
|
|
65
|
+
console.print("[bold blue]正在筛选基金...[/bold blue]")
|
|
66
|
+
|
|
67
|
+
df = dm.search_funds(
|
|
68
|
+
fund_type=fund_type,
|
|
69
|
+
company=company,
|
|
70
|
+
min_scale=min_scale,
|
|
71
|
+
max_scale=max_scale,
|
|
72
|
+
keyword=keyword,
|
|
73
|
+
limit=limit,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
if df.empty:
|
|
77
|
+
console.print("[yellow]未找到符合条件的基金[/yellow]")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
# 显示结果
|
|
81
|
+
renderer = TableRenderer()
|
|
82
|
+
table = renderer.render_fund_list(df)
|
|
83
|
+
console.print(table)
|
|
84
|
+
|
|
85
|
+
console.print(f"\n[green]共找到 {len(df)} 只基金[/green]")
|
|
86
|
+
|
|
87
|
+
# 导出
|
|
88
|
+
if output:
|
|
89
|
+
df.to_csv(output, index=False, encoding="utf-8-sig")
|
|
90
|
+
console.print(f"[green]已导出到: {output}[/green]")
|
|
91
|
+
|
|
92
|
+
except Exception as e:
|
|
93
|
+
console.print(f"[red]筛选失败: {e}[/red]")
|
|
94
|
+
raise typer.Exit(1) from None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@app.command("fee")
|
|
98
|
+
def filter_by_fee(
|
|
99
|
+
max_fee: float = typer.Argument(..., help="最大费率(%)"),
|
|
100
|
+
fund_type: str = typer.Option(None, help="基金类型"),
|
|
101
|
+
limit: int = typer.Option(20, help="返回数量"),
|
|
102
|
+
):
|
|
103
|
+
"""费率筛选 (FUND-FILTER-005)"""
|
|
104
|
+
from fund_cli.core.screener import FundScreener
|
|
105
|
+
|
|
106
|
+
screener = FundScreener()
|
|
107
|
+
try:
|
|
108
|
+
df = screener.screen_by_fee(max_fee, fund_type)
|
|
109
|
+
if df.empty:
|
|
110
|
+
console.print("[yellow]未找到符合条件的基金[/yellow]")
|
|
111
|
+
return
|
|
112
|
+
_render_fund_table(df.head(limit))
|
|
113
|
+
except Exception as e:
|
|
114
|
+
console.print(f"[red]筛选失败: {e}[/red]")
|
|
115
|
+
raise typer.Exit(1) from None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@app.command("manager")
|
|
119
|
+
def filter_by_manager(
|
|
120
|
+
manager_name: str = typer.Argument(..., help="基金经理名称"),
|
|
121
|
+
limit: int = typer.Option(20, help="返回数量"),
|
|
122
|
+
):
|
|
123
|
+
"""经理筛选 (FUND-FILTER-006)"""
|
|
124
|
+
from fund_cli.core.screener import FundScreener
|
|
125
|
+
|
|
126
|
+
screener = FundScreener()
|
|
127
|
+
try:
|
|
128
|
+
df = screener.screen_by_manager(manager_name)
|
|
129
|
+
if df.empty:
|
|
130
|
+
console.print(f"[yellow]未找到经理 {manager_name} 管理的基金[/yellow]")
|
|
131
|
+
return
|
|
132
|
+
_render_fund_table(df.head(limit))
|
|
133
|
+
except Exception as e:
|
|
134
|
+
console.print(f"[red]筛选失败: {e}[/red]")
|
|
135
|
+
raise typer.Exit(1) from None
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@app.command("rating")
|
|
139
|
+
def filter_by_rating(
|
|
140
|
+
min_rating: int = typer.Argument(..., help="最低评级(1-5)"),
|
|
141
|
+
limit: int = typer.Option(20, help="返回数量"),
|
|
142
|
+
):
|
|
143
|
+
"""评级筛选 (FUND-FILTER-007)"""
|
|
144
|
+
from fund_cli.core.screener import FundScreener
|
|
145
|
+
|
|
146
|
+
screener = FundScreener()
|
|
147
|
+
try:
|
|
148
|
+
df = screener.screen_by_rating(min_rating)
|
|
149
|
+
if df.empty:
|
|
150
|
+
console.print(f"[yellow]未找到评级>={min_rating}的基金[/yellow]")
|
|
151
|
+
return
|
|
152
|
+
_render_fund_table(df.head(limit))
|
|
153
|
+
except Exception as e:
|
|
154
|
+
console.print(f"[red]筛选失败: {e}[/red]")
|
|
155
|
+
raise typer.Exit(1) from None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@app.command("advanced")
|
|
159
|
+
def filter_advanced(
|
|
160
|
+
expression: str = typer.Argument(..., help="筛选表达式"),
|
|
161
|
+
limit: int = typer.Option(20, help="返回数量"),
|
|
162
|
+
):
|
|
163
|
+
"""高级表达式筛选 (FUND-FILTER-012)"""
|
|
164
|
+
from fund_cli.core.screener import FundScreener
|
|
165
|
+
|
|
166
|
+
screener = FundScreener()
|
|
167
|
+
try:
|
|
168
|
+
all_funds = screener._dm.search_funds(limit=1000)
|
|
169
|
+
if all_funds.empty:
|
|
170
|
+
console.print("[yellow]无基金数据[/yellow]")
|
|
171
|
+
return
|
|
172
|
+
result = screener.evaluate_expression(all_funds, expression)
|
|
173
|
+
if result.empty:
|
|
174
|
+
console.print("[yellow]无匹配结果[/yellow]")
|
|
175
|
+
return
|
|
176
|
+
_render_fund_table(result.head(limit))
|
|
177
|
+
except ValueError as e:
|
|
178
|
+
console.print(f"[red]{e}[/red]")
|
|
179
|
+
raise typer.Exit(1) from None
|
|
180
|
+
except Exception as e:
|
|
181
|
+
console.print(f"[red]筛选失败: {e}[/red]")
|
|
182
|
+
raise typer.Exit(1) from None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@app.command("template")
|
|
186
|
+
def filter_template(
|
|
187
|
+
action: str = typer.Argument(..., help="操作: save/load/list/delete"),
|
|
188
|
+
name: str = typer.Option(None, help="模板名称"),
|
|
189
|
+
):
|
|
190
|
+
"""筛选模板管理 (FUND-FILTER-011)"""
|
|
191
|
+
from fund_cli.core.screener import FundScreener
|
|
192
|
+
|
|
193
|
+
screener = FundScreener()
|
|
194
|
+
if action == "list":
|
|
195
|
+
templates = screener.list_templates()
|
|
196
|
+
if not templates:
|
|
197
|
+
console.print("[yellow]暂无保存的模板[/yellow]")
|
|
198
|
+
return
|
|
199
|
+
for t in templates:
|
|
200
|
+
console.print(f" - {t}")
|
|
201
|
+
elif action == "save":
|
|
202
|
+
if not name:
|
|
203
|
+
console.print("[red]请指定模板名称[/red]")
|
|
204
|
+
raise typer.Exit(1) from None
|
|
205
|
+
from fund_cli.data.models import FundFilter
|
|
206
|
+
|
|
207
|
+
screener.save_template(name, FundFilter())
|
|
208
|
+
console.print(f"[green]模板 {name} 已保存[/green]")
|
|
209
|
+
elif action == "load":
|
|
210
|
+
if not name:
|
|
211
|
+
console.print("[red]请指定模板名称[/red]")
|
|
212
|
+
raise typer.Exit(1) from None
|
|
213
|
+
try:
|
|
214
|
+
template = screener.load_template(name)
|
|
215
|
+
console.print(f"模板 {name}: {template.model_dump_json(indent=2)}")
|
|
216
|
+
except FileNotFoundError:
|
|
217
|
+
console.print(f"[red]模板 {name} 不存在[/red]")
|
|
218
|
+
raise typer.Exit(1) from None
|
|
219
|
+
elif action == "delete":
|
|
220
|
+
if not name:
|
|
221
|
+
console.print("[red]请指定模板名称[/red]")
|
|
222
|
+
raise typer.Exit(1) from None
|
|
223
|
+
if screener.delete_template(name):
|
|
224
|
+
console.print(f"[green]模板 {name} 已删除[/green]")
|
|
225
|
+
else:
|
|
226
|
+
console.print(f"[yellow]模板 {name} 不存在[/yellow]")
|
|
227
|
+
else:
|
|
228
|
+
console.print(f"[red]未知操作: {action},支持: save/load/list/delete[/red]")
|
|
229
|
+
raise typer.Exit(1) from None
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
@app.command("performance")
|
|
233
|
+
def filter_performance(
|
|
234
|
+
min_return: float = typer.Option(None, help="最低年化收益率(%)"),
|
|
235
|
+
max_drawdown: float = typer.Option(None, help="最大回撤(%)"),
|
|
236
|
+
min_sharpe: float = typer.Option(None, help="最低夏普比率"),
|
|
237
|
+
limit: int = typer.Option(20, help="返回数量"),
|
|
238
|
+
):
|
|
239
|
+
"""业绩指标筛选"""
|
|
240
|
+
from fund_cli.core.screener import FundScreener
|
|
241
|
+
from fund_cli.data.models import FundFilter
|
|
242
|
+
|
|
243
|
+
screener = FundScreener()
|
|
244
|
+
f = FundFilter(
|
|
245
|
+
min_return_1y=min_return, max_drawdown=max_drawdown, min_sharpe=min_sharpe, limit=limit
|
|
246
|
+
)
|
|
247
|
+
try:
|
|
248
|
+
df = screener.screen(f)
|
|
249
|
+
if df.empty:
|
|
250
|
+
console.print("[yellow]未找到符合条件的基金[/yellow]")
|
|
251
|
+
return
|
|
252
|
+
_render_fund_table(df)
|
|
253
|
+
except Exception as e:
|
|
254
|
+
console.print(f"[red]筛选失败: {e}[/red]")
|
|
255
|
+
raise typer.Exit(1) from None
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@app.command("export")
|
|
259
|
+
def export_results(
|
|
260
|
+
output: str = typer.Argument(..., help="输出文件路径"),
|
|
261
|
+
format: str = typer.Option("csv", help="输出格式: csv/json"),
|
|
262
|
+
):
|
|
263
|
+
"""导出筛选结果"""
|
|
264
|
+
from fund_cli.core.screener import FundScreener
|
|
265
|
+
|
|
266
|
+
screener = FundScreener()
|
|
267
|
+
try:
|
|
268
|
+
df = screener._dm.search_funds(limit=1000)
|
|
269
|
+
if df.empty:
|
|
270
|
+
console.print("[yellow]无数据可导出[/yellow]")
|
|
271
|
+
return
|
|
272
|
+
if format == "csv":
|
|
273
|
+
df.to_csv(output, index=False, encoding="utf-8-sig")
|
|
274
|
+
elif format == "json":
|
|
275
|
+
df.to_json(output, orient="records", force_ascii=False, indent=2)
|
|
276
|
+
else:
|
|
277
|
+
console.print(f"[red]不支持的格式: {format}[/red]")
|
|
278
|
+
raise typer.Exit(1) from None
|
|
279
|
+
console.print(f"[green]已导出 {len(df)} 条记录到 {output}[/green]")
|
|
280
|
+
except Exception as e:
|
|
281
|
+
console.print(f"[red]导出失败: {e}[/red]")
|
|
282
|
+
raise typer.Exit(1) from None
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
app()
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""
|
|
2
|
+
持仓分析命令
|
|
3
|
+
|
|
4
|
+
提供基金持仓查询、行业配置、集中度、变化追踪和风格分析功能。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from fund_cli.analysis.holding import HoldingAnalyzer
|
|
13
|
+
from fund_cli.core.data_manager import DataManager
|
|
14
|
+
from fund_cli.utils.validators import validate_fund_code
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(help="持仓分析命令")
|
|
17
|
+
console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@app.command("query")
|
|
21
|
+
def query_holdings(
|
|
22
|
+
fund_code: str = typer.Argument(..., help="基金代码"),
|
|
23
|
+
top_n: int = typer.Option(10, help="显示前N大持仓"),
|
|
24
|
+
):
|
|
25
|
+
"""查询基金持仓 (FUND-HOLDING-001)"""
|
|
26
|
+
validate_fund_code(fund_code)
|
|
27
|
+
dm = DataManager()
|
|
28
|
+
try:
|
|
29
|
+
holdings = dm.get_fund_holdings(fund_code)
|
|
30
|
+
analyzer = HoldingAnalyzer()
|
|
31
|
+
top = analyzer.top_holdings(holdings, top_n=top_n)
|
|
32
|
+
console.print(f"\n[bold]{fund_code}[/bold] 前十大持仓:")
|
|
33
|
+
table = Table(show_header=True, header_style="bold blue")
|
|
34
|
+
table.add_column("股票代码", style="cyan")
|
|
35
|
+
table.add_column("股票名称")
|
|
36
|
+
table.add_column("占净值比", justify="right")
|
|
37
|
+
table.add_column("持仓市值(万)", justify="right")
|
|
38
|
+
for _, row in top.iterrows():
|
|
39
|
+
table.add_row(
|
|
40
|
+
str(row.get("stock_code", "")),
|
|
41
|
+
str(row.get("stock_name", "")),
|
|
42
|
+
f"{row.get('weight', 0):.2f}%",
|
|
43
|
+
f"{row.get('market_value', 0):,.0f}" if pd.notna(row.get("market_value")) else "-",
|
|
44
|
+
)
|
|
45
|
+
console.print(table)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
console.print(f"[red]获取持仓数据失败: {e}[/red]")
|
|
48
|
+
raise typer.Exit(1) from None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command("industry")
|
|
52
|
+
def industry_analysis(fund_code: str = typer.Argument(..., help="基金代码")):
|
|
53
|
+
"""行业配置分析 (FUND-HOLDING-002)"""
|
|
54
|
+
validate_fund_code(fund_code)
|
|
55
|
+
dm = DataManager()
|
|
56
|
+
try:
|
|
57
|
+
holdings = dm.get_fund_holdings(fund_code)
|
|
58
|
+
analyzer = HoldingAnalyzer()
|
|
59
|
+
distribution = analyzer.industry_distribution(holdings)
|
|
60
|
+
console.print(f"\n[bold]{fund_code}[/bold] 行业配置:")
|
|
61
|
+
table = Table(show_header=True, header_style="bold blue")
|
|
62
|
+
table.add_column("行业", style="cyan")
|
|
63
|
+
table.add_column("占比", justify="right")
|
|
64
|
+
for industry, weight in distribution.items():
|
|
65
|
+
table.add_row(str(industry), f"{weight:.2f}%")
|
|
66
|
+
console.print(table)
|
|
67
|
+
except Exception as e:
|
|
68
|
+
console.print(f"[red]行业分析失败: {e}[/red]")
|
|
69
|
+
raise typer.Exit(1) from None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@app.command("concentration")
|
|
73
|
+
def concentration_analysis(fund_code: str = typer.Argument(..., help="基金代码")):
|
|
74
|
+
"""持仓集中度分析 (FUND-HOLDING-004)"""
|
|
75
|
+
validate_fund_code(fund_code)
|
|
76
|
+
dm = DataManager()
|
|
77
|
+
try:
|
|
78
|
+
holdings = dm.get_fund_holdings(fund_code)
|
|
79
|
+
analyzer = HoldingAnalyzer()
|
|
80
|
+
hhi = analyzer.concentration_hhi(holdings)
|
|
81
|
+
level = analyzer._hhi_level(hhi)
|
|
82
|
+
console.print(f"\n[bold]{fund_code}[/bold] 持仓集中度:")
|
|
83
|
+
console.print(f" HHI指数: {hhi:.4f}")
|
|
84
|
+
console.print(f" 集中度等级: [bold]{level}[/bold]")
|
|
85
|
+
except Exception as e:
|
|
86
|
+
console.print(f"[red]集中度分析失败: {e}[/red]")
|
|
87
|
+
raise typer.Exit(1) from None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.command("changes")
|
|
91
|
+
def holding_changes(
|
|
92
|
+
fund_code: str = typer.Argument(..., help="基金代码"),
|
|
93
|
+
period: str = typer.Option("latest", help="对比周期: latest/quarter"),
|
|
94
|
+
):
|
|
95
|
+
"""持仓变化追踪 (FUND-HOLDING-005)"""
|
|
96
|
+
validate_fund_code(fund_code)
|
|
97
|
+
dm = DataManager()
|
|
98
|
+
try:
|
|
99
|
+
current = dm.get_fund_holdings(fund_code)
|
|
100
|
+
if current.empty:
|
|
101
|
+
console.print("[yellow]暂无持仓数据[/yellow]")
|
|
102
|
+
return
|
|
103
|
+
analyzer = HoldingAnalyzer()
|
|
104
|
+
console.print(f"\n[bold]{fund_code}[/bold] 最新持仓(前10):")
|
|
105
|
+
top = analyzer.top_holdings(current, top_n=10)
|
|
106
|
+
table = Table(show_header=True, header_style="bold blue")
|
|
107
|
+
table.add_column("股票代码", style="cyan")
|
|
108
|
+
table.add_column("股票名称")
|
|
109
|
+
table.add_column("占净值比", justify="right")
|
|
110
|
+
for _, row in top.iterrows():
|
|
111
|
+
table.add_row(
|
|
112
|
+
str(row.get("stock_code", "")),
|
|
113
|
+
str(row.get("stock_name", "")),
|
|
114
|
+
f"{row.get('weight', 0):.2f}%",
|
|
115
|
+
)
|
|
116
|
+
console.print(table)
|
|
117
|
+
console.print("[yellow]提示: 历史持仓对比需要多期数据支持[/yellow]")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
console.print(f"[red]持仓变化分析失败: {e}[/red]")
|
|
120
|
+
raise typer.Exit(1) from None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command("style")
|
|
124
|
+
def style_analysis_cmd(fund_code: str = typer.Argument(..., help="基金代码")):
|
|
125
|
+
"""风格分析 (FUND-HOLDING-006)"""
|
|
126
|
+
validate_fund_code(fund_code)
|
|
127
|
+
dm = DataManager()
|
|
128
|
+
try:
|
|
129
|
+
holdings = dm.get_fund_holdings(fund_code)
|
|
130
|
+
analyzer = HoldingAnalyzer()
|
|
131
|
+
result = analyzer.style_analysis(holdings)
|
|
132
|
+
console.print(f"\n[bold]{fund_code}[/bold] 风格分析:")
|
|
133
|
+
console.print(f" 市值风格: [bold]{result['market_cap_style']}[/bold]")
|
|
134
|
+
console.print(f" 投资风格: [bold]{result['investment_style']}[/bold]")
|
|
135
|
+
console.print(f" 九宫格位置: [bold]{result['grid_position']}[/bold]")
|
|
136
|
+
console.print(f" 大盘股权重: {result['large_cap_weight']:.1f}%")
|
|
137
|
+
console.print(f" 价值股权重: {result['value_weight']:.1f}%")
|
|
138
|
+
except Exception as e:
|
|
139
|
+
console.print(f"[red]风格分析失败: {e}[/red]")
|
|
140
|
+
raise typer.Exit(1) from None
|