FinMarketPy / 回测可信度体检

回测结果可信吗:三层体检清单与一份可复现协议

这是本站最主要的差异化内容:官方资料里没有「怎么判断回测结果能不能信」这件事。README 只给了两个示例,PLANNED_FEATURES.md 里「更多交易结果统计」还列在待办。下面这份清单的每一条都能在 0.11.19 源码里找到对应依据(参数名、实现细节或示例注释),分成数据层、策略层、结果层三层;最后一节是一份可复现协议,把该写下来的东西列全。它不是官方标准,也不是投资建议。

依据:BacktestRequest 参数依据:SMA 的 shift(1) 实现依据:官方示例注释本站未实机运行
数据层连续性 / 重复 / 时区 / 缺失 / 价格字段
策略层成本 / 信号延迟 / 未来函数 / 杠杆
结果层年化因子 / 基准 / 集中度 / 样本外
回测可信度三层检查示意(依据 BacktestRequest 参数、marketconstants.py 与官方示例注释整理,非官方标准);示意非官方流程图,请以仓库代码与实际运行结果为准。
FinMarketPy · FinMarketPy · FinMarketPy · FinMarketPy · FinMarketPy · FinMarketPy · FinMarketPy · Why

为什么一个「漂亮」的回测结果可能没有意义

同一份数据、同一个策略,只改几个口径参数,结论就能从「跑赢基准」变成「跑输基准」。下面这些都会真实改变结果,而且都不需要你写错代码。

被改的东西变化方向对结果的典型影响在 finmarketpy 里对应哪个参数
交易成本从 2.5bp 改成 0去掉摩擦高换手策略收益被系统性抬高(趋势策略尤其明显)spot_tc_bpspot_rc_bp
信号延后去掉(当日信号当日成交)引入未来函数结果显著变好,且好得不真实signal_delay
年化因子与数据频率不匹配口径错位年化收益、年化波动、Sharpe 同时失真ann_factorresample_ann_factor
用现货价而不是含 carry 的总收益指数漏掉收益来源外汇策略的收益被低估或错估(官方示例注释明确提过)asset_df 的来源选择
不设最大杠杆放开约束低波动期仓位被放大,回撤结构完全改变signal_vol_max_leveragemax_net_exposuremax_abs_exposure
累计方式从乘法换成加法口径变化长期收益被系统性低估(加法指数忽略复利)cum_index'mult' / 'add'
不加基准缺少对照无法判断策略是在创造收益还是在跟随市场include_benchmark
结论:看到任何回测结果(包括本站引用的、也包括别人文章里的),先问三件事——数据是什么、口径是什么、有没有对照。答不上来的数字,先不要下结论。
Layer 1

数据层怎么查:先确认喂进去的数据是对的

这一层的问题往往在回测之前就存在,但会在结果里被放大成「策略问题」。

