📋 代码审核报告(2026-08-08,kimi-k2.7-code 全量复审:8文件) #17

Open
opened 2026-08-08 07:46:27 +08:00 by xiaowu · 2 comments

代码审核报告(2026-08-08,kimi-k2.7-code 全量复审)

审核模型: kimi-k2.7-code
审核时间: 2026-08-08 07:45
审核范围: quanxiel 全部 8 个 Python 文件(8 个模块)

关联:本次为 #1(2026-07-31 审核)的补充复审,覆盖当时超时/缺失的 strategy.py、backtest.py,并全量重审。


🔴 跨文件最重要的共性问题

  1. 前视偏差(look-ahead bias)多处存在
    • factors.py:全样本 z-score 标准化(t 日用了未来数据)、全局分位数而非截面分位数
    • data_loader.py:财务数据用报告期 end_date 直接生效+ffill,未等公告日(ann_date
    • backtest.py:同 bar 执行——当日收盘价既生成信号又成交
  2. 幸存者偏差importer.py 复权因子/财务报表只按当前上市列表(L)导入,退市/暂停股数据缺失
  3. 财务数据未来函数风险importer.py 用最新公告覆盖历史公告,丢点-in-time 版本
  4. 回测正确性backtest.py 风控参数(max_position_pct/max_turnover/min_holding_period)读取了但完全没生效;期末清算重复记录净值导致指标异常
  5. 静默降级data_loader.py 所有 DB 异常静默回退随机模拟数据,且不同因子模拟数据完全相同

📋 各文件审核结论

alpha/config.py

审核: alpha/config.py

config.py 作为纯配置类,结构清晰、注释完整,且通过 field(default_factory=...) 正确避免了可变默认值的陷阱。但该文件存在“文档与实现不一致”以及环境变量类型转换、关键参数校验缺失等稳健性问题,这些问题会在下游数据获取、回测或输出阶段引发连锁报错。由于文件本身只含静态参数,暂未看到直接的回测未来函数或因子计算错误。

alpha/strategy.py

审核: alpha/strategy.py

该文件整体结构清晰,抽象了信号生成、权重分配与因子合成,接口设计较为规范。但在多因子合成、分位数信号、日期边界以及股票池过滤等关键环节存在真实且影响回测正确性的问题,部分问题可能引入未来函数或导致信号缺失,需要优先修复。

alpha/backtest.py

审核: alpha/backtest.py

该回测引擎结构清晰、模块职责分离明确,但在回测正确性和数据质量上存在关键缺陷:缺失价格的持仓估值、风控参数完全未生效、期末清算重复记录净值,以及同 bar 执行带来的前视偏差风险。当前代码若直接用于绩效评估,可能导致权益曲线失真和风险被低估,需要先修复上述问题。

alpha/evaluation.py

审核: alpha/evaluation.py

该模块结构清晰,把常用绩效指标拆分为独立方法,便于扩展和单独调用;但核心输入列(return / nav)的语义处理存在明显歧义,且年化、回撤、基准对齐等关键计算在异常数据或边界条件下不够健壮,可能在真实资金曲线上给出错误结果。由于这是纯绩效统计模块,未发现未来函数、信号生成或复权处理相关代码。

alpha/factors.py

审核: alpha/factors.py

该模块在因子注册、抽象基类和 IC 计算框架上有清晰的设计,但因子标准化、分层回测和换手率分析存在明显的前视偏差或逻辑错误,直接用于实盘回测会导致信号失真。建议在继续扩展因子库之前先修正核心的截面处理与时序对齐问题。

alpha/data_loader.py

审核: alpha/data_loader.py

该数据加载器接口清晰、字段白名单 + SQL 参数化能够有效防止注入,整体结构便于 Notebook 快速演示。但代码在“回测正确性”上存在硬伤:财务数据使用前视了报告期;所有数据库异常都静默回退到随机模拟数据;模拟数据又让不同因子完全相同。这些问题在不连接真实数据库时极难被发现,会严重污染研究结论。

quantitative_data/importer.py

审核: quantitative_data/importer.py

这是一个结构清晰、功能较完整的数据导入模块,批量化 UPSERT、按日期全市场拉取日线、断点续传等设计都值得肯定。但作为量化数据管道,存在几个方法论层面的严重问题:当前上市列表导致退市/暂停股的关键数据缺失(幸存者偏差)、财务报表保留最新公告覆盖历史公告(未来函数风险)、以及财务接口参数语义误用可能导致最新报告期缺失。此外还有一些数据类型、辅助函数和性能方面的中等/轻微问题。

quantitative_data/config.py

审核: quantitative_data/config.py

该文件是一份数据层配置,整体上将敏感信息交给环境变量/.env 管理,并做了启动期凭证非空校验,符合基本安全习惯。但作为一个会被多处 import 的 config.py,它在模块导入时直接抛出硬错误,且对日期、批次等关键参数缺少格式/范围校验,目前不涉及量化策略逻辑,因此 look-ahead bias、因子计算、回测执行等风险暂不适用。

亮点

  • SQL 白名单+参数化防注入、批量 UPSERT 去重、断点续传设计(importer/data_loader)
  • 因子注册表+抽象基类、IC 截面分组思路(factors)
  • A 股交易成本(佣金/印花税/滑点)双向往来处理(backtest)

完整逐条问题清单(🔴🟡🟢 分级)见评论。

# 代码审核报告(2026-08-08,kimi-k2.7-code 全量复审) 审核模型: kimi-k2.7-code 审核时间: 2026-08-08 07:45 审核范围: quanxiel 全部 8 个 Python 文件(8 个模块) > 关联:本次为 #1(2026-07-31 审核)的补充复审,覆盖当时超时/缺失的 strategy.py、backtest.py,并全量重审。 --- ## 🔴 跨文件最重要的共性问题 1. **前视偏差(look-ahead bias)多处存在**: - `factors.py`:全样本 z-score 标准化(t 日用了未来数据)、全局分位数而非截面分位数 - `data_loader.py`:财务数据用报告期 `end_date` 直接生效+ffill,未等公告日(`ann_date`) - `backtest.py`:同 bar 执行——当日收盘价既生成信号又成交 2. **幸存者偏差**:`importer.py` 复权因子/财务报表只按当前上市列表(L)导入,退市/暂停股数据缺失 3. **财务数据未来函数风险**:`importer.py` 用最新公告覆盖历史公告,丢点-in-time 版本 4. **回测正确性**:`backtest.py` 风控参数(max_position_pct/max_turnover/min_holding_period)读取了但完全没生效;期末清算重复记录净值导致指标异常 5. **静默降级**:`data_loader.py` 所有 DB 异常静默回退随机模拟数据,且不同因子模拟数据完全相同 ## 📋 各文件审核结论 ### alpha/config.py # 审核: alpha/config.py `config.py` 作为纯配置类,结构清晰、注释完整,且通过 `field(default_factory=...)` 正确避免了可变默认值的陷阱。但该文件存在“文档与实现不一致”以及环境变量类型转换、关键参数校验缺失等稳健性问题,这些问题会在下游数据获取、回测或输出阶段引发连锁报错。由于文件本身只含静态参数,暂未看到直接的回测未来函数或因子计算错误。 --- ### alpha/strategy.py # 审核: alpha/strategy.py 该文件整体结构清晰,抽象了信号生成、权重分配与因子合成,接口设计较为规范。但在多因子合成、分位数信号、日期边界以及股票池过滤等关键环节存在真实且影响回测正确性的问题,部分问题可能引入未来函数或导致信号缺失,需要优先修复。 ### alpha/backtest.py # 审核: alpha/backtest.py 该回测引擎结构清晰、模块职责分离明确,但在回测正确性和数据质量上存在关键缺陷:缺失价格的持仓估值、风控参数完全未生效、期末清算重复记录净值,以及同 bar 执行带来的前视偏差风险。当前代码若直接用于绩效评估,可能导致权益曲线失真和风险被低估,需要先修复上述问题。 ### alpha/evaluation.py # 审核: alpha/evaluation.py 该模块结构清晰,把常用绩效指标拆分为独立方法,便于扩展和单独调用;但核心输入列(`return` / `nav`)的语义处理存在明显歧义,且年化、回撤、基准对齐等关键计算在异常数据或边界条件下不够健壮,可能在真实资金曲线上给出错误结果。由于这是纯绩效统计模块,未发现未来函数、信号生成或复权处理相关代码。 ### alpha/factors.py # 审核: alpha/factors.py 该模块在因子注册、抽象基类和 IC 计算框架上有清晰的设计,但**因子标准化、分层回测和换手率分析存在明显的前视偏差或逻辑错误**,直接用于实盘回测会导致信号失真。建议在继续扩展因子库之前先修正核心的截面处理与时序对齐问题。 ### alpha/data_loader.py # 审核: alpha/data_loader.py 该数据加载器接口清晰、字段白名单 + SQL 参数化能够有效防止注入,整体结构便于 Notebook 快速演示。但代码在“回测正确性”上存在硬伤:财务数据使用前视了报告期;所有数据库异常都静默回退到随机模拟数据;模拟数据又让不同因子完全相同。这些问题在不连接真实数据库时极难被发现,会严重污染研究结论。 ### quantitative_data/importer.py # 审核: quantitative_data/importer.py 这是一个结构清晰、功能较完整的数据导入模块,批量化 UPSERT、按日期全市场拉取日线、断点续传等设计都值得肯定。但作为量化数据管道,存在几个**方法论层面的严重问题**:当前上市列表导致退市/暂停股的关键数据缺失(幸存者偏差)、财务报表保留最新公告覆盖历史公告(未来函数风险)、以及财务接口参数语义误用可能导致最新报告期缺失。此外还有一些数据类型、辅助函数和性能方面的中等/轻微问题。 --- ### quantitative_data/config.py # 审核: quantitative_data/config.py 该文件是一份数据层配置,整体上将敏感信息交给环境变量/`.env` 管理,并做了启动期凭证非空校验,符合基本安全习惯。但作为一个会被多处 import 的 `config.py`,它在模块导入时直接抛出硬错误,且对日期、批次等关键参数缺少格式/范围校验,目前不涉及量化策略逻辑,因此 look-ahead bias、因子计算、回测执行等风险暂不适用。 ## ✅ 亮点 - SQL 白名单+参数化防注入、批量 UPSERT 去重、断点续传设计(importer/data_loader) - 因子注册表+抽象基类、IC 截面分组思路(factors) - A 股交易成本(佣金/印花税/滑点)双向往来处理(backtest) --- 完整逐条问题清单(🔴🟡🟢 分级)见评论。
Author

完整问题清单(Part 1/2)

alpha/config.py

审查结论

config.py 作为纯配置类,结构清晰、注释完整,且通过 field(default_factory=...) 正确避免了可变默认值的陷阱。但该文件存在“文档与实现不一致”以及环境变量类型转换、关键参数校验缺失等稳健性问题,这些问题会在下游数据获取、回测或输出阶段引发连锁报错。由于文件本身只含静态参数,暂未看到直接的回测未来函数或因子计算错误。


问题清单

🔴严重

[🔴严重] AlphaConfig.db_port 字段定义(约第 24–26 行)- int(os.getenv("DB_PORT", "12345")) 缺少异常保护
DB_PORT 被设为空字符串、带空格或其他非数字值时,int() 会直接抛出 ValueError,导致配置实例化失败、程序启动崩溃。
修复建议:在 __post_init__ 中或读取时加 try/except,非法时回退到默认端口并打印明确警告。

🟡中等

[🟡中等] 文件头部 docstring(约第 1–13 行)- 说明支持 .env 文件,但代码未加载 .env
注释提示“可创建 .env 文件(参考 .env.example)”,但 config.py 中并未调用 load_dotenv() 或类似逻辑,仅读取系统环境变量;按说明创建 .env 后数据库密码等参数仍不会生效。
修复建议:在模块顶部或应用入口处增加 from dotenv import load_dotenv; load_dotenv(),并确保 python-dotenv 在依赖列表中。

[🟡中等] AlphaConfig.start_date / end_date 字段(约第 40–41 行)- 日期为字符串且无任何格式与顺序校验
若后续模块传入非法格式或 start_date > end_date,会在回测/数据获取阶段才暴露错误。
修复建议:在 __post_init__ 中用 datetime.strptime 校验格式,并断言 start_date <= end_date

[🟡中等] AlphaConfig.output_dir 字段(约第 58 行)- 输出目录仅作字符串配置,未校验或创建
默认 ./output 目录若不存在,后续保存 trade log 或结果时会抛出 FileNotFoundError
修复建议:在配置初始化或输出模块中执行 os.makedirs(self.output_dir, exist_ok=True)

[🟡中等] 交易成本与仓位约束参数(约第 44–51 行)- 缺少非负性与合理性校验
commission_rateslippagestamp_taxmax_position_pct 等若被误配为负数或大于 1,会直接污染回测结果。
修复建议:在 __post_init__ 中统一校验 0 <= rate <= 1 等约束,并给出明确报错。

🟢轻微

[🟢轻微] 导入 typing.Optional(第 16 行)- 已导入但代码中未使用
属于未使用导入,影响代码整洁度。
修复建议:删除 Optional 或保留供后续扩展。

[🟢轻微] 默认 end_date="2025-12-31"(约第 41 行)- 固定远期截止日期存在数据覆盖风险
若运行时刻行情数据尚未覆盖到 2025 年底,或该日期已超过当前日期且未做截断,可能导致空数据或未来数据问题。
修复建议:在回测入口根据实际数据可用性裁剪区间,或默认取最新交易日。


亮点

  1. 正确避免可变默认陷阱factor_windows 使用 field(default_factory=lambda: [5, 10, 20, 60]) 声明,符合 dataclass 最佳实践。
  2. 敏感信息通过环境变量管理:数据库密码不硬编码,默认空值,符合基本安全规范。
  3. 配置项分类与注释清晰:数据库、回测参数、交易成本、组合约束、因子研究、输出等分组明确,便于后续维护。

alpha/strategy.py

审查结论

该文件整体结构清晰,抽象了信号生成、权重分配与因子合成,接口设计较为规范。但在多因子合成、分位数信号、日期边界以及股票池过滤等关键环节存在真实且影响回测正确性的问题,部分问题可能引入未来函数或导致信号缺失,需要优先修复。

问题清单

🔴 严重

[🔴] Strategy.compute_composite_factor — NaN 污染导致合成因子失效
代码将每个因子标准化后直接 composite += w * z,由于 aligned 经过 reindex 后存在大量 NaN,任一因子在某个 (date, stock) 上缺失时,该位置的合成因子会变为 NaN,即使其他因子有有效值也会丢失。建议对每个因子标准化后 fillna(0) 再参与加权求和,或显式要求因子交集并在缺失时给出合理处理。

[🔴] Strategy.run_step — 日期缺失时使用 iloc[-1] 存在未来函数风险
date 不在 composite.index 时,代码回退到 composite.iloc[-1:],若当前日期早于最新因子日期,则会使用未来因子。建议改为 composite.loc[:date].iloc[-1:]composite.asof(date),确保只取当前日期及之前的最新截面。

[🔴] Strategyfilter_universe 股票池参数被定义但从未使用
filter_universe 在 dataclass 中声明,但 compute_composite_factor 与权重分配均未用它过滤列,传入的股票池约束会完全失效。建议在合成因子前用 filter_universe 限制 all_stocks,并在权重分配阶段再次校验。

🟡 中等

[🟡] QuantileSignal.generateduplicates='drop' 与固定分位标签不匹配
pd.qcut(..., duplicates='drop') 会在重复值较多时减少实际分档数,但代码仍固定判断 long_quantile-1 / short_quantile-1,可能出现某一侧(尤其是 top 档)永远没有信号。建议根据实际返回的最大/最小标签动态判断,或在初始化时校验分档可行性。

[🟡] QuantileSignal.generate — 未保证截面样本数不少于分档数
min_stocks 默认 10 仅保证最小股票数,未与 n_quantiles 比较;当 len(row) < n_quantiles 时会触发 ValueError 并被静默跳过,导致该日无信号。建议增加 len(row) >= self.n_quantiles 的前置校验或动态调整分位数。

[🟡] EqualWeightAllocator / FactorWeightAllocator — 忽略空头信号
SignalGenerator 文档明确定义 -1=做空,但两个权重分配器只处理 signal > 0 的多头,空头信号被完全丢弃。若策略为多空,需支持负权重;若为纯多头,应在信号生成器或文档中明确说明。

[🟡] EqualWeightAllocator.allocate — 截仓未按信号强度排序
long_stocks[:self.max_positions] 按列顺序截取,未按因子值或信号强度排序,可能保留弱信号、剔除强信号。建议按 latest_signal 或因子值降序后再取前 N。

[🟡] 权重分配器 — 完全未使用 pricescashpositions
EqualWeightAllocatorFactorWeightAllocator 均未结合股价、可用资金和当前持仓计算,目标权重与实际可买股数、整数股、换手成本、滑点等脱节。建议在 allocator 中加入基于价格和现金的目标市值计算,或明确说明此处仅输出名义权重、由下游模块处理成交。

[🟡] Strategy.compute_composite_factor — 小样本行标准化产生 NaN
aligned.std(axis=1) + 1e-12 无法修复“有效数据过少导致 std 为 NaN”的情况,该日期所有合成因子会变为 NaN。建议设置 std(..., min_periods=...) 或在小样本时跳过该截面。

[🟡] 信号生成器 — 未校验输入日期索引唯一性
factor_df 存在重复日期,factor_df.loc[date] 会返回 DataFrame,row.dropna() 行为异常,可能导致 pd.qcut 输入错误。建议进入计算前校验 index.is_unique,必要时 ~index.duplicated() 去重并排序。

🟢 轻微

[🟢] FactorWeightAllocator.allocatemin_weight 截断后重新归一化可能扭曲权重
min_weight * len(selected) > 1 或原始权重均为极小值时,clip 后再归一化会压缩为近似等权,无法反映因子差异。建议仅在低于阈值时做下限处理,并校验分母有效性。

[🟢] FactorWeightAllocator.allocate — 每次 fallback 都新建 EqualWeightAllocator 实例
return EqualWeightAllocator(self.max_positions).allocate(...) 会重复创建对象,建议预创建实例或使用类内静态方法。

[🟢] Strategy.compute_composite_factor — 未校验 factor_weights 之和
当用户传入非归一化权重时,合成因子的量级会任意变化,进而影响 Z-Score/分位数信号的触发。建议在使用前对权重做归一化或校验。

[🟢] Strategy.run_step — 每次调用都重新计算全区间 composite
在逐日回测中会重复进行标准化与矩阵对齐,性能开销较大。建议将 composite 预计算并缓存,run_step 仅切片使用。

[🟢] QuantileSignal.__init__ — 未校验分位参数合法性
long_quantileshort_quantile 可能超出 [1, n_quantiles] 范围,导致信号永远为空。建议在初始化时校验。

亮点

  • 接口抽象合理:通过 SignalGeneratorWeightAllocator 抽象基类解耦信号生成与权重分配,便于扩展不同策略。
  • 因子合成使用截面标准化compute_composite_factor 在合并前对每个因子做截面 Z-Score,有助于消除量纲差异,符合多因子常用做法。
  • 异常捕获避免崩溃QuantileSignal.generatepd.qcutValueError 做了 try/except,防止极端数据导致整个回测中断。

alpha/backtest.py

审查结论

该回测引擎结构清晰、模块职责分离明确,但在回测正确性和数据质量上存在关键缺陷:缺失价格的持仓估值、风控参数完全未生效、期末清算重复记录净值,以及同 bar 执行带来的前视偏差风险。当前代码若直接用于绩效评估,可能导致权益曲线失真和风险被低估,需要先修复上述问题。

问题清单

🔴 Broker.init / Broker.execute — 从配置读取了 max_position_pctmax_turnovermin_holding_period,但在 execute_buy_sell 中完全没有使用,风控约束形同虚设。
修复建议:在生成订单前校验/截断目标权重,并在买卖执行中加入个股仓位上限、单日换手上限和最短持有期检查。

🔴 BacktestEngine.run — 循环的最后一日已经调用 record_daily 记录净值,循环结束后又调用 _liquidate 并再次 record_daily(final_date),导致净值曲线出现重复索引,后续计算收益率、夏普等会异常。
修复建议:将期末清算合并到循环最后一天,或移除重复的最终 record_daily,确保每个交易日只有一条净值记录。

🔴 Position.market_value / Portfolio.update_market_prices — 当持仓股票某日在 current_prices 中缺失时,update_market_prices 不会更新其 current_price,而 market_value 会继续使用旧价格(隐式前向填充),退市/停牌股票会被持续按过期价格估值,导致权益虚高。
修复建议:在 record_daily 前对所有持仓用上一日有效价格显式前向填充,或对长期缺失价格的股票按退市/停牌规则处理。

🟡 BacktestEngine.run + Broker.execute — 策略信号生成与订单成交均使用 price_data.loc[date] 的同一日价格(默认收盘价),属于同 bar 执行,策略可用当日收盘价决定当日仓位,存在前视偏差。
修复建议:使用上一日收盘价生成信号,并以次日开盘价或带滑点的次日收盘价执行成交。

🟡 Broker.executetarget_weights 未做归一化和有效性校验;权重总和大于 1、为负或为空时,会导致现金不足、买入被截断、实际仓位偏离目标。
修复建议:在 execute 开头校验 target_weights,必要时归一化、过滤负值并输出警告。

🟡 BacktestEngine.runall_dates = sorted(price_data.index) 未去重,若输入数据存在重复交易日,会导致单日多次调仓、多次记录净值。
修复建议:使用 price_data.index.unique().sort_values(),或在发现重复日期时报错/去重。

🟡 BacktestEngine.runfactor_dataprice_data 的日期、股票池未做任何对齐检查,缺失或错位的因子可能被策略错误使用。
修复建议:回测前将各因子 DataFrame 按 price_dataindex/columns 进行 reindex 并对齐,检查缺失率。

🟡 Broker.execute / _buy — 现金不足时仅对单笔买入订单截断数量,未考虑多只股票顺序和后续订单对权重的影响,排在前面的股票可能占满现金,导致最终组合偏离目标。
修复建议:调仓前根据目标权重和预估交易成本统一计算各股票可成交数量,或按目标权重优先级排序并预留后续订单现金。

🟢 BacktestEngine.runrebalance_freq 只显式判断 'M'/'W',其他字符串(如 'Q')会静默按日调仓处理。
修复建议:增加对 'D' 的显式判断,并对未识别的频率抛出 ValueError

🟢 BacktestEngine.run — 期末清算时若 final_date 不在 price_data.index 中,final_prices 为空,持仓无法平仓但又会重复记录一次净值,结果不一致。
修复建议:仅在 final_date 存在有效价格时执行最终清算,否则使用上一个有效交易日的价格。

🟢 Portfolio.get_position / Broker.executeget_position 会在未实际成交时创建 quantity=0Position 对象,长期运行会累积大量空持仓。
修复建议:在 diff_qty == 0 时不调用 get_position,或定期清理 quantity == 0 的持仓。

🟢 BacktestEngine.runprogress_callbackstrategy.run_step 的调用没有异常处理,一个合约/策略异常会导致整个回测中断。
修复建议:增加 try/except 包裹策略调用与回调,记录日志并可选择跳过异常日期。

亮点

  • 使用 dataclass 定义 TradeRecordPosition,交易记录和持仓结构清晰;佣金、印花税、滑点在买卖两侧分别实现,符合 A 股交易成本习惯。
  • PortfolioBacktestEngine 职责分离,并提供了 progress_callback 接口,便于扩展和集成。
  • 在买入时考虑了整手(100 股)和现金不足时的数量调整,体现了对 A 股最小交易单位的基本处理。

alpha/evaluation.py

审查结论

该模块结构清晰,把常用绩效指标拆分为独立方法,便于扩展和单独调用;但核心输入列(return / nav)的语义处理存在明显歧义,且年化、回撤、基准对齐等关键计算在异常数据或边界条件下不够健壮,可能在真实资金曲线上给出错误结果。由于这是纯绩效统计模块,未发现未来函数、信号生成或复权处理相关代码。

问题清单

🔴 严重

[🔴] PerformanceEvaluator.__init__daily_return 生成逻辑)
当输入列名为 return 时,代码直接使用 self.equity["return"].diff()。这隐含假设该列是“累计收益率”或从 0 开始的价格序列;若用户传入的 return 已经是日收益率,则所有指标都会基于“收益率的差分”计算,结果完全错误。
→ 修复建议:明确约定输入语义,或分别支持 daily_return / cum_return / nav,避免对同名列做歧义转换。

