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:
2026-05-25 00:15:59 +08:00
parent c07974ad94
commit 798a316ad5
2 changed files with 33 additions and 14 deletions

View File

@@ -399,29 +399,30 @@ class TushareSource:
复权公式:
- 后复权 (hfq): close_hfq = close × adj_factor
- 前复权 (qfq): close_qfq = close × adj_factor / latest_factor
Args:
code: ETF代码'159915.SZ', '518880.SH'
start_date: 开始日期 'YYYY-MM-DD'
end_date: 结束日期 'YYYY-MM-DD'
adj: 复权类型,ETF 仅支持 'hfq'(后复权)
adj: 复权类型,支持 'hfq'(后复权)'qfq'(前复权)
Returns:
DataFrame with columns: date, code, open, high, low, close, volume, adj_factor
"""
if adj != 'hfq':
raise ValueError(f"ETF 仅支持 adj='hfq'(后复权),当前: {adj}")
if adj not in ['qfq', 'hfq']:
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
1. 使用 fund_daily() 获取原始价格
2. 使用 fund_adj() 获取复权因子
3. 计算复权价格close_hfq = close × adj_factor
3. 根据 adj 参数计算复权价格
fund_adj 单次限 2000 条,按 5 年分段请求再拼接。
@@ -496,14 +497,32 @@ class TushareSource:
df_adj_aligned = df_adj.reindex(df_daily.index)
df_adj_aligned['adj_factor'] = df_adj_aligned['adj_factor'].ffill().fillna(1.0)
# 步骤 5: 计算复权价格
# 步骤 5: 计算复权价格
df = df_daily.copy()
df['adj_factor'] = df_adj_aligned['adj_factor']
df['close_hfq'] = (df['close'] * df['adj_factor']).round(4)
df['open'] = (df['open'] * df['adj_factor']).round(4)
df['high'] = (df['high'] * df['adj_factor']).round(4)
df['low'] = (df['low'] * df['adj_factor']).round(4)
df['close'] = df['close_hfq'] # close 列设为后复权价格
if adj == 'hfq':
# 后复权: close_hfq = close × adj_factor
df['close_hfq'] = (df['close'] * df['adj_factor']).round(4)
df['open'] = (df['open'] * df['adj_factor']).round(4)
df['high'] = (df['high'] * df['adj_factor']).round(4)
df['low'] = (df['low'] * df['adj_factor']).round(4)
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
return df[['code', 'open', 'high', 'low', 'close', 'volume', 'adj_factor']]

View File

@@ -171,7 +171,7 @@ class UniversalDataFetcher:
# 各资产类型支持的 adj 参数
VALID_ADJ_BY_TYPE = {
AssetType.CHINA_INDEX: ['raw'], # 指数无复权
AssetType.CHINA_ETF: ['raw', 'hfq'], # ETF 支持后复权
AssetType.CHINA_ETF: ['raw', 'qfq', 'hfq'], # ETF 支持前复权/后复权
AssetType.CHINA_STOCK: ['raw', 'qfq', 'hfq'],
AssetType.US_INDEX: ['raw'], # 指数无复权
AssetType.US_STOCK: ['raw', 'qfq', 'hfq'],