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
fund_cli/data/models.py
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""
|
|
2
|
+
数据模型定义
|
|
3
|
+
|
|
4
|
+
使用 Pydantic 定义基金相关的数据模型。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import date, datetime
|
|
8
|
+
from enum import Enum as PyEnum
|
|
9
|
+
|
|
10
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FundType(str, PyEnum):
|
|
14
|
+
"""基金类型枚举"""
|
|
15
|
+
|
|
16
|
+
EQUITY = "股票型"
|
|
17
|
+
BOND = "债券型"
|
|
18
|
+
MIXED = "混合型"
|
|
19
|
+
INDEX = "指数型"
|
|
20
|
+
QDII = "QDII"
|
|
21
|
+
MONEY = "货币型"
|
|
22
|
+
ETF = "ETF"
|
|
23
|
+
LOF = "LOF"
|
|
24
|
+
OTHER = "其他"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FundInfo(BaseModel):
|
|
28
|
+
"""
|
|
29
|
+
基金基础信息模型
|
|
30
|
+
|
|
31
|
+
包含基金的基本属性信息。
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
model_config = ConfigDict(
|
|
35
|
+
str_strip_whitespace=True,
|
|
36
|
+
validate_assignment=True,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# 基本信息
|
|
40
|
+
code: str = Field(..., min_length=6, max_length=6, description="基金代码")
|
|
41
|
+
name: str = Field(..., min_length=1, description="基金名称")
|
|
42
|
+
type: FundType = Field(..., description="基金类型")
|
|
43
|
+
|
|
44
|
+
# 详细信息
|
|
45
|
+
establish_date: date | None = Field(None, description="成立日期")
|
|
46
|
+
manager: str | None = Field(None, description="基金经理")
|
|
47
|
+
company: str | None = Field(None, description="基金公司")
|
|
48
|
+
scale: float | None = Field(None, ge=0, description="规模(亿元)")
|
|
49
|
+
|
|
50
|
+
# 业绩信息
|
|
51
|
+
return_1m: float | None = Field(None, description="近1月收益率(%)")
|
|
52
|
+
return_3m: float | None = Field(None, description="近3月收益率(%)")
|
|
53
|
+
return_6m: float | None = Field(None, description="近6月收益率(%)")
|
|
54
|
+
return_1y: float | None = Field(None, description="近1年收益率(%)")
|
|
55
|
+
return_3y: float | None = Field(None, description="近3年收益率(%)")
|
|
56
|
+
return_this_year: float | None = Field(None, description="今年以来收益率(%)")
|
|
57
|
+
|
|
58
|
+
# 风险指标
|
|
59
|
+
max_drawdown: float | None = Field(None, description="最大回撤(%)")
|
|
60
|
+
sharpe_ratio: float | None = Field(None, description="夏普比率")
|
|
61
|
+
|
|
62
|
+
@field_validator("code")
|
|
63
|
+
@classmethod
|
|
64
|
+
def validate_code(cls, v: str) -> str:
|
|
65
|
+
"""验证基金代码格式"""
|
|
66
|
+
if not v.isdigit():
|
|
67
|
+
raise ValueError("基金代码必须为6位数字")
|
|
68
|
+
return v
|
|
69
|
+
|
|
70
|
+
def __repr__(self) -> str:
|
|
71
|
+
return f"FundInfo(code={self.code!r}, name={self.name!r}, type={self.type.value!r})"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class NavData(BaseModel):
|
|
75
|
+
"""
|
|
76
|
+
单条净值数据模型
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
model_config = ConfigDict(
|
|
80
|
+
str_strip_whitespace=True,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
fund_code: str = Field(..., description="基金代码")
|
|
84
|
+
nav_date: date = Field(..., description="净值日期")
|
|
85
|
+
unit_nav: float = Field(..., gt=0, description="单位净值")
|
|
86
|
+
accumulated_nav: float | None = Field(None, gt=0, description="累计净值")
|
|
87
|
+
daily_return: float | None = Field(None, description="日收益率(%)")
|
|
88
|
+
|
|
89
|
+
@field_validator("fund_code")
|
|
90
|
+
@classmethod
|
|
91
|
+
def validate_fund_code(cls, v: str) -> str:
|
|
92
|
+
"""验证基金代码格式"""
|
|
93
|
+
if not v.isdigit() or len(v) != 6:
|
|
94
|
+
raise ValueError("基金代码必须为6位数字")
|
|
95
|
+
return v
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class FundFilter(BaseModel):
|
|
99
|
+
"""
|
|
100
|
+
基金筛选条件模型
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
model_config = ConfigDict(
|
|
104
|
+
str_strip_whitespace=True,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
# 基础筛选
|
|
108
|
+
fund_type: FundType | None = Field(None, description="基金类型")
|
|
109
|
+
company: str | None = Field(None, description="基金公司")
|
|
110
|
+
manager: str | None = Field(None, description="基金经理")
|
|
111
|
+
|
|
112
|
+
# 规模筛选
|
|
113
|
+
min_scale: float | None = Field(None, ge=0, description="最小规模(亿元)")
|
|
114
|
+
max_scale: float | None = Field(None, ge=0, description="最大规模(亿元)")
|
|
115
|
+
|
|
116
|
+
# 业绩筛选
|
|
117
|
+
min_return_1y: float | None = Field(None, description="近1年收益率下限(%)")
|
|
118
|
+
max_return_1y: float | None = Field(None, description="近1年收益率上限(%)")
|
|
119
|
+
|
|
120
|
+
# 风险筛选
|
|
121
|
+
max_drawdown: float | None = Field(None, description="最大回撤上限(%)")
|
|
122
|
+
min_sharpe: float | None = Field(None, description="夏普比率下限")
|
|
123
|
+
|
|
124
|
+
# V1.0 新增筛选
|
|
125
|
+
fee_rate_max: float | None = Field(None, ge=0, description="管理费率上限(%)")
|
|
126
|
+
manager_name: str | None = Field(None, description="基金经理名称")
|
|
127
|
+
min_rating: int | None = Field(None, ge=1, le=5, description="最低评级(星)")
|
|
128
|
+
|
|
129
|
+
# 关键词搜索
|
|
130
|
+
keyword: str | None = Field(None, description="关键词")
|
|
131
|
+
|
|
132
|
+
# 排序
|
|
133
|
+
sort_by: str | None = Field(None, description="排序字段")
|
|
134
|
+
sort_order: str = Field(default="desc", description="排序方向: asc/desc")
|
|
135
|
+
|
|
136
|
+
# 分页
|
|
137
|
+
limit: int = Field(default=100, ge=1, le=1000, description="返回数量限制")
|
|
138
|
+
offset: int = Field(default=0, ge=0, description="偏移量")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class AnalysisResult(BaseModel):
|
|
142
|
+
"""
|
|
143
|
+
分析结果模型
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
model_config = ConfigDict(
|
|
147
|
+
arbitrary_types_allowed=True,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
fund_code: str = Field(..., description="基金代码")
|
|
151
|
+
fund_name: str = Field(..., description="基金名称")
|
|
152
|
+
analysis_date: date = Field(default_factory=date.today, description="分析日期")
|
|
153
|
+
start_date: date = Field(..., description="分析开始日期")
|
|
154
|
+
end_date: date = Field(..., description="分析结束日期")
|
|
155
|
+
|
|
156
|
+
# 收益指标
|
|
157
|
+
total_return: float = Field(..., description="总收益率(%)")
|
|
158
|
+
annualized_return: float = Field(..., description="年化收益率(%)")
|
|
159
|
+
|
|
160
|
+
# 风险指标
|
|
161
|
+
volatility: float = Field(..., description="年化波动率(%)")
|
|
162
|
+
max_drawdown: float = Field(..., description="最大回撤(%)")
|
|
163
|
+
var_95: float | None = Field(None, description="VaR(95%)")
|
|
164
|
+
|
|
165
|
+
# 风险调整收益
|
|
166
|
+
sharpe_ratio: float = Field(..., description="夏普比率")
|
|
167
|
+
sortino_ratio: float | None = Field(None, description="索提诺比率")
|
|
168
|
+
calmar_ratio: float | None = Field(None, description="卡玛比率")
|
|
169
|
+
|
|
170
|
+
# 相对指标
|
|
171
|
+
alpha: float | None = Field(None, description="Alpha")
|
|
172
|
+
beta: float | None = Field(None, description="Beta")
|
|
173
|
+
information_ratio: float | None = Field(None, description="信息比率")
|
|
174
|
+
tracking_error: float | None = Field(None, description="跟踪误差(%)")
|
|
175
|
+
|
|
176
|
+
def to_dict(self) -> dict:
|
|
177
|
+
"""转换为字典"""
|
|
178
|
+
return self.model_dump()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class HoldingInfo(BaseModel):
|
|
182
|
+
"""持仓信息模型"""
|
|
183
|
+
|
|
184
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
185
|
+
fund_code: str = Field(..., description="基金代码")
|
|
186
|
+
report_date: date = Field(..., description="报告日期")
|
|
187
|
+
stock_code: str = Field(..., description="股票代码")
|
|
188
|
+
stock_name: str = Field(..., description="股票名称")
|
|
189
|
+
weight: float = Field(..., ge=0, le=100, description="占净值比例(%)")
|
|
190
|
+
market_value: float | None = Field(None, ge=0, description="持仓市值(万元)")
|
|
191
|
+
industry: str | None = Field(None, description="所属行业")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
class FundManager(BaseModel):
|
|
195
|
+
"""基金经理模型"""
|
|
196
|
+
|
|
197
|
+
model_config = ConfigDict(str_strip_whitespace=True, validate_assignment=True)
|
|
198
|
+
name: str = Field(..., min_length=1, description="经理姓名")
|
|
199
|
+
fund_code: str = Field(..., description="基金代码")
|
|
200
|
+
fund_name: str | None = Field(None, description="基金名称")
|
|
201
|
+
start_date: date | None = Field(None, description="任职起始日")
|
|
202
|
+
tenure_days: int | None = Field(None, ge=0, description="任职天数")
|
|
203
|
+
total_return: float | None = Field(None, description="任期总收益率(%)")
|
|
204
|
+
annual_return: float | None = Field(None, description="年化收益率(%)")
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class HoldingSnapshot(BaseModel):
|
|
208
|
+
"""持仓快照模型"""
|
|
209
|
+
|
|
210
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
211
|
+
fund_code: str = Field(..., description="基金代码")
|
|
212
|
+
report_date: date = Field(..., description="报告日期")
|
|
213
|
+
total_stock_count: int = Field(..., ge=0, description="持股数量")
|
|
214
|
+
top10_weight: float = Field(..., ge=0, le=100, description="前十大重仓股占比(%)")
|
|
215
|
+
industry_distribution: dict[str, float] = Field(default_factory=dict, description="行业分布")
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class OptimizationConstraint(BaseModel):
|
|
219
|
+
"""优化约束条件模型"""
|
|
220
|
+
|
|
221
|
+
min_weight: float = Field(default=0.0, ge=0, le=1, description="最小权重")
|
|
222
|
+
max_weight: float = Field(default=1.0, ge=0, le=1, description="最大权重")
|
|
223
|
+
target_return: float | None = Field(None, description="目标收益率")
|
|
224
|
+
max_volatility: float | None = Field(None, ge=0, description="最大波动率")
|
|
225
|
+
max_drawdown: float | None = Field(None, le=0, description="最大回撤")
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
class MonitorRule(BaseModel):
|
|
229
|
+
"""监控规则模型"""
|
|
230
|
+
|
|
231
|
+
fund_code: str = Field(..., description="基金代码")
|
|
232
|
+
rule_type: str = Field(
|
|
233
|
+
default="nav_change", description="规则类型: nav_change/threshold/drawdown"
|
|
234
|
+
)
|
|
235
|
+
threshold: float = Field(default=-2.0, description="阈值")
|
|
236
|
+
enabled: bool = Field(default=True, description="是否启用")
|
|
237
|
+
created_at: datetime = Field(default_factory=datetime.now, description="创建时间")
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class OutputConfig(BaseModel):
|
|
241
|
+
"""输出格式配置模型"""
|
|
242
|
+
|
|
243
|
+
default_format: str = Field(default="table", description="默认输出格式: table/csv/json")
|
|
244
|
+
csv_encoding: str = Field(default="utf-8-sig", description="CSV编码")
|
|
245
|
+
csv_delimiter: str = Field(default=",", description="CSV分隔符")
|
|
246
|
+
json_indent: int = Field(default=2, ge=0, description="JSON缩进")
|
|
247
|
+
number_decimal: int = Field(default=2, ge=0, le=6, description="数字小数位")
|
|
248
|
+
date_format: str = Field(default="%Y-%m-%d", description="日期格式")
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
装饰器模块
|
|
3
|
+
|
|
4
|
+
提供常用装饰器。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from collections.abc import Callable
|
|
9
|
+
from functools import wraps
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def timer(func: Callable) -> Callable:
|
|
14
|
+
"""
|
|
15
|
+
计时装饰器
|
|
16
|
+
|
|
17
|
+
记录函数执行时间。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
@wraps(func)
|
|
21
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
22
|
+
start_time = time.time()
|
|
23
|
+
result = func(*args, **kwargs)
|
|
24
|
+
end_time = time.time()
|
|
25
|
+
elapsed = end_time - start_time
|
|
26
|
+
print(f"[{func.__name__}] 执行时间: {elapsed:.2f}秒")
|
|
27
|
+
return result
|
|
28
|
+
|
|
29
|
+
return wrapper
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def retry(
|
|
33
|
+
max_attempts: int = 3,
|
|
34
|
+
delay: float = 1.0,
|
|
35
|
+
exceptions: tuple = (Exception,),
|
|
36
|
+
) -> Callable:
|
|
37
|
+
"""
|
|
38
|
+
重试装饰器
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
max_attempts: 最大重试次数
|
|
42
|
+
delay: 重试间隔(秒)
|
|
43
|
+
exceptions: 需要重试的异常类型
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
def decorator(func: Callable) -> Callable:
|
|
47
|
+
@wraps(func)
|
|
48
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
49
|
+
last_exception = None
|
|
50
|
+
|
|
51
|
+
for attempt in range(max_attempts):
|
|
52
|
+
try:
|
|
53
|
+
return func(*args, **kwargs)
|
|
54
|
+
except exceptions as e:
|
|
55
|
+
last_exception = e
|
|
56
|
+
if attempt < max_attempts - 1:
|
|
57
|
+
time.sleep(delay)
|
|
58
|
+
|
|
59
|
+
raise last_exception
|
|
60
|
+
|
|
61
|
+
return wrapper
|
|
62
|
+
|
|
63
|
+
return decorator
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def deprecated(message: str = "") -> Callable:
|
|
67
|
+
"""
|
|
68
|
+
废弃警告装饰器
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
message: 废弃说明
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
def decorator(func: Callable) -> Callable:
|
|
75
|
+
@wraps(func)
|
|
76
|
+
def wrapper(*args, **kwargs) -> Any:
|
|
77
|
+
import warnings
|
|
78
|
+
|
|
79
|
+
warnings.warn(
|
|
80
|
+
f"{func.__name__} 已废弃。{message}",
|
|
81
|
+
DeprecationWarning,
|
|
82
|
+
stacklevel=2,
|
|
83
|
+
)
|
|
84
|
+
return func(*args, **kwargs)
|
|
85
|
+
|
|
86
|
+
return wrapper
|
|
87
|
+
|
|
88
|
+
return decorator
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
"""
|
|
2
|
+
辅助函数模块
|
|
3
|
+
|
|
4
|
+
提供通用辅助函数。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import date, datetime
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def format_percentage(value: float | None, decimal: int = 2) -> str:
|
|
11
|
+
"""
|
|
12
|
+
格式化百分比显示
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
value: 数值
|
|
16
|
+
decimal: 小数位数
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
格式化后的字符串
|
|
20
|
+
"""
|
|
21
|
+
if value is None:
|
|
22
|
+
return "-"
|
|
23
|
+
|
|
24
|
+
return f"{value:.{decimal}f}%"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def format_currency(
|
|
28
|
+
value: float | None,
|
|
29
|
+
unit: str = "亿",
|
|
30
|
+
decimal: int = 2,
|
|
31
|
+
) -> str:
|
|
32
|
+
"""
|
|
33
|
+
格式化货币显示
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
value: 数值
|
|
37
|
+
unit: 单位
|
|
38
|
+
decimal: 小数位数
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
格式化后的字符串
|
|
42
|
+
"""
|
|
43
|
+
if value is None:
|
|
44
|
+
return "-"
|
|
45
|
+
|
|
46
|
+
return f"{value:.{decimal}f}{unit}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def format_date(value: date | datetime | None, fmt: str = "%Y-%m-%d") -> str:
|
|
50
|
+
"""
|
|
51
|
+
格式化日期显示
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
value: 日期值
|
|
55
|
+
fmt: 格式字符串
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
格式化后的字符串
|
|
59
|
+
"""
|
|
60
|
+
if value is None:
|
|
61
|
+
return "-"
|
|
62
|
+
|
|
63
|
+
if isinstance(value, datetime):
|
|
64
|
+
return value.strftime(fmt)
|
|
65
|
+
elif isinstance(value, date):
|
|
66
|
+
return value.strftime(fmt)
|
|
67
|
+
|
|
68
|
+
return str(value)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def format_number(value: float | None, decimal: int = 2) -> str:
|
|
72
|
+
"""
|
|
73
|
+
格式化数字显示
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
value: 数值
|
|
77
|
+
decimal: 小数位数
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
格式化后的字符串
|
|
81
|
+
"""
|
|
82
|
+
if value is None:
|
|
83
|
+
return "-"
|
|
84
|
+
|
|
85
|
+
return f"{value:.{decimal}f}"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def safe_divide(
|
|
89
|
+
numerator: float,
|
|
90
|
+
denominator: float,
|
|
91
|
+
default: float = 0.0,
|
|
92
|
+
) -> float:
|
|
93
|
+
"""
|
|
94
|
+
安全除法
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
numerator: 分子
|
|
98
|
+
denominator: 分母
|
|
99
|
+
default: 默认值(分母为0时)
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
计算结果
|
|
103
|
+
"""
|
|
104
|
+
if denominator == 0:
|
|
105
|
+
return default
|
|
106
|
+
return numerator / denominator
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def truncate_string(s: str, max_length: int = 20, suffix: str = "...") -> str:
|
|
110
|
+
"""
|
|
111
|
+
截断字符串
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
s: 原字符串
|
|
115
|
+
max_length: 最大长度
|
|
116
|
+
suffix: 后缀
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
截断后的字符串
|
|
120
|
+
"""
|
|
121
|
+
if not s:
|
|
122
|
+
return ""
|
|
123
|
+
|
|
124
|
+
if len(s) <= max_length:
|
|
125
|
+
return s
|
|
126
|
+
|
|
127
|
+
return s[: max_length - len(suffix)] + suffix
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""
|
|
2
|
+
验证器模块
|
|
3
|
+
|
|
4
|
+
提供各类数据验证功能。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def validate_fund_code(fund_code: str) -> bool:
|
|
11
|
+
"""
|
|
12
|
+
验证基金代码格式
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
fund_code: 基金代码
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
是否有效
|
|
19
|
+
"""
|
|
20
|
+
if not fund_code:
|
|
21
|
+
return False
|
|
22
|
+
|
|
23
|
+
# 基金代码应为6位数字
|
|
24
|
+
if not re.match(r"^\d{6}$", fund_code):
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
return True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def validate_date(date_str: str) -> bool:
|
|
31
|
+
"""
|
|
32
|
+
验证日期格式
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
date_str: 日期字符串 (YYYY-MM-DD)
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
是否有效
|
|
39
|
+
"""
|
|
40
|
+
if not date_str:
|
|
41
|
+
return False
|
|
42
|
+
|
|
43
|
+
pattern = r"^\d{4}-\d{2}-\d{2}$"
|
|
44
|
+
if not re.match(pattern, date_str):
|
|
45
|
+
return False
|
|
46
|
+
|
|
47
|
+
return True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def validate_positive_number(value: float | None) -> bool:
|
|
51
|
+
"""
|
|
52
|
+
验证正数
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
value: 数值
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
是否为正数
|
|
59
|
+
"""
|
|
60
|
+
if value is None:
|
|
61
|
+
return False
|
|
62
|
+
return value > 0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def validate_percentage(value: float | None) -> bool:
|
|
66
|
+
"""
|
|
67
|
+
验证百分比范围
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
value: 百分比值
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
是否在有效范围内
|
|
74
|
+
"""
|
|
75
|
+
if value is None:
|
|
76
|
+
return False
|
|
77
|
+
return -100 <= value <= 100
|
fund_cli/views/charts.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
图表渲染模块
|
|
3
|
+
|
|
4
|
+
提供图表生成功能。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ChartRenderer:
|
|
11
|
+
"""
|
|
12
|
+
图表渲染器
|
|
13
|
+
|
|
14
|
+
使用 Plotly 生成交互式图表。
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self):
|
|
18
|
+
"""初始化图表渲染器"""
|
|
19
|
+
self._plotly = None
|
|
20
|
+
|
|
21
|
+
def _get_plotly(self):
|
|
22
|
+
"""延迟加载 Plotly"""
|
|
23
|
+
if self._plotly is None:
|
|
24
|
+
try:
|
|
25
|
+
import plotly.graph_objects as go
|
|
26
|
+
|
|
27
|
+
self._plotly = go
|
|
28
|
+
except ImportError as e:
|
|
29
|
+
raise ImportError("Plotly 未安装,请运行: pip install plotly") from e
|
|
30
|
+
return self._plotly
|
|
31
|
+
|
|
32
|
+
def render_nav_chart(
|
|
33
|
+
self,
|
|
34
|
+
nav_data: pd.DataFrame,
|
|
35
|
+
title: str = "净值走势",
|
|
36
|
+
) -> dict:
|
|
37
|
+
"""
|
|
38
|
+
渲染净值走势图
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
nav_data: 净值数据
|
|
42
|
+
title: 图表标题
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Plotly Figure 字典
|
|
46
|
+
"""
|
|
47
|
+
go = self._get_plotly()
|
|
48
|
+
|
|
49
|
+
fig = go.Figure()
|
|
50
|
+
|
|
51
|
+
# 单位净值
|
|
52
|
+
fig.add_trace(
|
|
53
|
+
go.Scatter(
|
|
54
|
+
x=nav_data["nav_date"],
|
|
55
|
+
y=nav_data["unit_nav"],
|
|
56
|
+
mode="lines",
|
|
57
|
+
name="单位净值",
|
|
58
|
+
line={"color": "blue"},
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# 累计净值
|
|
63
|
+
if "accumulated_nav" in nav_data.columns:
|
|
64
|
+
fig.add_trace(
|
|
65
|
+
go.Scatter(
|
|
66
|
+
x=nav_data["nav_date"],
|
|
67
|
+
y=nav_data["accumulated_nav"],
|
|
68
|
+
mode="lines",
|
|
69
|
+
name="累计净值",
|
|
70
|
+
line={"color": "green", "dash": "dot"},
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
fig.update_layout(
|
|
75
|
+
title=title,
|
|
76
|
+
xaxis_title="日期",
|
|
77
|
+
yaxis_title="净值",
|
|
78
|
+
hovermode="x unified",
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return fig.to_dict()
|
|
82
|
+
|
|
83
|
+
def render_drawdown_chart(
|
|
84
|
+
self,
|
|
85
|
+
drawdown: pd.Series,
|
|
86
|
+
title: str = "回撤分析",
|
|
87
|
+
) -> dict:
|
|
88
|
+
"""
|
|
89
|
+
渲染回撤图表
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
drawdown: 回撤序列
|
|
93
|
+
title: 图表标题
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Plotly Figure 字典
|
|
97
|
+
"""
|
|
98
|
+
go = self._get_plotly()
|
|
99
|
+
|
|
100
|
+
fig = go.Figure()
|
|
101
|
+
|
|
102
|
+
fig.add_trace(
|
|
103
|
+
go.Scatter(
|
|
104
|
+
x=drawdown.index,
|
|
105
|
+
y=drawdown * 100,
|
|
106
|
+
mode="lines",
|
|
107
|
+
name="回撤",
|
|
108
|
+
fill="tozeroy",
|
|
109
|
+
line={"color": "red"},
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
fig.update_layout(
|
|
114
|
+
title=title,
|
|
115
|
+
xaxis_title="日期",
|
|
116
|
+
yaxis_title="回撤(%)",
|
|
117
|
+
hovermode="x unified",
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
return fig.to_dict()
|