[🔴] PerformanceEvaluator.total_returnnav 分支)
直接返回 nav.iloc[-1] - 1.0,默认初始 NAV 为 1.0;若传入的是绝对资金曲线(如起始 1,000,000),累计收益会被严重算错。
→ 修复建议:统一使用 nav.iloc[-1] / nav.iloc[0] - 1.0,不依赖初始值。

🟡 中等

[🟡] PerformanceEvaluator.annual_return(负收益边界)
total_return < -1years 为分数时,(1 + total) ** (1 / years) 会产生 nan 并伴随 RuntimeWarning,未做保护。
→ 修复建议:在计算前判断 1 + total <= 0,返回 np.nan 或改用对数年化。

[🟡] PerformanceEvaluator._year_frac(年化分母)
使用 n / periods_per_year 代替实际日历时长,未处理缺失交易日、非日频数据等情况,会导致年化收益/波动率被错误缩放。
→ 修复建议:基于索引的实际日期跨度计算年化因子,例如 (last - first).days / 365.25,或在输入前补齐交易日。

[🟡] PerformanceEvaluator.sortino_ratio(下行偏差定义)
仅取 returns < 0 的子样本来计算标准差,不符合 Sortino 比率通常使用的目标收益率/MAR(至少应考虑无风险利率),且仅使用亏损样本能低估/偏离真实下行偏差。
→ 修复建议:以 returns - target 为基准,保留全部观测,计算 sqrt(mean(min(0, returns - target)^2))

