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,286 @@
1
+ """
2
+ LLM 提供商管理(V2.0 实现)
3
+
4
+ 管理多个 LLM 提供商的连接和调用,支持OpenAI、阿里云Qwen等。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import time
10
+ from abc import ABC, abstractmethod
11
+ from typing import Any
12
+
13
+ import requests
14
+
15
+ from fund_cli.config import AIConfig
16
+
17
+
18
+ class LLMProvider(ABC):
19
+ """LLM 提供商抽象基类"""
20
+
21
+ def __init__(self, config: AIConfig):
22
+ self.config = config
23
+
24
+ @abstractmethod
25
+ def generate(self, prompt: str, **kwargs: Any) -> str:
26
+ """生成文本
27
+
28
+ Args:
29
+ prompt: 输入提示词
30
+ **kwargs: 额外参数
31
+
32
+ Returns:
33
+ 生成的文本
34
+ """
35
+ pass
36
+
37
+ @abstractmethod
38
+ def is_available(self) -> bool:
39
+ """检查提供商是否可用
40
+
41
+ Returns:
42
+ 是否可用
43
+ """
44
+ pass
45
+
46
+ @abstractmethod
47
+ def validate_config(self) -> bool:
48
+ """验证配置是否有效
49
+
50
+ Returns:
51
+ 配置是否有效
52
+ """
53
+ pass
54
+
55
+
56
+ class OpenAIProvider(LLMProvider):
57
+ """OpenAI 提供商"""
58
+
59
+ def __init__(self, config: AIConfig):
60
+ super().__init__(config)
61
+ self.api_key = config.api_key
62
+ self.model = config.model or "gpt-4"
63
+ self.base_url = config.api_base or "https://api.openai.com/v1"
64
+
65
+ def generate(self, prompt: str, **kwargs: Any) -> str:
66
+ """使用 OpenAI API 生成文本"""
67
+ if not self.api_key:
68
+ raise ValueError("OpenAI API key not configured")
69
+
70
+ headers = {
71
+ "Authorization": f"Bearer {self.api_key}",
72
+ "Content-Type": "application/json",
73
+ }
74
+
75
+ payload = {
76
+ "model": kwargs.get("model", self.model),
77
+ "messages": [{"role": "user", "content": prompt}],
78
+ "temperature": kwargs.get("temperature", self.config.temperature),
79
+ "max_tokens": kwargs.get("max_tokens", self.config.max_tokens),
80
+ }
81
+
82
+ response = self._call_with_retry(f"{self.base_url}/chat/completions", headers, payload)
83
+
84
+ if response and "choices" in response:
85
+ return response["choices"][0]["message"]["content"]
86
+ raise RuntimeError("Invalid response from OpenAI API")
87
+
88
+ def is_available(self) -> bool:
89
+ """检查OpenAI API是否可用"""
90
+ if not self.api_key:
91
+ return False
92
+ try:
93
+ headers = {"Authorization": f"Bearer {self.api_key}"}
94
+ response = requests.get(
95
+ f"{self.base_url}/models",
96
+ headers=headers,
97
+ timeout=self.config.timeout,
98
+ )
99
+ return response.status_code == 200
100
+ except Exception:
101
+ return False
102
+
103
+ def validate_config(self) -> bool:
104
+ """验证配置"""
105
+ return bool(self.api_key and self.model)
106
+
107
+ def _call_with_retry(self, url: str, headers: dict, payload: dict) -> dict | None:
108
+ """带重试的API调用"""
109
+ last_error = None
110
+ for attempt in range(self.config.retry_count):
111
+ try:
112
+ response = requests.post(
113
+ url,
114
+ headers=headers,
115
+ json=payload,
116
+ timeout=self.config.timeout,
117
+ )
118
+ response.raise_for_status()
119
+ return response.json()
120
+ except requests.RequestException as e:
121
+ last_error = e
122
+ if attempt < self.config.retry_count - 1:
123
+ time.sleep(2**attempt) # 指数退避
124
+ raise RuntimeError(
125
+ f"API call failed after {self.config.retry_count} attempts: {last_error}"
126
+ )
127
+
128
+
129
+ class QwenProvider(LLMProvider):
130
+ """阿里云Qwen 提供商"""
131
+
132
+ def __init__(self, config: AIConfig):
133
+ super().__init__(config)
134
+ self.api_key = config.qwen_api_key or config.api_key
135
+ self.model = config.qwen_model or "qwen-max"
136
+ self.base_url = config.qwen_base_url or "https://dashscope.aliyuncs.com/api/v1"
137
+
138
+ def generate(self, prompt: str, **kwargs: Any) -> str:
139
+ """使用 Qwen API 生成文本"""
140
+ if not self.api_key:
141
+ raise ValueError("Qwen API key not configured")
142
+
143
+ headers = {
144
+ "Authorization": f"Bearer {self.api_key}",
145
+ "Content-Type": "application/json",
146
+ }
147
+
148
+ payload = {
149
+ "model": kwargs.get("model", self.model),
150
+ "input": {"messages": [{"role": "user", "content": prompt}]},
151
+ "parameters": {
152
+ "temperature": kwargs.get("temperature", self.config.temperature),
153
+ "max_tokens": kwargs.get("max_tokens", self.config.max_tokens),
154
+ "result_format": "message",
155
+ },
156
+ }
157
+
158
+ response = self._call_with_retry(
159
+ f"{self.base_url}/services/aigc/text-generation/generation",
160
+ headers,
161
+ payload,
162
+ )
163
+
164
+ if response and "output" in response:
165
+ return response["output"]["choices"][0]["message"]["content"]
166
+ raise RuntimeError("Invalid response from Qwen API")
167
+
168
+ def is_available(self) -> bool:
169
+ """检查Qwen API是否可用"""
170
+ if not self.api_key:
171
+ return False
172
+ try:
173
+ headers = {"Authorization": f"Bearer {self.api_key}"}
174
+ # 使用简单的模型列表接口测试
175
+ response = requests.get(
176
+ f"{self.base_url}/models",
177
+ headers=headers,
178
+ timeout=self.config.timeout,
179
+ )
180
+ return response.status_code == 200
181
+ except Exception:
182
+ return False
183
+
184
+ def validate_config(self) -> bool:
185
+ """验证配置"""
186
+ return bool(self.api_key and self.model)
187
+
188
+ def _call_with_retry(self, url: str, headers: dict, payload: dict) -> dict | None:
189
+ """带重试的API调用"""
190
+ last_error = None
191
+ for attempt in range(self.config.retry_count):
192
+ try:
193
+ response = requests.post(
194
+ url,
195
+ headers=headers,
196
+ json=payload,
197
+ timeout=self.config.timeout,
198
+ )
199
+ response.raise_for_status()
200
+ return response.json()
201
+ except requests.RequestException as e:
202
+ last_error = e
203
+ if attempt < self.config.retry_count - 1:
204
+ time.sleep(2**attempt) # 指数退避
205
+ raise RuntimeError(
206
+ f"API call failed after {self.config.retry_count} attempts: {last_error}"
207
+ )
208
+
209
+
210
+ class LiteLLMProvider(LLMProvider):
211
+ """LiteLLM 统一封装提供商"""
212
+
213
+ def __init__(self, config: AIConfig):
214
+ super().__init__(config)
215
+ self.api_key = config.api_key
216
+ self.model = config.model or "gpt-4"
217
+
218
+ def generate(self, prompt: str, **kwargs: Any) -> str:
219
+ """使用 LiteLLM 生成文本"""
220
+ try:
221
+ import litellm
222
+
223
+ litellm.api_key = self.api_key
224
+
225
+ response = litellm.completion(
226
+ model=self.model,
227
+ messages=[{"role": "user", "content": prompt}],
228
+ temperature=kwargs.get("temperature", self.config.temperature),
229
+ max_tokens=kwargs.get("max_tokens", self.config.max_tokens),
230
+ )
231
+ return response.choices[0].message.content
232
+ except ImportError as err:
233
+ raise RuntimeError("litellm not installed. Install with: pip install litellm") from err
234
+ except Exception as e:
235
+ raise RuntimeError(f"LiteLLM API error: {e}") from e
236
+
237
+ def is_available(self) -> bool:
238
+ """检查LiteLLM是否可用"""
239
+ try:
240
+ import importlib.util
241
+
242
+ spec = importlib.util.find_spec("litellm")
243
+ if spec is None:
244
+ return False
245
+ return bool(self.api_key)
246
+ except ImportError:
247
+ return False
248
+
249
+ def validate_config(self) -> bool:
250
+ """验证配置"""
251
+ return bool(self.api_key and self.model)
252
+
253
+
254
+ def get_provider(config: AIConfig | None = None) -> LLMProvider:
255
+ """
256
+ 获取 LLM 提供商实例
257
+
258
+ Args:
259
+ config: AI配置,如果为None则使用默认配置
260
+
261
+ Returns:
262
+ LLM 提供商实例
263
+
264
+ Raises:
265
+ ValueError: 如果提供商类型不支持
266
+ """
267
+ if config is None:
268
+ from fund_cli.config import get_config
269
+
270
+ config = get_config().ai
271
+
272
+ providers = {
273
+ "openai": OpenAIProvider,
274
+ "qwen": QwenProvider,
275
+ "litellm": LiteLLMProvider,
276
+ }
277
+
278
+ provider_name = config.provider.lower()
279
+ provider_class = providers.get(provider_name)
280
+
281
+ if provider_class is None:
282
+ raise ValueError(
283
+ f"不支持的提供商: {provider_name}. " f"支持的提供商: {', '.join(providers.keys())}"
284
+ )
285
+
286
+ return provider_class(config)
@@ -0,0 +1,17 @@
1
+ """分析模块 - 业绩分析、风险分析、归因分析、组合分析、经理分析、持仓分析"""
2
+
3
+ from fund_cli.analysis.attribution import AttributionAnalyzer
4
+ from fund_cli.analysis.holding import HoldingAnalyzer
5
+ from fund_cli.analysis.manager import ManagerAnalyzer
6
+ from fund_cli.analysis.performance import PerformanceAnalyzer
7
+ from fund_cli.analysis.portfolio import PortfolioAnalyzer
8
+ from fund_cli.analysis.risk import RiskAnalyzer
9
+
10
+ __all__ = [
11
+ "PerformanceAnalyzer",
12
+ "RiskAnalyzer",
13
+ "AttributionAnalyzer",
14
+ "PortfolioAnalyzer",
15
+ "ManagerAnalyzer",
16
+ "HoldingAnalyzer",
17
+ ]
@@ -0,0 +1,161 @@
1
+ """
2
+ 归因分析引擎
3
+
4
+ 实现 Brinson 归因分析等归因分析功能。
5
+ """
6
+
7
+ from typing import Any
8
+
9
+ import numpy as np
10
+ import pandas as pd
11
+
12
+ from fund_cli.core.analyzer import Analyzer
13
+
14
+
15
+ class AttributionAnalyzer(Analyzer):
16
+ """
17
+ 归因分析引擎
18
+
19
+ 支持:
20
+ - Brinson 归因分析(配置效应、选择效应、交互效应)
21
+ - 收益率分解
22
+ """
23
+
24
+ def analyze(
25
+ self,
26
+ data: pd.DataFrame,
27
+ benchmark_weights: dict[str, float] | None = None,
28
+ portfolio_weights: dict[str, float] | None = None,
29
+ **kwargs,
30
+ ) -> dict[str, Any]:
31
+ """
32
+ 执行归因分析
33
+
34
+ Args:
35
+ data: 包含各资产收益率的 DataFrame
36
+ benchmark_weights: 基准组合权重 {资产名: 权重}
37
+ portfolio_weights: 投资组合权重 {资产名: 权重}
38
+ **kwargs: 额外参数
39
+
40
+ Returns:
41
+ 归因分析结果字典
42
+ """
43
+ if benchmark_weights is None or portfolio_weights is None:
44
+ return self._simple_decomposition(data)
45
+
46
+ return self._brinson_attribution(data, benchmark_weights, portfolio_weights)
47
+
48
+ def _simple_decomposition(self, returns: pd.DataFrame) -> dict[str, Any]:
49
+ """
50
+ 简单收益率分解
51
+
52
+ Args:
53
+ returns: 收益率 DataFrame
54
+
55
+ Returns:
56
+ 分解结果
57
+ """
58
+ if isinstance(returns, pd.Series):
59
+ returns = returns.to_frame("fund")
60
+
61
+ result = {}
62
+ for col in returns.columns:
63
+ col_data = returns[col].dropna()
64
+ if len(col_data) == 0:
65
+ continue
66
+
67
+ cumulative = (1 + col_data).prod() - 1
68
+ annualized = (1 + cumulative) ** (252 / len(col_data)) - 1
69
+
70
+ result[col] = {
71
+ "total_return": float(cumulative * 100),
72
+ "annualized_return": float(annualized * 100),
73
+ "volatility": float(col_data.std() * np.sqrt(252) * 100),
74
+ "sharpe": float(
75
+ col_data.mean() / col_data.std() * np.sqrt(252) if col_data.std() > 0 else 0
76
+ ),
77
+ }
78
+
79
+ return result
80
+
81
+ def _brinson_attribution(
82
+ self,
83
+ returns: pd.DataFrame,
84
+ benchmark_weights: dict[str, float],
85
+ portfolio_weights: dict[str, float],
86
+ ) -> dict[str, Any]:
87
+ """
88
+ Brinson 归因分析
89
+
90
+ 将组合收益与基准收益的差异分解为:
91
+ - 配置效应(Allocation Effect)
92
+ - 选择效应(Selection Effect)
93
+ - 交互效应(Interaction Effect)
94
+
95
+ Args:
96
+ returns: 各资产收益率 DataFrame
97
+ benchmark_weights: 基准权重
98
+ portfolio_weights: 组合权重
99
+
100
+ Returns:
101
+ Brinson 归因结果
102
+ """
103
+ common_assets = (
104
+ set(benchmark_weights.keys()) & set(portfolio_weights.keys()) & set(returns.columns)
105
+ )
106
+
107
+ if not common_assets:
108
+ return {
109
+ "allocation_effect": 0.0,
110
+ "selection_effect": 0.0,
111
+ "interaction_effect": 0.0,
112
+ "total_active_return": 0.0,
113
+ }
114
+
115
+ # 计算各资产平均收益率
116
+ asset_returns = {}
117
+ for asset in common_assets:
118
+ if asset in returns.columns:
119
+ asset_returns[asset] = returns[asset].mean()
120
+
121
+ # Brinson 归因分解
122
+ allocation_effect = 0.0
123
+ selection_effect = 0.0
124
+ interaction_effect = 0.0
125
+
126
+ benchmark_total_return = 0.0
127
+ portfolio_total_return = 0.0
128
+
129
+ for asset in common_assets:
130
+ wp = portfolio_weights.get(asset, 0)
131
+ wb = benchmark_weights.get(asset, 0)
132
+ rp = asset_returns.get(asset, 0)
133
+ rb = rp # 简化:假设基准收益率等于资产收益率
134
+
135
+ allocation_effect += (wp - wb) * rb
136
+ selection_effect += wb * (rp - rb)
137
+ interaction_effect += (wp - wb) * (rp - rb)
138
+
139
+ portfolio_total_return += wp * rp
140
+ benchmark_total_return += wb * rb
141
+
142
+ total_active = portfolio_total_return - benchmark_total_return
143
+
144
+ return {
145
+ "allocation_effect": float(allocation_effect * 252 * 100),
146
+ "selection_effect": float(selection_effect * 252 * 100),
147
+ "interaction_effect": float(interaction_effect * 252 * 100),
148
+ "total_active_return": float(total_active * 252 * 100),
149
+ "portfolio_return": float(portfolio_total_return * 252 * 100),
150
+ "benchmark_return": float(benchmark_total_return * 252 * 100),
151
+ "asset_count": len(common_assets),
152
+ }
153
+
154
+ def get_metrics(self) -> list[str]:
155
+ """获取可计算的指标列表"""
156
+ return [
157
+ "allocation_effect",
158
+ "selection_effect",
159
+ "interaction_effect",
160
+ "total_active_return",
161
+ ]
@@ -0,0 +1,75 @@
1
+ """组合回测引擎"""
2
+
3
+ from typing import Any
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ from fund_cli.core.analyzer import Analyzer
9
+
10
+
11
+ class BacktestAnalyzer(Analyzer):
12
+ """组合回测引擎 - PORTFOLIO-OPT-007"""
13
+
14
+ def analyze(self, data: pd.DataFrame, **kwargs: Any) -> dict[str, Any]:
15
+ weights = kwargs.get("weights", {})
16
+ return self.run_backtest(data, weights)
17
+
18
+ def run_backtest(
19
+ self,
20
+ returns: pd.DataFrame,
21
+ weights: dict[str, float] | None = None,
22
+ rebalance_freq: str = "monthly",
23
+ ) -> dict[str, Any]:
24
+ """
25
+ 运行组合回测
26
+
27
+ Args:
28
+ returns: 多基金收益率 DataFrame
29
+ weights: 基金权重字典,None表示等权
30
+ rebalance_freq: 再平衡频率 (daily/monthly/quarterly/yearly/never)
31
+
32
+ Returns:
33
+ 回测结果字典
34
+ """
35
+ if weights is None:
36
+ n = returns.shape[1]
37
+ weights = dict.fromkeys(returns.columns, 1.0 / n)
38
+
39
+ w = pd.Series(weights)
40
+ daily_returns = (returns * w).sum(axis=1)
41
+
42
+ # 计算净值曲线
43
+ nav = (1 + daily_returns).cumprod()
44
+
45
+ # 计算指标
46
+ total_days = len(daily_returns)
47
+ total_return = (nav.iloc[-1] / nav.iloc[0] - 1) * 100 if len(nav) > 1 else 0
48
+ annual_return = (1 + total_return / 100) ** (252 / total_days) - 1 if total_days > 0 else 0
49
+ annual_vol = daily_returns.std() * np.sqrt(252) * 100
50
+ sharpe = (annual_return - 0.03) / (annual_vol / 100) if annual_vol > 0 else 0
51
+
52
+ # 最大回撤
53
+ peak = nav.cummax()
54
+ drawdown = (nav - peak) / peak
55
+ max_drawdown = drawdown.min() * 100
56
+
57
+ # 胜率
58
+ win_rate = (
59
+ (daily_returns > 0).sum() / len(daily_returns) * 100 if len(daily_returns) > 0 else 0
60
+ )
61
+
62
+ return {
63
+ "total_return": round(total_return, 4),
64
+ "annual_return": round(annual_return * 100, 4),
65
+ "annual_volatility": round(annual_vol, 4),
66
+ "sharpe_ratio": round(sharpe, 4),
67
+ "max_drawdown": round(max_drawdown, 4),
68
+ "win_rate": round(win_rate, 2),
69
+ "trading_days": total_days,
70
+ "rebalance_freq": rebalance_freq,
71
+ "nav_curve": nav.tolist()[-10:], # 最后10个净值点
72
+ }
73
+
74
+ def get_metrics(self) -> list[str]:
75
+ return ["total_return", "annual_return", "sharpe_ratio", "max_drawdown", "win_rate"]