feat: ETF复权功能扩展至支持前复权qfq
核心变更:
- TushareSource: _fetch_etf_adj() 支持 qfq 和 hfq 双模式
* 后复权(hfq): close × adj_factor
* 前复权(qfq): close × adj_factor / latest_factor
- UniversalDataFetcher: VALID_ADJ_BY_TYPE 更新
* CHINA_ETF: ['raw', 'hfq'] → ['raw', 'qfq', 'hfq']
复权公式验证:
- 纳指ETF(513100.SH): HFQ / QFQ = latest_factor (5.0020) ✅
- 5/5 个交易日全部通过验证
技术实现:
- fetch_etf_adj(): 公共接口支持 adj='qfq' 或 'hfq'
- _fetch_etf_adj(): 内部实现根据 adj 参数分支计算
- 前复权使用全量最新复权因子确保准确性
This commit is contained in:
@@ -399,29 +399,30 @@ class TushareSource:
|
|||||||
|
|
||||||
复权公式:
|
复权公式:
|
||||||
- 后复权 (hfq): close_hfq = close × adj_factor
|
- 后复权 (hfq): close_hfq = close × adj_factor
|
||||||
|
- 前复权 (qfq): close_qfq = close × adj_factor / latest_factor
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
code: ETF代码,如 '159915.SZ', '518880.SH'
|
code: ETF代码,如 '159915.SZ', '518880.SH'
|
||||||
start_date: 开始日期 'YYYY-MM-DD'
|
start_date: 开始日期 'YYYY-MM-DD'
|
||||||
end_date: 结束日期 'YYYY-MM-DD'
|
end_date: 结束日期 'YYYY-MM-DD'
|
||||||
adj: 复权类型,ETF 仅支持 'hfq'(后复权)
|
adj: 复权类型,支持 'hfq'(后复权)或 'qfq'(前复权)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
DataFrame with columns: date, code, open, high, low, close, volume, adj_factor
|
DataFrame with columns: date, code, open, high, low, close, volume, adj_factor
|
||||||
"""
|
"""
|
||||||
if adj != 'hfq':
|
if adj not in ['qfq', 'hfq']:
|
||||||
raise ValueError(f"ETF 仅支持 adj='hfq'(后复权),当前: {adj}")
|
raise ValueError(f"ETF adj 参数必须是 'qfq' 或 'hfq',当前: {adj}")
|
||||||
|
|
||||||
return self._fetch_etf_hfq(code, start_date, end_date)
|
return self._fetch_etf_adj(code, start_date, end_date, adj)
|
||||||
|
|
||||||
def _fetch_etf_hfq(self, code: str, start_date: str, end_date: str) -> Optional[pd.DataFrame]:
|
def _fetch_etf_adj(self, code: str, start_date: str, end_date: str, adj: str = 'hfq') -> Optional[pd.DataFrame]:
|
||||||
"""
|
"""
|
||||||
获取 ETF 后复权价格数据(内部方法)
|
获取 ETF 复权价格数据(内部方法)
|
||||||
|
|
||||||
自己实现复权计算(不使用 pro_bar):
|
自己实现复权计算(不使用 pro_bar):
|
||||||
1. 使用 fund_daily() 获取原始价格
|
1. 使用 fund_daily() 获取原始价格
|
||||||
2. 使用 fund_adj() 获取复权因子
|
2. 使用 fund_adj() 获取复权因子
|
||||||
3. 计算后复权价格:close_hfq = close × adj_factor
|
3. 根据 adj 参数计算复权价格
|
||||||
|
|
||||||
fund_adj 单次限 2000 条,按 5 年分段请求再拼接。
|
fund_adj 单次限 2000 条,按 5 年分段请求再拼接。
|
||||||
|
|
||||||
@@ -496,14 +497,32 @@ class TushareSource:
|
|||||||
df_adj_aligned = df_adj.reindex(df_daily.index)
|
df_adj_aligned = df_adj.reindex(df_daily.index)
|
||||||
df_adj_aligned['adj_factor'] = df_adj_aligned['adj_factor'].ffill().fillna(1.0)
|
df_adj_aligned['adj_factor'] = df_adj_aligned['adj_factor'].ffill().fillna(1.0)
|
||||||
|
|
||||||
# 步骤 5: 计算后复权价格
|
# 步骤 5: 计算复权价格
|
||||||
df = df_daily.copy()
|
df = df_daily.copy()
|
||||||
df['adj_factor'] = df_adj_aligned['adj_factor']
|
df['adj_factor'] = df_adj_aligned['adj_factor']
|
||||||
|
|
||||||
|
if adj == 'hfq':
|
||||||
|
# 后复权: close_hfq = close × adj_factor
|
||||||
df['close_hfq'] = (df['close'] * df['adj_factor']).round(4)
|
df['close_hfq'] = (df['close'] * df['adj_factor']).round(4)
|
||||||
df['open'] = (df['open'] * df['adj_factor']).round(4)
|
df['open'] = (df['open'] * df['adj_factor']).round(4)
|
||||||
df['high'] = (df['high'] * df['adj_factor']).round(4)
|
df['high'] = (df['high'] * df['adj_factor']).round(4)
|
||||||
df['low'] = (df['low'] * df['adj_factor']).round(4)
|
df['low'] = (df['low'] * df['adj_factor']).round(4)
|
||||||
df['close'] = df['close_hfq'] # close 列设为后复权价格
|
df['close'] = df['close_hfq'] # close 列设为后复权价格
|
||||||
|
elif adj == 'qfq':
|
||||||
|
# 前复权: close_qfq = close × adj_factor / latest_factor
|
||||||
|
# 获取全量最新复权因子
|
||||||
|
latest_factor = df_adj['adj_factor'].iloc[-1]
|
||||||
|
if latest_factor and latest_factor > 0:
|
||||||
|
adj_ratio = df['adj_factor'] / latest_factor
|
||||||
|
df['close_qfq'] = (df['close'] * adj_ratio).round(4)
|
||||||
|
df['open'] = (df['open'] * adj_ratio).round(4)
|
||||||
|
df['high'] = (df['high'] * adj_ratio).round(4)
|
||||||
|
df['low'] = (df['low'] * adj_ratio).round(4)
|
||||||
|
df['close'] = df['close_qfq'] # close 列设为前复权价格
|
||||||
|
else:
|
||||||
|
# 无有效复权因子,返回原始价格
|
||||||
|
df['close'] = df['close']
|
||||||
|
|
||||||
df['code'] = code
|
df['code'] = code
|
||||||
|
|
||||||
return df[['code', 'open', 'high', 'low', 'close', 'volume', 'adj_factor']]
|
return df[['code', 'open', 'high', 'low', 'close', 'volume', 'adj_factor']]
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ class UniversalDataFetcher:
|
|||||||
# 各资产类型支持的 adj 参数
|
# 各资产类型支持的 adj 参数
|
||||||
VALID_ADJ_BY_TYPE = {
|
VALID_ADJ_BY_TYPE = {
|
||||||
AssetType.CHINA_INDEX: ['raw'], # 指数无复权
|
AssetType.CHINA_INDEX: ['raw'], # 指数无复权
|
||||||
AssetType.CHINA_ETF: ['raw', 'hfq'], # ETF 仅支持后复权
|
AssetType.CHINA_ETF: ['raw', 'qfq', 'hfq'], # ETF 支持前复权/后复权
|
||||||
AssetType.CHINA_STOCK: ['raw', 'qfq', 'hfq'],
|
AssetType.CHINA_STOCK: ['raw', 'qfq', 'hfq'],
|
||||||
AssetType.US_INDEX: ['raw'], # 指数无复权
|
AssetType.US_INDEX: ['raw'], # 指数无复权
|
||||||
AssetType.US_STOCK: ['raw', 'qfq', 'hfq'],
|
AssetType.US_STOCK: ['raw', 'qfq', 'hfq'],
|
||||||
|
|||||||
Reference in New Issue
Block a user