[🟡] PerformanceEvaluator.alpha / beta / information_ratio(样本不足与对齐)
_align_benchmark 使用 inner join + dropna,策略与基准日期不重合时会静默丢弃数据;当对齐后样本量小于 2 时,np.cov 会返回 NaN 并可能触发警告,后续仍参与计算。
→ 修复建议:增加 len(aligned) < 2 校验并返回 np.nan,同时可考虑使用 reindex 对齐并显式处理缺失。

[🟡] PerformanceEvaluator.profit_loss_ratio(空子集除零)
若序列中没有盈利日或亏损日,avg_winavg_loss 会为 NaN 或 0,导致返回 NaN 或异常大值。
→ 修复建议:在子集为空时返回 np.nan,并对 avg_loss == 0 单独处理。

[🟡] PerformanceEvaluator.__init__ 及多个方法(索引未校验)
未检查 equity_curve.index 是否唯一、是否按时间排序、是否为日期类型。重复索引会导致 pct_change / diff / cumprod 产生错误行;非日期索引会让 _year_frac 等单位换算无意义。
→ 修复建议:初始化时校验 index.is_uniqueindex.is_monotonic_increasingis_datetime64

🟢 轻微

[🟢] ReportGenerator.to_markdown(整数指标格式化)
对“最长回撤天数”等整数指标使用 {value:.4f},输出形如 15.0000,可读性不佳。
→ 修复建议:根据数值类型选择 d.4f 格式化。

[🟢] ReportGenerator.to_markdown(交易对象未防御)
直接访问 t.datet.stock 等属性,未校验 trades 元素结构,传入不兼容对象会抛出 AttributeError
→ 修复建议:使用 hasattr 检查或使用 dataclass/NamedTuple 约束交易对象结构。

[🟢] PerformanceEvaluator.full_reportperiods_per_year 被忽略)
full_report 未暴露 periods_per_year 参数;若数据为周/月频,必须逐个调用方法,无法一次性生成正确频率的报告。
→ 修复建议:为 full_report 增加 periods_per_year 参数并透传给各指标方法。

亮点

  1. 指标封装清晰:将夏普、Calmar、Sortino、Alpha/Beta、信息比率等常见指标拆分为独立方法,便于按需调用与后续扩展。
  2. 基准对齐保证样本一致:使用 inner join + dropna 对齐策略与基准,避免了因交易日不一致导致回归/相对指标出现偏差。
  3. 分母保护到位:多处使用 + 1e-12 防止除零崩溃,提升了代码在常数收益序列上的稳定性。
