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,50 @@
1
+ """
2
+ 优化引擎基类
3
+
4
+ 定义组合优化引擎的标准接口。
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from typing import Any
9
+
10
+ import pandas as pd
11
+
12
+
13
+ class Optimizer(ABC):
14
+ """
15
+ 优化引擎基类
16
+
17
+ 所有优化引擎必须继承此类。
18
+ """
19
+
20
+ @abstractmethod
21
+ def optimize(
22
+ self,
23
+ returns: pd.DataFrame,
24
+ **kwargs,
25
+ ) -> dict[str, Any]:
26
+ """
27
+ 执行组合优化
28
+
29
+ Args:
30
+ returns: 多资产收益率 DataFrame,每列一个资产
31
+ **kwargs: 额外参数(如目标收益率、最大回撤限制等)
32
+
33
+ Returns:
34
+ 优化结果字典,包含:
35
+ - weights: 各资产权重字典
36
+ - expected_return: 预期收益率
37
+ - expected_volatility: 预期波动率
38
+ - sharpe_ratio: 夏普比率
39
+ """
40
+ pass
41
+
42
+ @abstractmethod
43
+ def get_methods(self) -> list[str]:
44
+ """
45
+ 获取支持的优化方法列表
46
+
47
+ Returns:
48
+ 方法名称列表
49
+ """
50
+ pass
@@ -0,0 +1,13 @@
1
+ """组合优化引擎"""
2
+
3
+ from fund_cli.core.optimizers.efficient_frontier import EfficientFrontierCalculator
4
+ from fund_cli.core.optimizers.max_sharpe import MaxSharpeOptimizer
5
+ from fund_cli.core.optimizers.mean_variance import MeanVarianceOptimizer
6
+ from fund_cli.core.optimizers.risk_parity import RiskParityOptimizer
7
+
8
+ __all__ = [
9
+ "MeanVarianceOptimizer",
10
+ "MaxSharpeOptimizer",
11
+ "RiskParityOptimizer",
12
+ "EfficientFrontierCalculator",
13
+ ]
@@ -0,0 +1,91 @@
1
+ """有效前沿计算器"""
2
+
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+
9
+ class EfficientFrontierCalculator:
10
+ """有效前沿计算器 - PORTFOLIO-OPT-006"""
11
+
12
+ def calculate(
13
+ self,
14
+ returns: pd.DataFrame,
15
+ n_points: int = 50,
16
+ risk_free_rate: float = 0.03,
17
+ ) -> dict[str, Any]:
18
+ """
19
+ 计算有效前沿上的点集
20
+
21
+ Args:
22
+ returns: 收益率 DataFrame
23
+ n_points: 前沿点数
24
+ risk_free_rate: 无风险利率
25
+
26
+ Returns:
27
+ 包含 frontier_returns, frontier_volatilities, frontier_sharpes 的字典
28
+ """
29
+ try:
30
+ from pypfopt import EfficientFrontier, expected_returns, risk_models
31
+
32
+ mu = expected_returns.mean_historical_return(returns)
33
+ S = risk_models.sample_cov(returns)
34
+
35
+ min_ret = mu.min()
36
+ max_ret = mu.max()
37
+ target_returns = np.linspace(min_ret, max_ret, n_points)
38
+
39
+ frontier_volatilities = []
40
+ frontier_returns = []
41
+ frontier_sharpes = []
42
+
43
+ for target in target_returns:
44
+ try:
45
+ ef = EfficientFrontier(mu, S)
46
+ ef.efficient_return(target)
47
+ ret, vol, _ = ef.portfolio_performance(
48
+ verbose=False, risk_free_rate=risk_free_rate
49
+ )
50
+ frontier_returns.append(ret)
51
+ frontier_volatilities.append(vol)
52
+ frontier_sharpes.append((ret - risk_free_rate) / vol if vol > 0 else 0)
53
+ except Exception:
54
+ continue
55
+
56
+ if len(frontier_returns) == 0:
57
+ return self._fallback_calculate(returns, n_points, risk_free_rate)
58
+
59
+ return {
60
+ "frontier_returns": [round(r, 6) for r in frontier_returns],
61
+ "frontier_volatilities": [round(v, 6) for v in frontier_volatilities],
62
+ "frontier_sharpes": [round(s, 4) for s in frontier_sharpes],
63
+ "n_points": len(frontier_returns),
64
+ }
65
+ except Exception:
66
+ return self._fallback_calculate(returns, n_points, risk_free_rate)
67
+
68
+ @staticmethod
69
+ def _fallback_calculate(
70
+ returns: pd.DataFrame, n_points: int, risk_free_rate: float
71
+ ) -> dict[str, Any]:
72
+ """回退实现"""
73
+ n = returns.shape[1]
74
+ vols = []
75
+ rets = []
76
+ for i in range(n_points):
77
+ np.random.seed(i)
78
+ w = np.random.dirichlet(np.ones(n))
79
+ port_ret = (returns.mean() * 252 * w).sum()
80
+ port_vol = np.sqrt(w @ returns.cov().values @ w * 252)
81
+ vols.append(port_vol)
82
+ rets.append(port_ret)
83
+ return {
84
+ "frontier_returns": [round(r, 6) for r in rets],
85
+ "frontier_volatilities": [round(v, 6) for v in vols],
86
+ "frontier_sharpes": [
87
+ round((r - risk_free_rate) / v, 4) if v > 0 else 0
88
+ for r, v in zip(rets, vols, strict=False)
89
+ ],
90
+ "n_points": n,
91
+ }
@@ -0,0 +1,54 @@
1
+ """最大夏普比率优化器"""
2
+
3
+ from typing import Any
4
+
5
+ import pandas as pd
6
+
7
+ from fund_cli.core.optimizer import Optimizer
8
+ from fund_cli.core.optimizers.mean_variance import MeanVarianceOptimizer
9
+ from fund_cli.data.models import OptimizationConstraint
10
+
11
+
12
+ class MaxSharpeOptimizer(Optimizer):
13
+ """最大夏普比率优化器 - PORTFOLIO-OPT-002"""
14
+
15
+ def __init__(self, risk_free_rate: float = 0.03):
16
+ self.risk_free_rate = risk_free_rate
17
+
18
+ def optimize(
19
+ self,
20
+ returns: pd.DataFrame,
21
+ constraints: OptimizationConstraint | None = None,
22
+ **kwargs: Any,
23
+ ) -> dict[str, Any]:
24
+ """执行最大夏普比率优化"""
25
+ try:
26
+ from pypfopt import EfficientFrontier, expected_returns, risk_models
27
+ except ImportError:
28
+ return MeanVarianceOptimizer._fallback_optimize(returns, constraints)
29
+
30
+ try:
31
+ mu = expected_returns.mean_historical_return(returns)
32
+ S = risk_models.sample_cov(returns)
33
+
34
+ min_w = constraints.min_weight if constraints else 0.0
35
+ max_w = constraints.max_weight if constraints else 1.0
36
+
37
+ ef = EfficientFrontier(mu, S, weight_bounds=(min_w, max_w))
38
+ ef.max_sharpe(risk_free_rate=self.risk_free_rate)
39
+
40
+ weights = ef.clean_weights()
41
+ perf = ef.portfolio_performance(verbose=False, risk_free_rate=self.risk_free_rate)
42
+
43
+ return {
44
+ "weights": {k: round(v, 6) for k, v in weights.items()},
45
+ "expected_return": round(perf[0], 6),
46
+ "volatility": round(perf[1], 6),
47
+ "sharpe_ratio": round(perf[2], 4),
48
+ "method": "max_sharpe",
49
+ }
50
+ except Exception:
51
+ return MeanVarianceOptimizer._fallback_optimize(returns, constraints)
52
+
53
+ def get_methods(self) -> list[str]:
54
+ return ["max_sharpe"]
@@ -0,0 +1,84 @@
1
+ """均值-方差优化器(Markowitz模型)"""
2
+
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ from fund_cli.core.optimizer import Optimizer
9
+ from fund_cli.data.models import OptimizationConstraint
10
+
11
+
12
+ class MeanVarianceOptimizer(Optimizer):
13
+ """均值-方差优化器 - PORTFOLIO-OPT-001"""
14
+
15
+ def __init__(self, risk_free_rate: float = 0.03):
16
+ self.risk_free_rate = risk_free_rate
17
+
18
+ def optimize(
19
+ self,
20
+ returns: pd.DataFrame,
21
+ constraints: OptimizationConstraint | None = None,
22
+ **kwargs: Any,
23
+ ) -> dict[str, Any]:
24
+ """
25
+ 执行均值-方差优化
26
+
27
+ Args:
28
+ returns: 收益率 DataFrame,每列一只基金
29
+ constraints: 优化约束条件
30
+
31
+ Returns:
32
+ 优化结果字典,包含 weights, expected_return, volatility, sharpe_ratio
33
+ """
34
+ try:
35
+ from pypfopt import EfficientFrontier, expected_returns, risk_models
36
+ except ImportError:
37
+ return self._fallback_optimize(returns, constraints)
38
+
39
+ try:
40
+ mu = expected_returns.mean_historical_return(returns)
41
+ S = risk_models.sample_cov(returns)
42
+
43
+ min_w = constraints.min_weight if constraints else 0.0
44
+ max_w = constraints.max_weight if constraints else 1.0
45
+
46
+ ef = EfficientFrontier(mu, S, weight_bounds=(min_w, max_w))
47
+
48
+ if constraints and constraints.target_return is not None:
49
+ ef.efficient_return(constraints.target_return)
50
+ else:
51
+ ef.max_sharpe(risk_free_rate=self.risk_free_rate)
52
+
53
+ weights = ef.clean_weights()
54
+ perf = ef.portfolio_performance(verbose=False, risk_free_rate=self.risk_free_rate)
55
+
56
+ return {
57
+ "weights": {k: round(v, 6) for k, v in weights.items()},
58
+ "expected_return": round(perf[0], 6),
59
+ "volatility": round(perf[1], 6),
60
+ "sharpe_ratio": round(perf[2], 4),
61
+ "method": "mean_variance",
62
+ }
63
+ except Exception:
64
+ return self._fallback_optimize(returns, constraints)
65
+
66
+ def get_methods(self) -> list[str]:
67
+ return ["mean_variance"]
68
+
69
+ @staticmethod
70
+ def _fallback_optimize(
71
+ returns: pd.DataFrame, constraints: OptimizationConstraint | None = None
72
+ ) -> dict[str, Any]:
73
+ """PyPortfolioOpt不可用时的回退实现"""
74
+ n = returns.shape[1]
75
+ equal_weights = {col: round(1.0 / n, 6) for col in returns.columns}
76
+ port_return = (returns.mean() * 252).mean()
77
+ port_vol = returns.std().mean() * np.sqrt(252)
78
+ return {
79
+ "weights": equal_weights,
80
+ "expected_return": round(port_return, 6),
81
+ "volatility": round(port_vol, 6),
82
+ "sharpe_ratio": round((port_return - 0.03) / port_vol, 4) if port_vol > 0 else 0.0,
83
+ "method": "equal_weight_fallback",
84
+ }
@@ -0,0 +1,60 @@
1
+ """风险平价优化器"""
2
+
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ from fund_cli.core.optimizer import Optimizer
9
+
10
+
11
+ class RiskParityOptimizer(Optimizer):
12
+ """风险平价优化器 - PORTFOLIO-OPT-003"""
13
+
14
+ def optimize(
15
+ self,
16
+ returns: pd.DataFrame,
17
+ **kwargs: Any,
18
+ ) -> dict[str, Any]:
19
+ """执行风险平价优化"""
20
+ try:
21
+ from pypfopt import HRPOpt
22
+ except ImportError:
23
+ return self._fallback_risk_parity(returns)
24
+
25
+ hrp = HRPOpt(returns)
26
+ weights = hrp.optimize()
27
+
28
+ port_returns = (returns * pd.Series(weights)).sum(axis=1)
29
+ expected_return = port_returns.mean() * 252
30
+ volatility = port_returns.std() * np.sqrt(252)
31
+ sharpe = (expected_return - 0.03) / volatility if volatility > 0 else 0.0
32
+
33
+ return {
34
+ "weights": {k: round(v, 6) for k, v in weights.items()},
35
+ "expected_return": round(expected_return, 6),
36
+ "volatility": round(volatility, 6),
37
+ "sharpe_ratio": round(sharpe, 4),
38
+ "method": "risk_parity",
39
+ }
40
+
41
+ def get_methods(self) -> list[str]:
42
+ return ["risk_parity"]
43
+
44
+ @staticmethod
45
+ def _fallback_risk_parity(returns: pd.DataFrame) -> dict[str, Any]:
46
+ """回退实现:基于波动率倒数加权"""
47
+ vols = returns.std()
48
+ inv_vols = 1.0 / vols
49
+ weights = inv_vols / inv_vols.sum()
50
+ port_returns = (returns * weights).sum(axis=1)
51
+ expected_return = port_returns.mean() * 252
52
+ volatility = port_returns.std() * np.sqrt(252)
53
+ sharpe = (expected_return - 0.03) / volatility if volatility > 0 else 0.0
54
+ return {
55
+ "weights": {k: round(v, 6) for k, v in weights.items()},
56
+ "expected_return": round(expected_return, 6),
57
+ "volatility": round(volatility, 6),
58
+ "sharpe_ratio": round(sharpe, 4),
59
+ "method": "inverse_volatility_fallback",
60
+ }
@@ -0,0 +1,67 @@
1
+ """
2
+ 报告生成器基类
3
+
4
+ 定义报告生成的标准接口。
5
+ """
6
+
7
+ from abc import ABC, abstractmethod
8
+ from typing import Any
9
+
10
+ import pandas as pd
11
+
12
+
13
+ class Reporter(ABC):
14
+ """
15
+ 报告生成器基类
16
+
17
+ 所有报告生成器必须继承此类。
18
+ """
19
+
20
+ @abstractmethod
21
+ def generate(
22
+ self,
23
+ fund_code: str,
24
+ metrics: dict[str, Any],
25
+ nav_data: pd.DataFrame | None = None,
26
+ benchmark_data: pd.DataFrame | None = None,
27
+ **kwargs,
28
+ ) -> str:
29
+ """
30
+ 生成报告
31
+
32
+ Args:
33
+ fund_code: 基金代码
34
+ metrics: 分析指标字典
35
+ nav_data: 净值数据
36
+ benchmark_data: 基准数据
37
+ **kwargs: 额外参数
38
+
39
+ Returns:
40
+ 报告内容字符串
41
+ """
42
+ pass
43
+
44
+ @abstractmethod
45
+ def save(
46
+ self,
47
+ content: str,
48
+ output_path: str,
49
+ ) -> None:
50
+ """
51
+ 保存报告到文件
52
+
53
+ Args:
54
+ content: 报告内容
55
+ output_path: 输出文件路径
56
+ """
57
+ pass
58
+
59
+ @abstractmethod
60
+ def get_formats(self) -> list:
61
+ """
62
+ 获取支持的报告格式
63
+
64
+ Returns:
65
+ 格式列表(如 ['html', 'markdown', 'pdf'])
66
+ """
67
+ pass
@@ -0,0 +1,6 @@
1
+ """报告生成器"""
2
+
3
+ from fund_cli.core.reporters.html_reporter import HtmlReporter
4
+ from fund_cli.core.reporters.markdown_reporter import MarkdownReporter
5
+
6
+ __all__ = ["HtmlReporter", "MarkdownReporter"]
@@ -0,0 +1,62 @@
1
+ """HTML报告生成器"""
2
+
3
+ from datetime import date
4
+ from typing import Any
5
+
6
+ from fund_cli.core.reporter import Reporter
7
+
8
+
9
+ class HtmlReporter(Reporter):
10
+ """HTML报告生成器 (FUND-ANALYZE-011)"""
11
+
12
+ def generate(self, fund_code: str, metrics: dict[str, Any], **kwargs) -> str:
13
+ kwargs.get("nav_data")
14
+ html = f"""<!DOCTYPE html>
15
+ <html lang="zh-CN">
16
+ <head><meta charset="UTF-8"><title>{fund_code} 分析报告</title>
17
+ <style>
18
+ body {{ font-family: -apple-system, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; color: #333; }}
19
+ h1 {{ color: #1a5276; border-bottom: 2px solid #2980b9; padding-bottom: 10px; }}
20
+ h2 {{ color: #2c3e50; margin-top: 30px; }}
21
+ table {{ border-collapse: collapse; width: 100%; margin: 10px 0; }}
22
+ th, td {{ border: 1px solid #ddd; padding: 8px 12px; text-align: left; }}
23
+ th {{ background: #2980b9; color: white; }}
24
+ tr:nth-child(even) {{ background: #f2f2f2; }}
25
+ .positive {{ color: #27ae60; }}
26
+ .negative {{ color: #e74c3c; }}
27
+ .footer {{ margin-top: 30px; font-size: 12px; color: #999; border-top: 1px solid #eee; padding-top: 10px; }}
28
+ </style></head><body>
29
+ <h1>{fund_code} 基金分析报告</h1>
30
+ <p>报告日期: {date.today().strftime('%Y-%m-%d')}</p>
31
+ <h2>核心指标</h2>
32
+ <table><tr><th>指标</th><th>值</th></tr>"""
33
+
34
+ key_metrics = [
35
+ ("总收益率", metrics.get("total_return", "N/A")),
36
+ ("年化收益率", metrics.get("annualized_return", "N/A")),
37
+ ("波动率", metrics.get("volatility", "N/A")),
38
+ ("夏普比率", metrics.get("sharpe_ratio", "N/A")),
39
+ ("最大回撤", metrics.get("max_drawdown", "N/A")),
40
+ ("索提诺比率", metrics.get("sortino_ratio", "N/A")),
41
+ ("Alpha", metrics.get("alpha", "N/A")),
42
+ ("Beta", metrics.get("beta", "N/A")),
43
+ ]
44
+ for name, value in key_metrics:
45
+ if isinstance(value, float):
46
+ cls = "positive" if value > 0 else "negative" if value < 0 else ""
47
+ html += f"<tr><td>{name}</td><td class='{cls}'>{value:.4f}</td></tr>"
48
+ else:
49
+ html += f"<tr><td>{name}</td><td>{value}</td></tr>"
50
+
51
+ html += "</table>"
52
+ html += "<div class='footer'>本报告由 Fund CLI 自动生成,仅供参考,不构成投资建议。</div>"
53
+ html += "</body></html>"
54
+ return html
55
+
56
+ def save(self, content: str, output_path: str) -> None:
57
+ from pathlib import Path
58
+
59
+ Path(output_path).write_text(content, encoding="utf-8")
60
+
61
+ def get_formats(self) -> list[str]:
62
+ return ["html"]
@@ -0,0 +1,40 @@
1
+ """Markdown报告生成器"""
2
+
3
+ from datetime import date
4
+ from typing import Any
5
+
6
+ from fund_cli.core.reporter import Reporter
7
+
8
+
9
+ class MarkdownReporter(Reporter):
10
+ """Markdown报告生成器"""
11
+
12
+ def generate(self, fund_code: str, metrics: dict[str, Any], **kwargs) -> str:
13
+ md = f"# {fund_code} 基金分析报告\n\n"
14
+ md += f"报告日期: {date.today().strftime('%Y-%m-%d')}\n\n"
15
+ md += "## 核心指标\n\n"
16
+ md += "| 指标 | 值 |\n|------|-----|\n"
17
+
18
+ key_metrics = [
19
+ ("总收益率", metrics.get("total_return")),
20
+ ("年化收益率", metrics.get("annualized_return")),
21
+ ("波动率", metrics.get("volatility")),
22
+ ("夏普比率", metrics.get("sharpe_ratio")),
23
+ ("最大回撤", metrics.get("max_drawdown")),
24
+ ]
25
+ for name, value in key_metrics:
26
+ if isinstance(value, float):
27
+ md += f"| {name} | {value:.4f} |\n"
28
+ else:
29
+ md += f"| {name} | {value or 'N/A'} |\n"
30
+
31
+ md += "\n---\n*本报告由 Fund CLI 自动生成,仅供参考。*\n"
32
+ return md
33
+
34
+ def save(self, content: str, output_path: str) -> None:
35
+ from pathlib import Path
36
+
37
+ Path(output_path).write_text(content, encoding="utf-8")
38
+
39
+ def get_formats(self) -> list[str]:
40
+ return ["markdown"]
@@ -0,0 +1,142 @@
1
+ """基金筛选引擎"""
2
+
3
+ import json
4
+ import re
5
+ from pathlib import Path
6
+
7
+ import pandas as pd
8
+
9
+ from fund_cli.data.models import FundFilter
10
+
11
+
12
+ class FundScreener:
13
+ """
14
+ 基金筛选引擎
15
+
16
+ 功能:
17
+ - 费率筛选 (FUND-FILTER-005)
18
+ - 经理筛选 (FUND-FILTER-006)
19
+ - 评级筛选 (FUND-FILTER-007)
20
+ - 高级表达式筛选 (FUND-FILTER-012)
21
+ - 模板保存/加载 (FUND-FILTER-011)
22
+ """
23
+
24
+ # 允许的表达式函数白名单
25
+ _SAFE_FUNCTIONS = {"sum", "mean", "min", "max", "abs", "len"}
26
+
27
+ def __init__(self, data_manager=None):
28
+ from fund_cli.core.data_manager import DataManager
29
+
30
+ self._dm = data_manager or DataManager()
31
+ self._template_dir = Path("~/.fund_cli/templates").expanduser()
32
+ self._template_dir.mkdir(parents=True, exist_ok=True)
33
+
34
+ def screen(self, filter_obj: FundFilter) -> pd.DataFrame:
35
+ """执行通用筛选"""
36
+ df = self._dm.search_funds(
37
+ fund_type=filter_obj.fund_type.value if filter_obj.fund_type else None,
38
+ company=filter_obj.company,
39
+ min_scale=filter_obj.min_scale,
40
+ max_scale=filter_obj.max_scale,
41
+ keyword=filter_obj.keyword,
42
+ limit=filter_obj.limit,
43
+ )
44
+
45
+ if df.empty:
46
+ return df
47
+
48
+ # 业绩筛选
49
+ if filter_obj.min_return_1y is not None and "return_1y" in df.columns:
50
+ df = df[df["return_1y"] >= filter_obj.min_return_1y]
51
+ if filter_obj.max_return_1y is not None and "return_1y" in df.columns:
52
+ df = df[df["return_1y"] <= filter_obj.max_return_1y]
53
+
54
+ # 风险筛选
55
+ if filter_obj.max_drawdown is not None and "max_drawdown" in df.columns:
56
+ df = df[df["max_drawdown"] >= filter_obj.max_drawdown]
57
+ if filter_obj.min_sharpe is not None and "sharpe_ratio" in df.columns:
58
+ df = df[df["sharpe_ratio"] >= filter_obj.min_sharpe]
59
+
60
+ # V1.0 新增筛选
61
+ if filter_obj.fee_rate_max is not None and "fee_rate" in df.columns:
62
+ df = df[df["fee_rate"] <= filter_obj.fee_rate_max]
63
+ if filter_obj.manager_name and "manager" in df.columns:
64
+ df = df[df["manager"].str.contains(filter_obj.manager_name, na=False)]
65
+ if filter_obj.min_rating is not None and "rating" in df.columns:
66
+ df = df[df["rating"] >= filter_obj.min_rating]
67
+
68
+ # 排序
69
+ if filter_obj.sort_by and filter_obj.sort_by in df.columns:
70
+ ascending = filter_obj.sort_order == "asc"
71
+ df = df.sort_values(filter_obj.sort_by, ascending=ascending)
72
+
73
+ return df.reset_index(drop=True)
74
+
75
+ def screen_by_fee(self, max_fee_rate: float, fund_type: str | None = None) -> pd.DataFrame:
76
+ """费率筛选 (FUND-FILTER-005)"""
77
+ f = FundFilter(fee_rate_max=max_fee_rate)
78
+ if fund_type:
79
+ from fund_cli.data.models import FundType
80
+
81
+ f.fund_type = FundType(fund_type)
82
+ return self.screen(f)
83
+
84
+ def screen_by_manager(self, manager_name: str) -> pd.DataFrame:
85
+ """经理筛选 (FUND-FILTER-006)"""
86
+ f = FundFilter(manager_name=manager_name)
87
+ return self.screen(f)
88
+
89
+ def screen_by_rating(self, min_rating: int) -> pd.DataFrame:
90
+ """评级筛选 (FUND-FILTER-007)"""
91
+ f = FundFilter(min_rating=min_rating)
92
+ return self.screen(f)
93
+
94
+ def evaluate_expression(self, df: pd.DataFrame, expression: str) -> pd.DataFrame:
95
+ """
96
+ 高级表达式筛选 (FUND-FILTER-012)
97
+
98
+ 使用 pandas query 安全执行表达式。
99
+ 支持的运算符: >, <, =, >=, <=, ==, !=
100
+ 支持的逻辑: AND, OR, and, or, &, |
101
+ """
102
+ try:
103
+ # 安全检查:禁止危险操作
104
+ dangerous = re.findall(
105
+ r"(?:__|import|exec|eval|open|os\.|sys\.)", expression, re.IGNORECASE
106
+ )
107
+ if dangerous:
108
+ raise ValueError(f"表达式包含不允许的操作: {dangerous}")
109
+
110
+ result = df.query(expression)
111
+ return result.reset_index(drop=True)
112
+ except Exception as e:
113
+ raise ValueError(f"表达式解析失败: {e}") from e
114
+
115
+ def save_template(self, name: str, filter_obj: FundFilter) -> None:
116
+ """保存筛选模板 (FUND-FILTER-011)"""
117
+ path = self._template_dir / f"{name}.json"
118
+ data = filter_obj.model_dump(mode="json")
119
+ path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
120
+
121
+ def load_template(self, name: str) -> FundFilter:
122
+ """加载筛选模板"""
123
+ path = self._template_dir / f"{name}.json"
124
+ if not path.exists():
125
+ raise FileNotFoundError(f"模板 {name} 不存在")
126
+ data = json.loads(path.read_text(encoding="utf-8"))
127
+ return FundFilter(**data)
128
+
129
+ def list_templates(self) -> list[str]:
130
+ """列出所有筛选模板"""
131
+ templates = []
132
+ for f in self._template_dir.glob("*.json"):
133
+ templates.append(f.stem)
134
+ return sorted(templates)
135
+
136
+ def delete_template(self, name: str) -> bool:
137
+ """删除筛选模板"""
138
+ path = self._template_dir / f"{name}.json"
139
+ if path.exists():
140
+ path.unlink()
141
+ return True
142
+ return False
@@ -0,0 +1,6 @@
1
+ """数据层 - 数据源适配器、缓存管理、数据模型"""
2
+
3
+ from fund_cli.data.base import DataSourceAdapter
4
+ from fund_cli.data.models import FundInfo, FundType, NavData
5
+
6
+ __all__ = ["FundInfo", "NavData", "FundType", "DataSourceAdapter"]