检查项怎么查不合格的典型症状处置方向
日期是否连续看索引的差分分布,与交易日历对照某段「平坦」的净值曲线用 findatapy 的 calendar.py 对齐交易日,而不是自然日
有没有重复日期df.index.duplicated().sum()收益被重复计入,波动率异常去重后再进回测;查数据源是否按合并了多个频率
有没有未来日期检查索引最大值是否超过今天看起来「刚更新过」的数据截断哨兵值/空行造成的时间戳
缺失值规模按列统计 NaN 比例与最长缺口某些品种整段没有数据却仍参与组合要么补齐,要么在标的集合里剔除
时区与对齐口径确认各数据源的时区是否统一(findatapy 有 timezone.py跨市场组合的信号错位一天统一到同一时区后再做横截面运算
价格字段是什么确认 trading_field 与实际字段一致把收盘价与结算价混用明确写进可复现协议;不要靠默认值
异常跳变看单日收益率分布的两端某一天贡献了全部收益/全部回撤核对是否数据错误、拆股、换月或单位问题
汇率交叉盘怎么来的确认非直盘报价是「原币种取数」还是「按 USD 交叉推算」同一货币对在不同数据源下收益不同findatapy 支持按 USD 交叉自动推算(如 AUDJPY),要记录你用哪种
Layer 2

策略层怎么查:信号与成交对不对得上

这一层是「未来函数」的重灾区。Backtest 引擎不会替你检查信号是否用了事后信息。

检查项怎么查不合格的典型症状处置方向
信号是否含未来信息用同一段数据、把信号整体延后一天重跑,看结果是否大幅衰减延后一天后收益腰斩甚至转负重构信号,只用当日及之前的信息
信号生成与成交是否错位确认 signal_delay 的取值与实际交易日历一致「当日收盘信号、当日收盘成交」这种不可能的执行至少延后一个周期;写进协议
成本是否计入检查 spot_tc_bp 是否为 0高换手策略收益异常漂亮设成与品种/频率相符的量级并说明依据
外汇展期成本检查 spot_rc_bp 与是否使用总收益指数长期持有的外汇策略收益系统性偏低或偏高官方示例注释已提醒:优先用含 carry 的总收益指数
杠杆与敞口约束检查 *_max_leveragemax_net_exposuremax_abs_exposure杠杆曲线出现不现实的尖峰设上限;把实际杠杆分布画出来看一眼
调仓频率是否现实检查 *_rebalance_freqposition_clip_*每日全额换手低频再平衡 + 单次调仓裁剪
停牌/不可交易时段确认这些日期在数据里是否被当作正常交易日在不可能成交的日期产生交易用交易日历剔除,或把该品种在该区间置为无信号
关键参数是否被硬编码把回测用到的参数从代码里抽出来换机器/换人复现结果不一致全部外提为可配置项,写进协议
官方内置指标是带滞后保护的,你自己写的不是。源码里 SMA 分支用 data_frame.shift(1).rolling(window=sma_period-1).sum() + data_frame_early 再除以周期,等于把历史部分整体错开一期;这是防未来函数的设计。而你传进去的自定义 signal_df 不会有任何检查——这是「越容易改的地方越容易出错」。
Layer 3

结果层查哪些:数字算对了,不代表结论成立

前三层都对,结果仍可能因为「收益结构」而不可采信。

检查项怎么查不合格的典型症状处置方向
收益是否集中在少数交易用 findatapy 的 Calculations().calculate_individual_trade_gains(signals, returns) 看逐笔分布去掉收益最高的 2–3 笔后策略转负要么承认这是尾部驱动、要么放弃该结论
Sharpe 的口径确认年化因子、无风险利率、收益频率三处一致同一策略在不同工具里 Sharpe 差一倍写清口径;比较不同策略前先统一
最大回撤发生在哪看回撤时间序列而不只看最大值回撤集中在某个单一历史阶段判断是结构性问题还是时代背景
是否与基准比较打开 include_benchmark 或用 BacktestComparison.plot_pnl(diff=True)只有绝对收益,没有相对收益至少补一个买入持有对照
是否做了样本外把区间分成两段分别跑样本内极好、样本外失效参数在样本内定,结论只在样本外下
参数敏感性把核心参数在小范围内上下扫一遍只有某一个「神奇参数值」能赚用参数平原(而非尖峰)判断稳健性
换手与实际容量统计交易笔数与名义规模分布策略容量远小于你的资金规模把容量约束写进结论
Reproducibility

可复现协议要写哪些字段

看到一个回测数字却不知道该数字怎么产生的,是最常见的问题。下面这份字段表的作用是「让别人能重跑」——里面对应到 finmarketpy 的具体参数,能填的尽量填。

字段为什么要写对应到 finmarketpy 的哪里
finmarketpy / findatapy / chartpy 版本依赖行为会随版本变化pip show 三个包;或记录安装方式(PyPI / git+)
Python 与 numpy / pandas 版本numpy 有 <2 硬约束,pandas 无上限环境自检命令(见安装页
数据源与凭证路径不同源的同名 ticker 可能不是同一序列data_sourcevendor_tickersvendor_fields
数据下载时间与频率历史会被修订(宏观数据尤其)start_date / finish_date / freq、抓取日期
时区与交易日历跨市场对齐差一天就出错findatapy 的时区与日历设置
价格字段与复权口径收盘价/结算价/前复权不是一回事trading_fieldfields
信号生成时间与执行时间未来函数的首要防线signal_delay、信号构造代码
成本与展期成本决定策略是否还有超额收益spot_tc_bpspot_rc_bp
杠杆与敞口约束决定回撤结构是否真实*_max_leveragemax_net_exposuremax_abs_exposure
再平衡频率与缺失值处理换手与信号序列都会变*_rebalance_freqTechParams.fillna
年化因子与累计方式直接决定统计口径ann_factorcum_index
基准与样本外区间没有对照就没有结论include_benchmark、区间切分方式
最小可复现包:把上表填成一段文字或一张 YAML,和你的回测代码、requirements 一起存档。这比「贴一张收益曲线截图」有用得多——曲线不能重跑,协议可以。
FAQ

关于回测可信度的高频问题

清单依据 0.11.19 源码与官方示例整理,不是官方标准;本站未实机运行验证。

「未来函数」到底指什么?怎么最简单查出来?

指回测里用了在当时还不可能知道的信息——最常见的是用当日收盘价生成信号、又用当日收盘价成交,或者用未来若干期的均值来构造当期信号。最省事的自查方法:把信号整体延后一期(或增强 signal_delay)重跑,如果收益大幅衰减甚至转负,就说明原结果的收益主要来自「提前知道」。这在 finmarketpy 里对应 br.signal_delay;引擎本身不会替你检查信号构造代码。

为什么官方示例特别强调「用总收益指数而不是现货价」?

官方 backtest_example.py 的注释原话意思是:这里用现货价算收益,但更好的做法是用包含 carry 的外汇总收益指数。原因是外汇持有本身会产生利差(carry)收益,只看现货价的涨跌会漏掉这一块。对趋势类策略来说,这会系统性改变结果。所以你在写外汇回测时,先确定收益口径是「现货」还是「总收益」,并把它写进可复现协议。

年化因子该用 252 还是 260?

看你的数据频率与市场日历。官方示例里设的是 br.ann_factor = 252,并配 signal_vol_obs_in_year = 252。关键在于年化因子、波动率估计窗、观测数三者要自洽:如果数据实际是每周 5 天而某段有节假日,实际观测数会少于 252;如果你在别的频率上复用 252,年化数字就会错。本站不给推荐值——写清你用的是哪个数、为什么。

我把成本设成 0 先看「纯策略收益」,可以吗?

作为探索可以,作为结论不行。高换手策略在零成本下的收益会明显偏高,一旦加回真实成本可能直接转负。比较稳妥的做法是:初始探索可以用零成本看信号方向,但任何要对外说的数字都必须带成本,并且说明成本取值依据。官方示例用的是 2.5bp。

回测看起来很好,但实盘完全不是这样,通常是哪里出问题?

常见于四类:①成本与滑点被低估(尤其是高换手策略);②执行时点与信号时点错位;③策略容量小于实际资金(冲击成本没算);④样本内过拟合。前两类能在回测配置里查到(spot_tc_bpsignal_delay),后两类属于方法论问题,需要样本外与容量分析。本站不提供实盘执行方案,也不会给出收益预期。

这些检查项能自动化吗?

一部分可以:数据层的重复日期、缺失率、异常跳变,结果层的逐笔收益集中度、参数敏感性扫描,都适合写成脚本定期跑。策略层的未来函数检查最难自动化(需要理解信号语义),目前最可靠的仍是对比「延后一期」的结果。注意本机环境的实测限制:marketconstants.py 里 Windows 的回测线程数默认是 1,大规模参数扫描在 Windows 上会很慢——这一条见报错与版本边界

下一步怎么走:先把数据与口径说清

大部分「结果不可信」的根源在数据这一层:数据从哪来、用了什么字段、怎么对齐。先把取数与凭证这条线理顺。