## 完整问题清单(Part 1/2) ### alpha/config.py ## 审查结论 `config.py` 作为纯配置类,结构清晰、注释完整,且通过 `field(default_factory=...)` 正确避免了可变默认值的陷阱。但该文件存在“文档与实现不一致”以及环境变量类型转换、关键参数校验缺失等稳健性问题,这些问题会在下游数据获取、回测或输出阶段引发连锁报错。由于文件本身只含静态参数,暂未看到直接的回测未来函数或因子计算错误。 --- ## 问题清单 🔴严重 **[🔴严重] `AlphaConfig.db_port` 字段定义(约第 24–26 行)- `int(os.getenv("DB_PORT", "12345"))` 缺少异常保护** 当 `DB_PORT` 被设为空字符串、带空格或其他非数字值时,`int()` 会直接抛出 `ValueError`,导致配置实例化失败、程序启动崩溃。 修复建议:在 `__post_init__` 中或读取时加 `try/except`,非法时回退到默认端口并打印明确警告。 🟡中等 **[🟡中等] 文件头部 docstring(约第 1–13 行)- 说明支持 `.env` 文件,但代码未加载 `.env`** 注释提示“可创建 `.env` 文件(参考 `.env.example`)”,但 `config.py` 中并未调用 `load_dotenv()` 或类似逻辑,仅读取系统环境变量;按说明创建 `.env` 后数据库密码等参数仍不会生效。 修复建议:在模块顶部或应用入口处增加 `from dotenv import load_dotenv; load_dotenv()`,并确保 `python-dotenv` 在依赖列表中。 **[🟡中等] `AlphaConfig.start_date` / `end_date` 字段(约第 40–41 行)- 日期为字符串且无任何格式与顺序校验** 若后续模块传入非法格式或 `start_date > end_date`,会在回测/数据获取阶段才暴露错误。 修复建议:在 `__post_init__` 中用 `datetime.strptime` 校验格式,并断言 `start_date <= end_date`。 **[🟡中等] `AlphaConfig.output_dir` 字段(约第 58 行)- 输出目录仅作字符串配置,未校验或创建** 默认 `./output` 目录若不存在,后续保存 trade log 或结果时会抛出 `FileNotFoundError`。 修复建议:在配置初始化或输出模块中执行 `os.makedirs(self.output_dir, exist_ok=True)`。 **[🟡中等] 交易成本与仓位约束参数(约第 44–51 行)- 缺少非负性与合理性校验** `commission_rate`、`slippage`、`stamp_tax`、`max_position_pct` 等若被误配为负数或大于 1,会直接污染回测结果。 修复建议:在 `__post_init__` 中统一校验 `0 <= rate <= 1` 等约束,并给出明确报错。 🟢轻微 **[🟢轻微] 导入 `typing.Optional`(第 16 行)- 已导入但代码中未使用** 属于未使用导入,影响代码整洁度。 修复建议:删除 `Optional` 或保留供后续扩展。 **[🟢轻微] 默认 `end_date="2025-12-31"`(约第 41 行)- 固定远期截止日期存在数据覆盖风险** 若运行时刻行情数据尚未覆盖到 2025 年底,或该日期已超过当前日期且未做截断,可能导致空数据或未来数据问题。 修复建议:在回测入口根据实际数据可用性裁剪区间,或默认取最新交易日。 --- ## 亮点 1. **正确避免可变默认陷阱**:`factor_windows` 使用 `field(default_factory=lambda: [5, 10, 20, 60])` 声明,符合 dataclass 最佳实践。 2. **敏感信息通过环境变量管理**:数据库密码不硬编码,默认空值,符合基本安全规范。 3. **配置项分类与注释清晰**:数据库、回测参数、交易成本、组合约束、因子研究、输出等分组明确,便于后续维护。 --- ### alpha/strategy.py ## 审查结论 该文件整体结构清晰,抽象了信号生成、权重分配与因子合成,接口设计较为规范。但在多因子合成、分位数信号、日期边界以及股票池过滤等关键环节存在真实且影响回测正确性的问题,部分问题可能引入未来函数或导致信号缺失,需要优先修复。 ## 问题清单 ### 🔴 严重 **[🔴] `Strategy.compute_composite_factor` — NaN 污染导致合成因子失效** 代码将每个因子标准化后直接 `composite += w * z`,由于 `aligned` 经过 `reindex` 后存在大量 NaN,任一因子在某个 `(date, stock)` 上缺失时,该位置的合成因子会变为 NaN,即使其他因子有有效值也会丢失。建议对每个因子标准化后 `fillna(0)` 再参与加权求和,或显式要求因子交集并在缺失时给出合理处理。 **[🔴] `Strategy.run_step` — 日期缺失时使用 `iloc[-1]` 存在未来函数风险** 当 `date` 不在 `composite.index` 时,代码回退到 `composite.iloc[-1:]`,若当前日期早于最新因子日期,则会使用未来因子。建议改为 `composite.loc[:date].iloc[-1:]` 或 `composite.asof(date)`,确保只取当前日期及之前的最新截面。 **[🔴] `Strategy` — `filter_universe` 股票池参数被定义但从未使用** `filter_universe` 在 dataclass 中声明,但 `compute_composite_factor` 与权重分配均未用它过滤列,传入的股票池约束会完全失效。建议在合成因子前用 `filter_universe` 限制 `all_stocks`,并在权重分配阶段再次校验。 ### 🟡 中等 **[🟡] `QuantileSignal.generate` — `duplicates='drop'` 与固定分位标签不匹配** `pd.qcut(..., duplicates='drop')` 会在重复值较多时减少实际分档数,但代码仍固定判断 `long_quantile-1` / `short_quantile-1`,可能出现某一侧(尤其是 top 档)永远没有信号。建议根据实际返回的最大/最小标签动态判断,或在初始化时校验分档可行性。 **[🟡] `QuantileSignal.generate` — 未保证截面样本数不少于分档数** `min_stocks` 默认 10 仅保证最小股票数,未与 `n_quantiles` 比较;当 `len(row) < n_quantiles` 时会触发 `ValueError` 并被静默跳过,导致该日无信号。建议增加 `len(row) >= self.n_quantiles` 的前置校验或动态调整分位数。 **[🟡] `EqualWeightAllocator` / `FactorWeightAllocator` — 忽略空头信号** `SignalGenerator` 文档明确定义 `-1=做空`,但两个权重分配器只处理 `signal > 0` 的多头,空头信号被完全丢弃。若策略为多空,需支持负权重;若为纯多头,应在信号生成器或文档中明确说明。 **[🟡] `EqualWeightAllocator.allocate` — 截仓未按信号强度排序** `long_stocks[:self.max_positions]` 按列顺序截取,未按因子值或信号强度排序,可能保留弱信号、剔除强信号。建议按 `latest_signal` 或因子值降序后再取前 N。 **[🟡] 权重分配器 — 完全未使用 `prices`、`cash`、`positions`** `EqualWeightAllocator` 与 `FactorWeightAllocator` 均未结合股价、可用资金和当前持仓计算,目标权重与实际可买股数、整数股、换手成本、滑点等脱节。建议在 allocator 中加入基于价格和现金的目标市值计算,或明确说明此处仅输出名义权重、由下游模块处理成交。 **[🟡] `Strategy.compute_composite_factor` — 小样本行标准化产生 NaN** `aligned.std(axis=1) + 1e-12` 无法修复“有效数据过少导致 std 为 NaN”的情况,该日期所有合成因子会变为 NaN。建议设置 `std(..., min_periods=...)` 或在小样本时跳过该截面。 **[🟡] 信号生成器 — 未校验输入日期索引唯一性** 若 `factor_df` 存在重复日期,`factor_df.loc[date]` 会返回 DataFrame,`row.dropna()` 行为异常,可能导致 `pd.qcut` 输入错误。建议进入计算前校验 `index.is_unique`,必要时 `~index.duplicated()` 去重并排序。 ### 🟢 轻微 **[🟢] `FactorWeightAllocator.allocate` — `min_weight` 截断后重新归一化可能扭曲权重** 当 `min_weight * len(selected) > 1` 或原始权重均为极小值时,clip 后再归一化会压缩为近似等权,无法反映因子差异。建议仅在低于阈值时做下限处理,并校验分母有效性。 **[🟢] `FactorWeightAllocator.allocate` — 每次 fallback 都新建 `EqualWeightAllocator` 实例** `return EqualWeightAllocator(self.max_positions).allocate(...)` 会重复创建对象,建议预创建实例或使用类内静态方法。 **[🟢] `Strategy.compute_composite_factor` — 未校验 `factor_weights` 之和** 当用户传入非归一化权重时,合成因子的量级会任意变化,进而影响 Z-Score/分位数信号的触发。建议在使用前对权重做归一化或校验。 **[🟢] `Strategy.run_step` — 每次调用都重新计算全区间 composite** 在逐日回测中会重复进行标准化与矩阵对齐,性能开销较大。建议将 composite 预计算并缓存,run_step 仅切片使用。 **[🟢] `QuantileSignal.__init__` — 未校验分位参数合法性** `long_quantile`、`short_quantile` 可能超出 `[1, n_quantiles]` 范围,导致信号永远为空。建议在初始化时校验。 ## 亮点 - **接口抽象合理**:通过 `SignalGenerator`、`WeightAllocator` 抽象基类解耦信号生成与权重分配,便于扩展不同策略。 - **因子合成使用截面标准化**:`compute_composite_factor` 在合并前对每个因子做截面 Z-Score,有助于消除量纲差异,符合多因子常用做法。 - **异常捕获避免崩溃**:`QuantileSignal.generate` 对 `pd.qcut` 的 `ValueError` 做了 try/except,防止极端数据导致整个回测中断。 --- ### alpha/backtest.py ## 审查结论 该回测引擎结构清晰、模块职责分离明确,但在回测正确性和数据质量上存在关键缺陷:缺失价格的持仓估值、风控参数完全未生效、期末清算重复记录净值,以及同 bar 执行带来的前视偏差风险。当前代码若直接用于绩效评估,可能导致权益曲线失真和风险被低估,需要先修复上述问题。 ## 问题清单 🔴 **Broker.__init__ / Broker.execute** — 从配置读取了 `max_position_pct`、`max_turnover`、`min_holding_period`,但在 `execute`、`_buy`、`_sell` 中完全没有使用,风控约束形同虚设。 修复建议:在生成订单前校验/截断目标权重,并在买卖执行中加入个股仓位上限、单日换手上限和最短持有期检查。 🔴 **BacktestEngine.run** — 循环的最后一日已经调用 `record_daily` 记录净值,循环结束后又调用 `_liquidate` 并再次 `record_daily(final_date)`,导致净值曲线出现重复索引,后续计算收益率、夏普等会异常。 修复建议:将期末清算合并到循环最后一天,或移除重复的最终 `record_daily`,确保每个交易日只有一条净值记录。 🔴 **Position.market_value / Portfolio.update_market_prices** — 当持仓股票某日在 `current_prices` 中缺失时,`update_market_prices` 不会更新其 `current_price`,而 `market_value` 会继续使用旧价格(隐式前向填充),退市/停牌股票会被持续按过期价格估值,导致权益虚高。 修复建议:在 `record_daily` 前对所有持仓用上一日有效价格显式前向填充,或对长期缺失价格的股票按退市/停牌规则处理。 🟡 **BacktestEngine.run + Broker.execute** — 策略信号生成与订单成交均使用 `price_data.loc[date]` 的同一日价格(默认收盘价),属于同 bar 执行,策略可用当日收盘价决定当日仓位,存在前视偏差。 修复建议:使用上一日收盘价生成信号,并以次日开盘价或带滑点的次日收盘价执行成交。 🟡 **Broker.execute** — `target_weights` 未做归一化和有效性校验;权重总和大于 1、为负或为空时,会导致现金不足、买入被截断、实际仓位偏离目标。 修复建议:在 `execute` 开头校验 `target_weights`,必要时归一化、过滤负值并输出警告。 🟡 **BacktestEngine.run** — `all_dates = sorted(price_data.index)` 未去重,若输入数据存在重复交易日,会导致单日多次调仓、多次记录净值。 修复建议:使用 `price_data.index.unique().sort_values()`,或在发现重复日期时报错/去重。 🟡 **BacktestEngine.run** — `factor_data` 与 `price_data` 的日期、股票池未做任何对齐检查,缺失或错位的因子可能被策略错误使用。 修复建议:回测前将各因子 DataFrame 按 `price_data` 的 `index/columns` 进行 `reindex` 并对齐,检查缺失率。 🟡 **Broker.execute / _buy** — 现金不足时仅对单笔买入订单截断数量,未考虑多只股票顺序和后续订单对权重的影响,排在前面的股票可能占满现金,导致最终组合偏离目标。 修复建议:调仓前根据目标权重和预估交易成本统一计算各股票可成交数量,或按目标权重优先级排序并预留后续订单现金。 🟢 **BacktestEngine.run** — `rebalance_freq` 只显式判断 `'M'`/`'W'`,其他字符串(如 `'Q'`)会静默按日调仓处理。 修复建议:增加对 `'D'` 的显式判断,并对未识别的频率抛出 `ValueError`。 🟢 **BacktestEngine.run** — 期末清算时若 `final_date` 不在 `price_data.index` 中,`final_prices` 为空,持仓无法平仓但又会重复记录一次净值,结果不一致。 修复建议:仅在 `final_date` 存在有效价格时执行最终清算,否则使用上一个有效交易日的价格。 🟢 **Portfolio.get_position / Broker.execute** — `get_position` 会在未实际成交时创建 `quantity=0` 的 `Position` 对象,长期运行会累积大量空持仓。 修复建议:在 `diff_qty == 0` 时不调用 `get_position`,或定期清理 `quantity == 0` 的持仓。 🟢 **BacktestEngine.run** — `progress_callback` 和 `strategy.run_step` 的调用没有异常处理,一个合约/策略异常会导致整个回测中断。 修复建议:增加 `try/except` 包裹策略调用与回调,记录日志并可选择跳过异常日期。 ## 亮点 - 使用 `dataclass` 定义 `TradeRecord` 与 `Position`,交易记录和持仓结构清晰;佣金、印花税、滑点在买卖两侧分别实现,符合 A 股交易成本习惯。 - `Portfolio` 与 `BacktestEngine` 职责分离,并提供了 `progress_callback` 接口,便于扩展和集成。 - 在买入时考虑了整手(100 股)和现金不足时的数量调整,体现了对 A 股最小交易单位的基本处理。 --- ### alpha/evaluation.py ## 审查结论 该模块结构清晰,把常用绩效指标拆分为独立方法,便于扩展和单独调用;但核心输入列(`return` / `nav`)的语义处理存在明显歧义,且年化、回撤、基准对齐等关键计算在异常数据或边界条件下不够健壮,可能在真实资金曲线上给出错误结果。由于这是纯绩效统计模块,未发现未来函数、信号生成或复权处理相关代码。 ## 问题清单 ### 🔴 严重 **[🔴] `PerformanceEvaluator.__init__`(`daily_return` 生成逻辑)** 当输入列名为 `return` 时,代码直接使用 `self.equity["return"].diff()`。这隐含假设该列是“累计收益率”或从 0 开始的价格序列;若用户传入的 `return` 已经是日收益率,则所有指标都会基于“收益率的差分”计算,结果完全错误。 → 修复建议:明确约定输入语义,或分别支持 `daily_return` / `cum_return` / `nav`,避免对同名列做歧义转换。 **[🔴] `PerformanceEvaluator.total_return`(`nav` 分支)** 直接返回 `nav.iloc[-1] - 1.0`,默认初始 NAV 为 1.0;若传入的是绝对资金曲线(如起始 1,000,000),累计收益会被严重算错。 → 修复建议:统一使用 `nav.iloc[-1] / nav.iloc[0] - 1.0`,不依赖初始值。 ### 🟡 中等 **[🟡] `PerformanceEvaluator.annual_return`(负收益边界)** 当 `total_return < -1` 且 `years` 为分数时,`(1 + total) ** (1 / years)` 会产生 `nan` 并伴随 RuntimeWarning,未做保护。 → 修复建议:在计算前判断 `1 + total <= 0`,返回 `np.nan` 或改用对数年化。 **[🟡] `PerformanceEvaluator._year_frac`(年化分母)** 使用 `n / periods_per_year` 代替实际日历时长,未处理缺失交易日、非日频数据等情况,会导致年化收益/波动率被错误缩放。 → 修复建议:基于索引的实际日期跨度计算年化因子,例如 `(last - first).days / 365.25`,或在输入前补齐交易日。 **[🟡] `PerformanceEvaluator.sortino_ratio`(下行偏差定义)** 仅取 `returns < 0` 的子样本来计算标准差,不符合 Sortino 比率通常使用的目标收益率/MAR(至少应考虑无风险利率),且仅使用亏损样本能低估/偏离真实下行偏差。 → 修复建议:以 `returns - target` 为基准,保留全部观测,计算 `sqrt(mean(min(0, returns - target)^2))`。 **[🟡] `PerformanceEvaluator.alpha` / `beta` / `information_ratio`(样本不足与对齐)** `_align_benchmark` 使用 `inner` join + `dropna`,策略与基准日期不重合时会静默丢弃数据;当对齐后样本量小于 2 时,`np.cov` 会返回 `NaN` 并可能触发警告,后续仍参与计算。 → 修复建议:增加 `len(aligned) < 2` 校验并返回 `np.nan`,同时可考虑使用 `reindex` 对齐并显式处理缺失。 **[🟡] `PerformanceEvaluator.profit_loss_ratio`(空子集除零)** 若序列中没有盈利日或亏损日,`avg_win` 或 `avg_loss` 会为 `NaN` 或 0,导致返回 `NaN` 或异常大值。 → 修复建议:在子集为空时返回 `np.nan`,并对 `avg_loss == 0` 单独处理。 **[🟡] `PerformanceEvaluator.__init__` 及多个方法(索引未校验)** 未检查 `equity_curve.index` 是否唯一、是否按时间排序、是否为日期类型。重复索引会导致 `pct_change` / `diff` / `cumprod` 产生错误行;非日期索引会让 `_year_frac` 等单位换算无意义。 → 修复建议:初始化时校验 `index.is_unique`、`index.is_monotonic_increasing` 及 `is_datetime64`。 ### 🟢 轻微 **[🟢] `ReportGenerator.to_markdown`(整数指标格式化)** 对“最长回撤天数”等整数指标使用 `{value:.4f}`,输出形如 `15.0000`,可读性不佳。 → 修复建议:根据数值类型选择 `d` 或 `.4f` 格式化。 **[🟢] `ReportGenerator.to_markdown`(交易对象未防御)** 直接访问 `t.date`、`t.stock` 等属性,未校验 `trades` 元素结构,传入不兼容对象会抛出 `AttributeError`。 → 修复建议:使用 `hasattr` 检查或使用 dataclass/NamedTuple 约束交易对象结构。 **[🟢] `PerformanceEvaluator.full_report`(`periods_per_year` 被忽略)** `full_report` 未暴露 `periods_per_year` 参数;若数据为周/月频,必须逐个调用方法,无法一次性生成正确频率的报告。 → 修复建议:为 `full_report` 增加 `periods_per_year` 参数并透传给各指标方法。 ## 亮点 1. **指标封装清晰**:将夏普、Calmar、Sortino、Alpha/Beta、信息比率等常见指标拆分为独立方法,便于按需调用与后续扩展。 2. **基准对齐保证样本一致**:使用 `inner join` + `dropna` 对齐策略与基准,避免了因交易日不一致导致回归/相对指标出现偏差。 3. **分母保护到位**:多处使用 `+ 1e-12` 防止除零崩溃,提升了代码在常数收益序列上的稳定性。
Author

