From 798a316ad51379be5cb6a7a4fdac4b4830d0c84e Mon Sep 17 00:00:00 2001 From: aszerW Date: Mon, 25 May 2026 00:15:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20ETF=E5=A4=8D=E6=9D=83=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=89=A9=E5=B1=95=E8=87=B3=E6=94=AF=E6=8C=81=E5=89=8D=E5=A4=8D?= =?UTF-8?q?=E6=9D=83qfq?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心变更: - 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 参数分支计算 - 前复权使用全量最新复权因子确保准确性 --- datasource/tushare_source.py | 45 +++++++++++++++++++++++---------- datasource/universal_fetcher.py | 2 +- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/datasource/tushare_source.py b/datasource/tushare_source.py index 556f503..ea78cb0 100644 --- a/datasource/tushare_source.py +++ b/datasource/tushare_source.py @@ -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']] diff --git a/datasource/universal_fetcher.py b/datasource/universal_fetcher.py index 0b9a375..476dade 100644 --- a/datasource/universal_fetcher.py +++ b/datasource/universal_fetcher.py @@ -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'],