完整问题清单(Part 2/2)

alpha/factors.py

审查结论

该模块在因子注册、抽象基类和 IC 计算框架上有清晰的设计,但因子标准化、分层回测和换手率分析存在明显的前视偏差或逻辑错误,直接用于实盘回测会导致信号失真。建议在继续扩展因子库之前先修正核心的截面处理与时序对齐问题。

问题清单

🔴 严重

🔴 严重 TechnicalFactor.compute(约第118–126行)- 全样本标准化导致前视偏差
mom_z / vol_z 使用整个序列的 .mean() / .std() 做标准化,意味着在 t 日使用了未来数据计算均值和标准差;对多资产面板而言还混入了未来截面信息。
修复建议:按截面(或至少按扩展窗口/滚动窗口)逐期标准化,例如 factor.groupby(level=0).apply(lambda s: (s - s.mean()) / s.std())

🔴 严重 FundamentalFactor.compute(约第140–161行)- 同样存在全样本标准化前视偏差
ROE、PE、PB、营收增速均在全样本上做 z-score,且未按行业/日期做中性化处理,面板下会用到未来和跨期信息。
修复建议:按财报披露截止日或交易日期做截面标准化,并处理缺失值(如仅对非缺失值做标准化后再合并)。

🔴 严重 FactorAnalyzer.quantile_returns(约第232–239行)- 全局分位数而非截面分位数,且累计收益计算错误
pd.qcut 直接对所有样本做全局分位,未按日期截面分组;cum_return = avg_return.cumsum() 是在“组号 1→5”上累加,失去了时间维度,不是组合累计收益。
修复建议:按 level=0 分组后对每个截面 qcut,再计算每组时序平均收益和累计收益。

🔴 严重 FactorAnalyzer.turnover(约第241–255行)- 换手率逻辑未实现且计算对象错误
MultiIndex 分支下仅写了 pass 未返回结果;非 MultiIndex 分支用 self.factor.autocorr(lag=1) 对整个面板做自相关,混淆了不同资产和日期,不能代表“相邻期分位数变化比例”。
修复建议:按日期截面计算持仓权重或分位组,再计算相邻期权的换手比例序列。

🟡 中等

🟡 中等 FactorAnalyzer.ic_decay(约第220–230行)- 池化计算 IC 衰减,未按期聚合
直接用 stats.spearmanr 对所有截面的因子值和未来收益做整体相关,得到的是池化相关系数,而不是“每期 IC 的均值”,会混入时序趋势。
修复建议:按 level=0 逐期计算 Spearman/Pearson IC,再对各期 IC 取平均。

🟡 中等 FactorAnalyzer.compute_ic(约第179–205行)- 未显式对齐因子与收益,缺失日期会抛 KeyError
fwd.get_group(g.name)forward_returns 缺少某个日期时会直接抛错;g.corr 对内部股票做内连接,也可能在截面不完整时静默忽略。
修复建议:先 self.factor.align(self.forward, join='inner'),或在 apply 中用 try/except 跳过缺失截面。

🟡 中等 BaseFactor.neutralize(约第69–96行)- 对空数据、共线行业和异常市值缺乏保护
market_cap <= 0clip(lower=1) 当成市值 1 处理,会扭曲回归;group 含 NaN 时 get_dummies 会将其所有行业 dummy 置 0;若 common_idx 为空或 X 列完全共线,lstsq 结果不稳定。
修复建议:过滤市值<=0 和分组为 NaN 的样本,并在回归前检查 X 的秩或改用 np.linalg.lstsq 的残差逻辑加异常处理。

🟡 中等 TechnicalFactor.compute / FundamentalFactor.compute - 未防御标准差为零
当因子序列恒定(如所有 PE 相同)或仅含 NaN 时,series.std() 为 0/NaN,导致 inf 或全 NaN,可能进一步触发 pd.qcut 报错。
修复建议:标准化前判断 std > 0,否则返回 NaN 或跳过该因子。

🟡 中等 FactorAnalyzer.quantile_returns / turnover(约第235、244行)- pd.qcut 对重复边界会抛异常
当因子存在大量相同值时,qcut 默认 duplicates='raise' 会抛出 ValueError
修复建议:使用 pd.qcut(..., duplicates='drop') 或改用 pd.cut

🟢 轻微

🟢 轻微 TechnicalFactor.compute(约第123–125行)- 动量/波动率窗口不一致导致合成结果含大量 NaN
momvol 的 NaN 长度不同,0.5 * mom_z + 0.5 * vol_z 在任一者为 NaN 时整体为 NaN,损失了更多可用信号日。
修复建议:统一窗口长度或采用 fillna / 加权时只使用两者均非 NaN 的截面。

🟢 轻微 BaseFactor.neutralize(约第83行)- 市值非正数未剔除而是硬编码为 1
np.log(market_cap.clip(lower=1)) 把 0/负市值变成 log(1)=0,相当于给这些样本赋予了错误市值。
修复建议:先 market_cap = market_cap[market_cap > 0] 再取 log,保证样本质量。

🟢 轻微 FactorAnalyzer.turnover(约第252行)- 依赖 self.ic_series 决定是否计算自相关系数
换手率计算与 IC 序列无必然联系;若用户未先调用 compute_ic,此处会返回空 Series,行为不稳定。
修复建议:移除对 ic_series 的状态依赖,直接基于因子计算换手率。

亮点

  1. 注册表 + 抽象基类设计清晰FactorRegistryBaseFactor 便于后续扩展新因子,并统一了 compute 接口。
  2. IC 计算采用截面分组思路compute_ic 对 MultiIndex 按日期分组计算 Spearman/Pearson,符合截面因子研究范式。
  3. 中性化实现方向正确:通过行业虚拟变量 + 市值做 OLS 取残差,是行业中性的常规做法。

alpha/data_loader.py

审查结论

该数据加载器接口清晰、字段白名单 + SQL 参数化能够有效防止注入,整体结构便于 Notebook 快速演示。但代码在“回测正确性”上存在硬伤:财务数据使用前视了报告期;所有数据库异常都静默回退到随机模拟数据;模拟数据又让不同因子完全相同。这些问题在不连接真实数据库时极难被发现,会严重污染研究结论。

问题清单

按严重程度排列:

🔴 _load_fina_field 函数 — 以财报 end_date 作为数据生效日期并直接 ffill(),相当于在报告期末当天就知道该期财务结果,而实际财报通常在报告期结束后数周才披露,构成典型的未来函数/前视偏差。
修复建议:使用 ann_date(公告日)或报告期后的首个交易日作为生效起点,再向前填充。

🔴 load_prices / load_factor / load_benchmark 函数 — 使用裸 except Exception 捕获所有异常后静默回退到模拟数据,SQL 语法错误、网络超时、表不存在等情况都会让调用方在不知情的情况下拿到随机数据。
修复建议:增加 strict 模式(默认开启),数据库异常时直接抛出;仅在网络不可用时明确回退,并在返回数据中标注 “SIMULATED”。

🔴 _simulate_factor 函数 — 每次调用都重置 np.random.seed(self.seed) 且未按因子名区分随机状态,导致不同因子的模拟矩阵完全相同;同时与 _simulate_prices 共享同一 seed,可能人为制造出因子与收益的相关性。
修复建议:为每个因子生成独立随机状态,例如 rng = np.random.default_rng(hash(name) ^ self.seed)

🟡 load_factor 函数 — 对于不在 DAILY_BASIC_FIELDSFINA_FIELDS 中的 name 也直接返回模拟数据,不会报错,容易把拼写错误当成有效因子。
修复建议:在入口处校验 name,未知因子抛出 ValueError

🟡 _load_prices_db 函数dropna(how='all', axis=1) 会静默剔除无数据的股票列,且不强制对齐到完整股票池/交易日,可能引入幸存者偏差或导致下游截面运算维度不一致。
修复建议:保留请求的股票列(缺失填 NaN),并打印/记录被剔除的股票。

🟡 _load_fina_field 函数WHERE end_date BETWEEN %s AND %s 会漏掉报告期在 start_date 之前、但在回测区间内已公告的最近一期财报,导致区间前段数据缺失。
修复建议:按公告日期对齐,并保留回测起点前最近一期可用报告。

🟡 _get_conn / DataLoader 实例 — 将 psycopg2 连接作为实例属性长期持有,psycopg2 连接非线程安全,且类未提供 close 方法,多线程或频繁实例化时可能并发异常或连接泄漏。
修复建议:每次查询使用上下文管理器新建/释放连接,或引入连接池,并暴露 close() 接口。

🟡 get_trade_dates 函数 — 数据库可用时返回的是 pd.to_datetime(...) 产生的 Series,与类型标注 pd.DatetimeIndex 不一致;数据库不可用时 pd.bdate_range 只排除周末,不是真实交易日。
修复建议:统一返回 pd.DatetimeIndex(...);无库时明确说明是“工作日近似”。

🟢 load_benchmark 函数 — 日期为空或数据库不可用时用股票池价格的算术均值代替真实指数,不能反映市值加权、分红、成分调整等,会误导绩效评估。
修复建议:回退序列命名或日志中标注 “SIMULATED”,且尽量采用随机游走生成独立基准。

🟢 _get_conn 函数 — 运行时修改全局 sys.path 以导入 config,会污染解释器环境,多个实例化可能重复插入路径。
修复建议:在模块顶层统一读取配置或从环境变量加载,避免运行时修改 sys.path

🟢 init / _stock_sql_placeholders — 未校验 stocks 是否为空,空列表会导致 SQL IN () 语法错误。
修复建议:在初始化或查询前检查 if not self.stocks 并抛出 ValueError

亮点

  1. SQL 安全:字段名通过白名单校验,股票代码使用参数化占位符,避免了 SQL 注入风险。
  2. 后复权价格计算daily.close * adj_factor 按同交易日 join,符合常见的后复权口径。
  3. 统一的回退机制:数据库不可用时自动生成确定性的模拟数据,便于离线 Notebook 演示完整流程。

quantitative_data/importer.py

审查结论

这是一个结构清晰、功能较完整的数据导入模块,批量化 UPSERT、按日期全市场拉取日线、断点续传等设计都值得肯定。但作为量化数据管道,存在几个方法论层面的严重问题:当前上市列表导致退市/暂停股的关键数据缺失(幸存者偏差)、财务报表保留最新公告覆盖历史公告(未来函数风险)、以及财务接口参数语义误用可能导致最新报告期缺失。此外还有一些数据类型、辅助函数和性能方面的中等/轻微问题。


问题清单

🔴 严重

1. import_adj_factor_batch / import_financial_statements 使用当前上市股票列表,造成幸存者偏差

  • 这两个函数接收的 stock_list 默认来自 get_stock_codes_from_db(),而该方法只取 list_status = 'L'
  • import_daily_by_date / import_daily_basic_by_date 是按交易日拉取全市场数据,会包含历史上已退市、暂停的股票。
  • 结果是:退市/暂停股票的复权因子、财务数据不会被导入,下游计算调整价格或财务因子时会系统性丢失这些股票,形成幸存者偏差。
  • 修复建议:对 adj_factor / 财务表使用全历史代码列表(包含 DP),或直接从已导入的 daily 表中提取出现过的所有 ts_code 进行导入。

2. 财务报表去重/更新机制存在未来函数风险(look-ahead bias)

  • batch_insert 中对 income / balancesheet / cashflow / fina_indicatorts_code, end_date[, report_type] 去重,并保留 ann_date / f_ann_date 最大的一条。
  • ON CONFLICT DO UPDATE 会用最新公告的数据覆盖旧公告,旧版本 announcement 行被丢失。
  • 如果下游直接用这个表做历史因子计算,会用到“事后修正”的财务数据,而这些修正在原报告期并不知晓。
  • 修复建议:将 ann_date(或 f_ann_date)纳入唯一键,保存每一版公告;或维护“点-in-time 表”与“最新快照表”两张表,查询时按 ann_date <= trade_date 过滤。

3. import_financial_statements 误用 Tushare 财务接口的 start_date / end_date 参数

  • 代码把回测起止日 START_DATE / END_DATE 直接传给 income_vip / balancesheet_vip / cashflow_vip / fina_indicator_vip
  • Tushare 这些接口的 start_date / end_date 通常表示公告日期区间,而非报告期;若 END_DATE 设为回测期末,会遗漏期末之后才公告的最近报告期数据。
  • 修复建议:使用 period 参数按报告期拉取,或将 end_date 显著延后并本地按 end_date 列过滤。

🟡 中等

4. import_daily_by_dateskip_existing 存在日期类型比较隐患

  • trade_dates 来自 trade_cal.cal_datedatetime.date),而 existing_dates 来自 daily.trade_date
  • 如果 daily.trade_date 在库中是 timestamp 类型,date == datetime 在 Python 集合比较中永远不等,导致已导入日期不会被跳过,重复拉取并 UPSERT。
  • 修复建议:在 SQL 里直接 NOT EXISTS 过滤缺失日期,或在 Python 侧统一转换为 datetime.date() 再比较。

5. 多个接口缺少显式数值转换,可能产生脏数据或插入失败

  • import_daily_basic_by_dateimport_financial_statementsimport_index_dailyimport_adj_factor 仅做日期/列名处理,未对数值列执行 pd.to_numeric(errors='coerce')
  • Tushare 返回的数值常以字符串形式出现,直接插入 NUMERIC 列时可能因 '-''''None' 等异常字符串导致隐式转换失败或写入脏数据。
  • 修复建议:为每个表增加 _normalize_*_df 函数,显式转换已知数值列;或在 batch_insert 前按数据库列类型做统一转换。

6. batch_insert 每次调用都查询 information_schema.columns,性能开销大

  • _get_table_columns 在每个 batch_insert 调用时都查一次元数据。
  • import_daily_by_date 中每个交易日调用一次,4000 个交易日会产生 4000 次元数据查询。
  • 修复建议:在模块级或连接级缓存表结构,表结构变更时刷新缓存。

7. check_table_summary 中日期信息元组嵌套错误

  • tables 中日期项写成 (("trade_cal", "cal_date"),),导致 tbl, col = date_info 解包失败(只有一个元素)。
  • 所有带日期的表都会进入 except 分支,日期统计功能失效。
  • 修复建议:去掉多余括号,改为 ("trade_cal", "cal_date")

8. import_stock_basic 缺失暂停股 P,且退市股拉取失败不阻断

  • get_all_stock_codes 已考虑 L/D/P 三类,但 import_stock_basic 只导入 L 并尝试 D,未处理 P
  • D 的拉取放在 try/except 中且仅记录 warning,失败不会重试,退市股基本信息可能静默缺失。
  • 修复建议:统一导入 L/D/P 三类;对 DP 也使用 fetch_with_retry 重试。

9. fetch_with_retry 在全部失败或返回空时静默返回空 DataFrame

  • 上层多数只记录 warning 并继续,不会把该日期/股票加入失败列表,导致缺数据不易被发现。
  • 修复建议:区分“API 返回空”和“拉取失败”,对后者抛异常或返回失败标记,便于断点续传和告警。

10. import_daily_basic 辅助函数可能同时传入互斥参数

  • 当传入 trade_date 时,start_date / end_date 仍被一起传给 daily_basic_vip,Tushare 可能不支持同时指定单日和区间。
  • 修复建议:根据调用模式互斥构造 kwargs,单日模式只传 trade_date

11. init_database 对 DDL 文件的切分方式不够健壮

  • 先删除 -- 开头行,再按 ; 切分。若 schema.sql 中包含函数/触发器体、或字符串字面量里带 ;,会错误拆分语句。
  • 修复建议:使用 psycopg2 直接执行整个文件,或按 ;\n 切分时保留引号/块上下文。

12. import_financial_statements 无跳过已存在逻辑且无重试

  • 每次全量重跑都会重新拉取所有股票的 4 个财务接口,耗时长且容易触发限流;单表失败仅 warning,缺失数据难以追溯。
  • 修复建议:增加按 ts_code + end_date 的 skip_existing 检查,并对每个接口使用 fetch_with_retry

🟢 轻微

13. 多处函数假设输入日期格式为 YYYY-MM-DD

  • import_trade_calimport_daily_by_date 直接使用 start_date.replace("-", "")
  • 若调用方传入紧凑格式 YYYYMMDD,会得到错误参数。
  • 修复建议:增加统一的日期归一化函数,兼容两种格式。

14. 模块级 logging.basicConfig 可能覆盖应用日志配置

  • 该模块被其他模块导入时会执行全局日志配置。
  • 修复建议:将日志配置移到 if __name__ == "__main__" 下,或提供配置开关。

15. get_sqlalchemy_engine 定义了引擎但当前模块未使用

  • get_pg_connection 使用的密码来源不一致(PASSWORD_ENCODED vs DB_CONFIG["password"]),容易造成维护混乱。
  • 修复建议:统一密码来源,或移除未使用的引擎。

16. import_daily_by_dateskip_existing 只检查某日是否有记录,不检查完整性

  • 若某天 API 返回部分数据并成功写入,后续会被视为“已有”而跳过,导致该日股票缺失。
  • 修复建议:增加按日 COUNT(DISTINCT ts_code) 阈值校验,或记录每日期望股票数。

亮点

  1. 按交易日拉取全市场日线import_daily_by_date)是比按股票循环更合理的设计,能显著减少 API 调用次数,并避免按股票循环容易引入的幸存者偏差。
  2. batch_insert 的批量 UPSERT + 批内去重逻辑实现得较为完整,特别是按公告日期保留最新记录、冲突键类型归一化、失败后逐行回退等细节考虑周到。
  3. 提供了断点续传与进度查询工具resume_daily_by_dateget_missing_daily_datescheck_daily_progress),对长期运行的数据维护非常实用。

quantitative_data/config.py

审查结论

该文件是一份数据层配置,整体上将敏感信息交给环境变量/.env 管理,并做了启动期凭证非空校验,符合基本安全习惯。但作为一个会被多处 import 的 config.py,它在模块导入时直接抛出硬错误,且对日期、批次等关键参数缺少格式/范围校验,目前不涉及量化策略逻辑,因此 look-ahead bias、因子计算、回测执行等风险暂不适用。

问题清单

🔴 严重

[🔴严重] 模块顶层(约第 54–66 行)- 关键凭证缺失时在 import 阶段直接 raise ValueError
这会让任何仅想引用配置常量、运行与数据库/Tushare 无关的单元测试、lint 或 notebook 的场景都无法导入模块;同时把“配置读取”与“使用凭证”强耦合。
修复建议:将强校验改为惰性校验(如提供 validate_config() 函数),仅在真正建立连接或调用 API 前检查,避免 import 即失败。

🟡 中等

[🟡中等] 全局变量 START_DATE / END_DATE(约第 69–70 行)- 缺少日期格式与区间校验
目前仅按字符串读取,未校验是否为合法日期、是否 START_DATE <= END_DATE;默认 END_DATE 为固定日历日,可能在运行时成为过期日期或历史运行时的未来日期,向下游数据获取/回测传递错误区间。
修复建议:用 datetime.date.fromisoformat 解析并校验区间,结束日期默认建议取 date.today() 或由调用方显式覆盖。

[🟡中等] .env 加载逻辑(约第 20–28 行)- 回退到 Path.cwd() / ".env" 可能误加载错误配置
当脚本从非项目目录启动时,可能加载到其他项目或临时目录下的 .env,导致使用了错误的数据库地址、Token 或参数。
修复建议:移除对当前工作目录的隐式回退,或仅允许显式指定 .env 路径,并在日志中明确输出加载的文件。

🟢 轻微

[🟢轻微] .env 加载分支(约第 24、32 行)- 使用 print 输出加载状态
在服务端进程、定时任务、测试或非 UTF-8 终端中,print 会污染 stdout/日志,且无法按日志级别关闭。
修复建议:改用 logging 模块按级别输出,或仅在 verbose/debug 模式下打印。

[🟢轻微] TUSHARE_TOKEN 定义与校验(约第 51、62 行)- 重复读取环境变量
第 51 行已读取 TUSHARE_TOKEN,第 62 行又重新读取到 _TUSHARE,存在维护时遗漏的风险。
修复建议:复用已定义的 TUSHARE_TOKEN 变量进行非空校验。

[🟢轻微] BATCH_SIZE(约第 68 行)- 未校验正整数
若环境变量设置为 0、负数或非数字字符串,下游批量插入可能出现除零或异常。
修复建议:赋值后校验 BATCH_SIZE > 0,并对非数字值给出明确错误提示。

亮点

  1. 敏感信息通过环境变量/.env 管理,未在代码中硬编码数据库密码或 Tushare Token,符合安全最佳实践。
  2. 对密码使用 quote_plus 编码,为生成 SQLAlchemy 连接串时避免特殊字符解析错误做了准备。
  3. 通过 Path(__file__).resolve() 定位 .env,相比直接使用相对路径更能抵御工作目录变化带来的影响。
## 完整问题清单(Part 2/2) ### alpha/factors.py ## 审查结论 该模块在因子注册、抽象基类和 IC 计算框架上有清晰的设计,但**因子标准化、分层回测和换手率分析存在明显的前视偏差或逻辑错误**,直接用于实盘回测会导致信号失真。建议在继续扩展因子库之前先修正核心的截面处理与时序对齐问题。 ## 问题清单 ### 🔴 严重 **🔴 严重 `TechnicalFactor.compute`(约第118–126行)- 全样本标准化导致前视偏差** `mom_z / vol_z` 使用整个序列的 `.mean()` / `.std()` 做标准化,意味着在 t 日使用了未来数据计算均值和标准差;对多资产面板而言还混入了未来截面信息。 修复建议:按截面(或至少按扩展窗口/滚动窗口)逐期标准化,例如 `factor.groupby(level=0).apply(lambda s: (s - s.mean()) / s.std())`。 **🔴 严重 `FundamentalFactor.compute`(约第140–161行)- 同样存在全样本标准化前视偏差** ROE、PE、PB、营收增速均在全样本上做 z-score,且未按行业/日期做中性化处理,面板下会用到未来和跨期信息。 修复建议:按财报披露截止日或交易日期做截面标准化,并处理缺失值(如仅对非缺失值做标准化后再合并)。 **🔴 严重 `FactorAnalyzer.quantile_returns`(约第232–239行)- 全局分位数而非截面分位数,且累计收益计算错误** `pd.qcut` 直接对所有样本做全局分位,未按日期截面分组;`cum_return = avg_return.cumsum()` 是在“组号 1→5”上累加,失去了时间维度,不是组合累计收益。 修复建议:按 `level=0` 分组后对每个截面 `qcut`,再计算每组时序平均收益和累计收益。 **🔴 严重 `FactorAnalyzer.turnover`(约第241–255行)- 换手率逻辑未实现且计算对象错误** MultiIndex 分支下仅写了 `pass` 未返回结果;非 MultiIndex 分支用 `self.factor.autocorr(lag=1)` 对整个面板做自相关,混淆了不同资产和日期,不能代表“相邻期分位数变化比例”。 修复建议:按日期截面计算持仓权重或分位组,再计算相邻期权的换手比例序列。 ### 🟡 中等 **🟡 中等 `FactorAnalyzer.ic_decay`(约第220–230行)- 池化计算 IC 衰减,未按期聚合** 直接用 `stats.spearmanr` 对所有截面的因子值和未来收益做整体相关,得到的是池化相关系数,而不是“每期 IC 的均值”,会混入时序趋势。 修复建议:按 `level=0` 逐期计算 Spearman/Pearson IC,再对各期 IC 取平均。 **🟡 中等 `FactorAnalyzer.compute_ic`(约第179–205行)- 未显式对齐因子与收益,缺失日期会抛 KeyError** `fwd.get_group(g.name)` 在 `forward_returns` 缺少某个日期时会直接抛错;`g.corr` 对内部股票做内连接,也可能在截面不完整时静默忽略。 修复建议:先 `self.factor.align(self.forward, join='inner')`,或在 `apply` 中用 `try/except` 跳过缺失截面。 **🟡 中等 `BaseFactor.neutralize`(约第69–96行)- 对空数据、共线行业和异常市值缺乏保护** `market_cap <= 0` 被 `clip(lower=1)` 当成市值 1 处理,会扭曲回归;`group` 含 NaN 时 `get_dummies` 会将其所有行业 dummy 置 0;若 `common_idx` 为空或 X 列完全共线,`lstsq` 结果不稳定。 修复建议:过滤市值<=0 和分组为 NaN 的样本,并在回归前检查 X 的秩或改用 `np.linalg.lstsq` 的残差逻辑加异常处理。 **🟡 中等 `TechnicalFactor.compute` / `FundamentalFactor.compute` - 未防御标准差为零** 当因子序列恒定(如所有 PE 相同)或仅含 NaN 时,`series.std()` 为 0/NaN,导致 `inf` 或全 NaN,可能进一步触发 `pd.qcut` 报错。 修复建议:标准化前判断 `std > 0`,否则返回 NaN 或跳过该因子。 **🟡 中等 `FactorAnalyzer.quantile_returns` / `turnover`(约第235、244行)- `pd.qcut` 对重复边界会抛异常** 当因子存在大量相同值时,`qcut` 默认 `duplicates='raise'` 会抛出 `ValueError`。 修复建议:使用 `pd.qcut(..., duplicates='drop')` 或改用 `pd.cut`。 ### 🟢 轻微 **🟢 轻微 `TechnicalFactor.compute`(约第123–125行)- 动量/波动率窗口不一致导致合成结果含大量 NaN** `mom` 与 `vol` 的 NaN 长度不同,`0.5 * mom_z + 0.5 * vol_z` 在任一者为 NaN 时整体为 NaN,损失了更多可用信号日。 修复建议:统一窗口长度或采用 `fillna` / 加权时只使用两者均非 NaN 的截面。 **🟢 轻微 `BaseFactor.neutralize`(约第83行)- 市值非正数未剔除而是硬编码为 1** `np.log(market_cap.clip(lower=1))` 把 0/负市值变成 log(1)=0,相当于给这些样本赋予了错误市值。 修复建议:先 `market_cap = market_cap[market_cap > 0]` 再取 log,保证样本质量。 **🟢 轻微 `FactorAnalyzer.turnover`(约第252行)- 依赖 `self.ic_series` 决定是否计算自相关系数** 换手率计算与 IC 序列无必然联系;若用户未先调用 `compute_ic`,此处会返回空 Series,行为不稳定。 修复建议:移除对 `ic_series` 的状态依赖,直接基于因子计算换手率。 ## 亮点 1. **注册表 + 抽象基类设计清晰**:`FactorRegistry` 与 `BaseFactor` 便于后续扩展新因子,并统一了 `compute` 接口。 2. **IC 计算采用截面分组思路**:`compute_ic` 对 MultiIndex 按日期分组计算 Spearman/Pearson,符合截面因子研究范式。 3. **中性化实现方向正确**:通过行业虚拟变量 + 市值做 OLS 取残差,是行业中性的常规做法。 --- ### alpha/data_loader.py ## 审查结论 该数据加载器接口清晰、字段白名单 + SQL 参数化能够有效防止注入,整体结构便于 Notebook 快速演示。但代码在“回测正确性”上存在硬伤:财务数据使用前视了报告期;所有数据库异常都静默回退到随机模拟数据;模拟数据又让不同因子完全相同。这些问题在不连接真实数据库时极难被发现,会严重污染研究结论。 ## 问题清单 按严重程度排列: 🔴 **_load_fina_field 函数** — 以财报 `end_date` 作为数据生效日期并直接 `ffill()`,相当于在报告期末当天就知道该期财务结果,而实际财报通常在报告期结束后数周才披露,构成典型的未来函数/前视偏差。 修复建议:使用 `ann_date`(公告日)或报告期后的首个交易日作为生效起点,再向前填充。 🔴 **load_prices / load_factor / load_benchmark 函数** — 使用裸 `except Exception` 捕获所有异常后静默回退到模拟数据,SQL 语法错误、网络超时、表不存在等情况都会让调用方在不知情的情况下拿到随机数据。 修复建议:增加 `strict` 模式(默认开启),数据库异常时直接抛出;仅在网络不可用时明确回退,并在返回数据中标注 “SIMULATED”。 🔴 **_simulate_factor 函数** — 每次调用都重置 `np.random.seed(self.seed)` 且未按因子名区分随机状态,导致不同因子的模拟矩阵完全相同;同时与 `_simulate_prices` 共享同一 seed,可能人为制造出因子与收益的相关性。 修复建议:为每个因子生成独立随机状态,例如 `rng = np.random.default_rng(hash(name) ^ self.seed)`。 🟡 **load_factor 函数** — 对于不在 `DAILY_BASIC_FIELDS` 或 `FINA_FIELDS` 中的 `name` 也直接返回模拟数据,不会报错,容易把拼写错误当成有效因子。 修复建议:在入口处校验 `name`,未知因子抛出 `ValueError`。 🟡 **_load_prices_db 函数** — `dropna(how='all', axis=1)` 会静默剔除无数据的股票列,且不强制对齐到完整股票池/交易日,可能引入幸存者偏差或导致下游截面运算维度不一致。 修复建议:保留请求的股票列(缺失填 `NaN`),并打印/记录被剔除的股票。 🟡 **_load_fina_field 函数** — `WHERE end_date BETWEEN %s AND %s` 会漏掉报告期在 `start_date` 之前、但在回测区间内已公告的最近一期财报,导致区间前段数据缺失。 修复建议:按公告日期对齐,并保留回测起点前最近一期可用报告。 🟡 **_get_conn / DataLoader 实例** — 将 `psycopg2` 连接作为实例属性长期持有,`psycopg2` 连接非线程安全,且类未提供 `close` 方法,多线程或频繁实例化时可能并发异常或连接泄漏。 修复建议:每次查询使用上下文管理器新建/释放连接,或引入连接池,并暴露 `close()` 接口。 🟡 **get_trade_dates 函数** — 数据库可用时返回的是 `pd.to_datetime(...)` 产生的 `Series`,与类型标注 `pd.DatetimeIndex` 不一致;数据库不可用时 `pd.bdate_range` 只排除周末,不是真实交易日。 修复建议:统一返回 `pd.DatetimeIndex(...)`;无库时明确说明是“工作日近似”。 🟢 **load_benchmark 函数** — 日期为空或数据库不可用时用股票池价格的算术均值代替真实指数,不能反映市值加权、分红、成分调整等,会误导绩效评估。 修复建议:回退序列命名或日志中标注 “SIMULATED”,且尽量采用随机游走生成独立基准。 🟢 **_get_conn 函数** — 运行时修改全局 `sys.path` 以导入 `config`,会污染解释器环境,多个实例化可能重复插入路径。 修复建议:在模块顶层统一读取配置或从环境变量加载,避免运行时修改 `sys.path`。 🟢 **__init__ / _stock_sql_placeholders** — 未校验 `stocks` 是否为空,空列表会导致 SQL `IN ()` 语法错误。 修复建议:在初始化或查询前检查 `if not self.stocks` 并抛出 `ValueError`。 ## 亮点 1. **SQL 安全**:字段名通过白名单校验,股票代码使用参数化占位符,避免了 SQL 注入风险。 2. **后复权价格计算**:`daily.close * adj_factor` 按同交易日 join,符合常见的后复权口径。 3. **统一的回退机制**:数据库不可用时自动生成确定性的模拟数据,便于离线 Notebook 演示完整流程。 --- ### quantitative_data/importer.py ## 审查结论 这是一个结构清晰、功能较完整的数据导入模块,批量化 UPSERT、按日期全市场拉取日线、断点续传等设计都值得肯定。但作为量化数据管道,存在几个**方法论层面的严重问题**:当前上市列表导致退市/暂停股的关键数据缺失(幸存者偏差)、财务报表保留最新公告覆盖历史公告(未来函数风险)、以及财务接口参数语义误用可能导致最新报告期缺失。此外还有一些数据类型、辅助函数和性能方面的中等/轻微问题。 --- ## 问题清单 ### 🔴 严重 **1. `import_adj_factor_batch` / `import_financial_statements` 使用当前上市股票列表,造成幸存者偏差** - 这两个函数接收的 `stock_list` 默认来自 `get_stock_codes_from_db()`,而该方法只取 `list_status = 'L'`。 - 但 `import_daily_by_date` / `import_daily_basic_by_date` 是按交易日拉取**全市场**数据,会包含历史上已退市、暂停的股票。 - 结果是:退市/暂停股票的**复权因子、财务数据不会被导入**,下游计算调整价格或财务因子时会系统性丢失这些股票,形成幸存者偏差。 - **修复建议**:对 adj_factor / 财务表使用全历史代码列表(包含 `D`、`P`),或直接从已导入的 `daily` 表中提取出现过的所有 `ts_code` 进行导入。 **2. 财务报表去重/更新机制存在未来函数风险(look-ahead bias)** - `batch_insert` 中对 `income` / `balancesheet` / `cashflow` / `fina_indicator` 按 `ts_code, end_date[, report_type]` 去重,并保留 `ann_date` / `f_ann_date` 最大的一条。 - `ON CONFLICT DO UPDATE` 会用最新公告的数据**覆盖旧公告**,旧版本 announcement 行被丢失。 - 如果下游直接用这个表做历史因子计算,会用到“事后修正”的财务数据,而这些修正在原报告期并不知晓。 - **修复建议**:将 `ann_date`(或 `f_ann_date`)纳入唯一键,保存每一版公告;或维护“点-in-time 表”与“最新快照表”两张表,查询时按 `ann_date <= trade_date` 过滤。 **3. `import_financial_statements` 误用 Tushare 财务接口的 `start_date` / `end_date` 参数** - 代码把回测起止日 `START_DATE` / `END_DATE` 直接传给 `income_vip` / `balancesheet_vip` / `cashflow_vip` / `fina_indicator_vip`。 - Tushare 这些接口的 `start_date` / `end_date` 通常表示**公告日期区间**,而非报告期;若 `END_DATE` 设为回测期末,会遗漏期末之后才公告的最近报告期数据。 - **修复建议**:使用 `period` 参数按报告期拉取,或将 `end_date` 显著延后并本地按 `end_date` 列过滤。 --- ### 🟡 中等 **4. `import_daily_by_date` 的 `skip_existing` 存在日期类型比较隐患** - `trade_dates` 来自 `trade_cal.cal_date`(`datetime.date`),而 `existing_dates` 来自 `daily.trade_date`。 - 如果 `daily.trade_date` 在库中是 `timestamp` 类型,`date == datetime` 在 Python 集合比较中永远不等,导致已导入日期不会被跳过,重复拉取并 UPSERT。 - **修复建议**:在 SQL 里直接 `NOT EXISTS` 过滤缺失日期,或在 Python 侧统一转换为 `datetime.date()` 再比较。 **5. 多个接口缺少显式数值转换,可能产生脏数据或插入失败** - `import_daily_basic_by_date`、`import_financial_statements`、`import_index_daily`、`import_adj_factor` 仅做日期/列名处理,未对数值列执行 `pd.to_numeric(errors='coerce')`。 - Tushare 返回的数值常以字符串形式出现,直接插入 NUMERIC 列时可能因 `'-'`、`''`、`'None'` 等异常字符串导致隐式转换失败或写入脏数据。 - **修复建议**:为每个表增加 `_normalize_*_df` 函数,显式转换已知数值列;或在 `batch_insert` 前按数据库列类型做统一转换。 **6. `batch_insert` 每次调用都查询 `information_schema.columns`,性能开销大** - `_get_table_columns` 在每个 `batch_insert` 调用时都查一次元数据。 - 在 `import_daily_by_date` 中每个交易日调用一次,4000 个交易日会产生 4000 次元数据查询。 - **修复建议**:在模块级或连接级缓存表结构,表结构变更时刷新缓存。 **7. `check_table_summary` 中日期信息元组嵌套错误** - `tables` 中日期项写成 `(("trade_cal", "cal_date"),)`,导致 `tbl, col = date_info` 解包失败(只有一个元素)。 - 所有带日期的表都会进入 `except` 分支,日期统计功能失效。 - **修复建议**:去掉多余括号,改为 `("trade_cal", "cal_date")`。 **8. `import_stock_basic` 缺失暂停股 `P`,且退市股拉取失败不阻断** - `get_all_stock_codes` 已考虑 `L/D/P` 三类,但 `import_stock_basic` 只导入 `L` 并尝试 `D`,未处理 `P`。 - `D` 的拉取放在 `try/except` 中且仅记录 warning,失败不会重试,退市股基本信息可能静默缺失。 - **修复建议**:统一导入 `L/D/P` 三类;对 `D`、`P` 也使用 `fetch_with_retry` 重试。 **9. `fetch_with_retry` 在全部失败或返回空时静默返回空 DataFrame** - 上层多数只记录 warning 并继续,不会把该日期/股票加入失败列表,导致缺数据不易被发现。 - **修复建议**:区分“API 返回空”和“拉取失败”,对后者抛异常或返回失败标记,便于断点续传和告警。 **10. `import_daily_basic` 辅助函数可能同时传入互斥参数** - 当传入 `trade_date` 时,`start_date` / `end_date` 仍被一起传给 `daily_basic_vip`,Tushare 可能不支持同时指定单日和区间。 - **修复建议**:根据调用模式互斥构造 kwargs,单日模式只传 `trade_date`。 **11. `init_database` 对 DDL 文件的切分方式不够健壮** - 先删除 `--` 开头行,再按 `;` 切分。若 `schema.sql` 中包含函数/触发器体、或字符串字面量里带 `;`,会错误拆分语句。 - **修复建议**:使用 `psycopg2` 直接执行整个文件,或按 `;\n` 切分时保留引号/块上下文。 **12. `import_financial_statements` 无跳过已存在逻辑且无重试** - 每次全量重跑都会重新拉取所有股票的 4 个财务接口,耗时长且容易触发限流;单表失败仅 warning,缺失数据难以追溯。 - **修复建议**:增加按 `ts_code + end_date` 的 skip_existing 检查,并对每个接口使用 `fetch_with_retry`。 --- ### 🟢 轻微 **13. 多处函数假设输入日期格式为 `YYYY-MM-DD`** - 如 `import_trade_cal`、`import_daily_by_date` 直接使用 `start_date.replace("-", "")`。 - 若调用方传入紧凑格式 `YYYYMMDD`,会得到错误参数。 - **修复建议**:增加统一的日期归一化函数,兼容两种格式。 **14. 模块级 `logging.basicConfig` 可能覆盖应用日志配置** - 该模块被其他模块导入时会执行全局日志配置。 - **修复建议**:将日志配置移到 `if __name__ == "__main__"` 下,或提供配置开关。 **15. `get_sqlalchemy_engine` 定义了引擎但当前模块未使用** - 与 `get_pg_connection` 使用的密码来源不一致(`PASSWORD_ENCODED` vs `DB_CONFIG["password"]`),容易造成维护混乱。 - **修复建议**:统一密码来源,或移除未使用的引擎。 **16. `import_daily_by_date` 的 `skip_existing` 只检查某日是否有记录,不检查完整性** - 若某天 API 返回部分数据并成功写入,后续会被视为“已有”而跳过,导致该日股票缺失。 - **修复建议**:增加按日 `COUNT(DISTINCT ts_code)` 阈值校验,或记录每日期望股票数。 --- ## 亮点 1. **按交易日拉取全市场日线**(`import_daily_by_date`)是比按股票循环更合理的设计,能显著减少 API 调用次数,并避免按股票循环容易引入的幸存者偏差。 2. **`batch_insert` 的批量 UPSERT + 批内去重逻辑**实现得较为完整,特别是按公告日期保留最新记录、冲突键类型归一化、失败后逐行回退等细节考虑周到。 3. **提供了断点续传与进度查询工具**(`resume_daily_by_date`、`get_missing_daily_dates`、`check_daily_progress`),对长期运行的数据维护非常实用。 --- ### quantitative_data/config.py ## 审查结论 该文件是一份数据层配置,整体上将敏感信息交给环境变量/`.env` 管理,并做了启动期凭证非空校验,符合基本安全习惯。但作为一个会被多处 import 的 `config.py`,它在模块导入时直接抛出硬错误,且对日期、批次等关键参数缺少格式/范围校验,目前不涉及量化策略逻辑,因此 look-ahead bias、因子计算、回测执行等风险暂不适用。 ## 问题清单 ### 🔴 严重 **[🔴严重] 模块顶层(约第 54–66 行)- 关键凭证缺失时在 import 阶段直接 `raise ValueError`** 这会让任何仅想引用配置常量、运行与数据库/Tushare 无关的单元测试、lint 或 notebook 的场景都无法导入模块;同时把“配置读取”与“使用凭证”强耦合。 修复建议:将强校验改为惰性校验(如提供 `validate_config()` 函数),仅在真正建立连接或调用 API 前检查,避免 import 即失败。 ### 🟡 中等 **[🟡中等] 全局变量 `START_DATE` / `END_DATE`(约第 69–70 行)- 缺少日期格式与区间校验** 目前仅按字符串读取,未校验是否为合法日期、是否 `START_DATE <= END_DATE`;默认 `END_DATE` 为固定日历日,可能在运行时成为过期日期或历史运行时的未来日期,向下游数据获取/回测传递错误区间。 修复建议:用 `datetime.date.fromisoformat` 解析并校验区间,结束日期默认建议取 `date.today()` 或由调用方显式覆盖。 **[🟡中等] `.env` 加载逻辑(约第 20–28 行)- 回退到 `Path.cwd() / ".env"` 可能误加载错误配置** 当脚本从非项目目录启动时,可能加载到其他项目或临时目录下的 `.env`,导致使用了错误的数据库地址、Token 或参数。 修复建议:移除对当前工作目录的隐式回退,或仅允许显式指定 `.env` 路径,并在日志中明确输出加载的文件。 ### 🟢 轻微 **[🟢轻微] `.env` 加载分支(约第 24、32 行)- 使用 `print` 输出加载状态** 在服务端进程、定时任务、测试或非 UTF-8 终端中,`print` 会污染 stdout/日志,且无法按日志级别关闭。 修复建议:改用 `logging` 模块按级别输出,或仅在 verbose/debug 模式下打印。 **[🟢轻微] `TUSHARE_TOKEN` 定义与校验(约第 51、62 行)- 重复读取环境变量** 第 51 行已读取 `TUSHARE_TOKEN`,第 62 行又重新读取到 `_TUSHARE`,存在维护时遗漏的风险。 修复建议:复用已定义的 `TUSHARE_TOKEN` 变量进行非空校验。 **[🟢轻微] `BATCH_SIZE`(约第 68 行)- 未校验正整数** 若环境变量设置为 `0`、负数或非数字字符串,下游批量插入可能出现除零或异常。 修复建议:赋值后校验 `BATCH_SIZE > 0`,并对非数字值给出明确错误提示。 ## 亮点 1. **敏感信息通过环境变量/`.env` 管理**,未在代码中硬编码数据库密码或 Tushare Token,符合安全最佳实践。 2. **对密码使用 `quote_plus` 编码**,为生成 SQLAlchemy 连接串时避免特殊字符解析错误做了准备。 3. **通过 `Path(__file__).resolve()` 定位 `.env`**,相比直接使用相对路径更能抵御工作目录变化带来的影响。
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: shellway/quanxiel#17