FinMarketPy / 回测全链路
finmarketpy 回测:四步链路、九个内置指标与一张参数表
官方最短路径只有四步:MarketDataRequest 描述数据 → fetch_market 取回行情 → TechIndicator.create_tech_ind 生成信号 → Backtest.calculate_trading_PnL 跑回测,最后 TradeAnalysis 出统计。看上去简单,但决定结果量级的是 BacktestRequest 上那一串参数:成本写几个 bp、信号延后几天、杠杆上限多少、年化因子用 252 还是别的——这些官方 README 没有列表,本站从 0.11.19 源码里抽出来整理成一页。
finmarketpy_examples/backtest_example.py 与 backtest/ 源码);示意非官方流程图,参数与行为以仓库代码为准。Backtest 和 TradingModel 有什么区别:先决定用哪一个
同一份源码里有两套入口,源码实测:Backtest 是底层类(直接吃信号 DataFrame),TradingModel 是在它之上的模板(负责装载参数与统一流程)。
| 对比项 | Backtest(底层类) | TradingModel(高层模板) |
|---|---|---|
| 信号从哪来 | 你自己传入一个 signal DataFrame | 在类内部按 tech_ind 与 TechParams 生成,或由子类实现 |
| 参数怎么给 | 方法参数里直接传 br(BacktestRequest) | load_parameters(br) 装载,另有 load_assets(br=None)、save_model(path) / load_model(path) |
| 核心调用 | calculate_trading_PnL(br, asset_df, signal_df, contract_value_df) | 模板方法 construct_strategy(br=None, …),内部再调底层 |
| 适合场景 | 信号来自外部(你自己算的因子、外部指标库、事件信号) | 标准的技术指标策略,想少写重复代码 |
| 官方示例 | backtest_example.py(G10 趋势跟踪、EURUSD 单品种) | tradingmodelfxtrend_example.py、tradingmodelfxtrend_bbg_example.py |
| 注意点 | 四个位置参数顺序固定:br, asset_df, signal_df, contract_value_df | 模型可序列化保存,便于把一次实验的参数存档 |
signal_df 传进引擎——引擎不会替你检查信号是否用了当日收盘后才有的信息。源码里内置指标的实现是带滞后处理的(例如 SMA 分支用 data_frame.shift(1).rolling(window=sma_period-1).sum() 再加当日值),但你自己写的信号没有这层保护。核对方法见回测可信度体检。四步链路怎么走:官方最小示例逐段拆解
下面的代码全部来自官方 backtest_example.py(0.11.19 发布包),只做顺序整理与注释,没有改写逻辑。
取数:MarketDataRequest + Market.fetch_market
md_request = MarketDataRequest(start_date="01 Jan 1989", finish_date=datetime.date.today(), freq='daily', data_source='alfred', tickers=tickers, fields=['close'], vendor_tickers=vendor_tickers, vendor_fields=['close'], cache_algo='internet_load_return', fred_api_key=fred_api_key),然后asset_df = Market(market_data_generator=MarketDataGenerator()).fetch_market(md_request)。此时asset_df就是后面回测的行情底表。声明回测口径:BacktestRequest
br = BacktestRequest()后赋值:br.start_date/br.finish_date、br.spot_tc_bp = 2.5(2.5bp 买卖价差)、br.ann_factor = 252;波动率目标组:br.signal_vol_adjust = True、br.signal_vol_target = 0.05、br.signal_vol_max_leverage = 3、br.signal_vol_periods = 60、br.signal_vol_obs_in_year = 252、br.signal_vol_rebalance_freq = 'BM'、br.signal_vol_resample_freq = None。生成信号:TechIndicator + TechParams
tech_params = TechParams(); tech_params.sma_period = 200; indicator = 'SMA';tech_ind.create_tech_ind(spot_df, indicator, tech_params);signal_df = tech_ind.get_signal()。示例里还可以加tech_params.only_allow_longs = True做单边约束。执行回测:calculate_trading_PnL
backtest.calculate_trading_PnL(br, asset_df, signal_df, contract_value_df=None, run_in_parallel=False)。随后三个取结果的方法:portfolio_cum()(组合累计曲线)、portfolio_signal()(各品种持仓)、pnl()(逐品种 PnL);portfolio_pnl_desc()可取组合描述文字,常用来拼图表标题。统计与出图:TradeAnalysis + chartpy
统计:
TradeAnalysis().run_strategy_returns_stats(trading_model);逐笔交易收益可用 findatapy 的Calculations().calculate_individual_trade_gains(signals, returns)。出图:Style()上设title、source、scale_factor = -1、file_output、auto_scale = True,再Chart().plot(port, style=style)。
DEXUSEU 一类序列代码。示例注释同时提醒:这里是用现货价算收益,更好的做法是用包含 carry 的外汇总收益指数。内置哪些技术指标(9 种)与它们的参数
官方 README 没有列指标清单。下表由 economics/techindicator.py 源码实读得出:create_tech_ind(data_frame_non_nan, name, tech_params, data_frame_non_nan_early=None),按 name 分派。
| name 取值 | 含义 | 对应参数 | 信号形态 | 注意点 |
|---|---|---|---|---|
SMA | 简单移动平均 | sma_period | 价格与均线的关系决定多空 | 实现里对历史部分用了 shift(1) 滚动求和,属防未来函数设计 |
SMA2 | 双均线组合 | sma2_period | 短均线相对长均线 | 官方示例里常用 200 周期单均线,双均线要自己设参数 |
EMA | 指数移动平均 | ema_period | 同 SMA 逻辑 | 对近期价格更敏感,换手通常更高 |
ROC | 变动率(动量) | roc_period | 按动量方向 | 典型动量策略的入口 |
GMMA | 顾比复合移动平均 | gmma_period_list | 多组均线的排列 | 参数是列表,需按源码要求传一组周期 |
RSI | 相对强弱指标 | rsi_period、rsi_lower、rsi_upper | 超买/超卖,可设上下阈值 | 源码注释写的是「RSI 退出超买时买、退出超卖时卖」——阈值语义要按注释理解 |
BB | 布林带 | bb_period、bb_mult | 价格与上下轨的关系 | 倍数参数决定带宽,直接影响信号频率 |
ATR | 真实波动幅度 | atr_period | 常与止损/止盈配合 | 更像风控输入而非方向信号 |
VWAP | 成交量加权均价 | —(按数据字段) | 价格相对 VWAP | 需要数据里有成交量字段,仅取收盘价会缺输入 |
通用参数:fillna
tech_params.fillna 控制是否对输入做前向填充(源码里走 ffill())。行情有缺口时它会改变信号序列,回测结果也会跟着变——所以这一项要写进你的可复现记录。
通用参数:signal_mult
信号倍数,用来放大或缩小信号强度;配合 only_allow_longs / only_allow_shorts 可以限制交易方向。单边约束会显著改变回测的换手与回撤结构。
通用参数:strip_signal_name
控制信号列名的处理方式。当你要把多个信号拼成一张表、再用 BacktestComparison 对比时,列名规则不一致会增加拼表成本,建议统一。
BacktestRequest 常用参数有哪些:决定结果量级的那一组
BacktestRequest 继承自 findatapy 的 MarketDataRequest,在 0.11.19 里有近 60 个属性。全部列出没有意义,下面按「不设就一定会错」的顺序挑出关键组。
| 参数组 | 代表属性 | 作用 | 示例值 | 不设的后果 / 注意点 |
|---|---|---|---|---|
| 表本身 | trading_field | 指定用哪个价格字段参与回测 | close | 用错字段(收盘价 vs 结算价)会让结果整体偏移 |
| 交易成本 | spot_tc_bp(现货成本 bp)、spot_rc_bp(roll 成本 bp) | 把买卖价差与展期成本折进收益 | 2.5 | 不设成本=零摩擦回测,趋势类策略会被系统性高估 |
| 信号时点 | signal_delay | 信号生效前延后几个周期 | 按数据频率设 | 不延后=当日信号当日成交,容易构成未来函数 |
| 波动率目标(逐信号) | signal_vol_adjust、signal_vol_target、signal_vol_max_leverage、signal_vol_periods、signal_vol_obs_in_year、signal_vol_rebalance_freq、signal_vol_resample_freq | 按估计波动率把仓位缩放到目标风险 | 0.05 / 3 / 60 / 252 / 'BM' | 不设最大杠杆,低波动期仓位会被放得极大 |
| 波动率目标(组合层) | portfolio_vol_* 同名一组 + portfolio_vol_resample_type | 对组合整体做目标波动控制 | 同上结构 | 逐信号与组合层同时开要清楚谁是外层约束 |
| 仓位与敞口 | max_net_exposure、max_abs_exposure、position_clip_*、portfolio_notional_size、portfolio_combination_weights | 限制净敞口/总敞口、裁剪单次调仓、定义名义规模与组合权重 | 按资金规模设 | 不裁剪会出现不现实的满仓换手 |
| 风控开关 | stop_loss、take_profit | 止损/止盈 | 按波动幅度设 | 开了它们之后交易笔数与胜率口径都会变 |
| 统计口径 | ann_factor、resample_ann_factor、cum_index | 年化因子与累计方式(乘法/加法) | 252、'mult' | 年化因子与数据频率不匹配会让年化收益/波动率全错 |
| 输出开关 | calc_stats、write_csv、write_csv_pnl、plot_interim、include_benchmark、plot_start / plot_finish | 是否算统计、是否落 CSV、是否画中间图、是否带基准、绘图区间 | 按需 | 不开基准就没法判断「策略是在赚钱还是在跟风」 |
跑完之后:统计、报告与对比
回测只给你净值,判断好坏要靠这三个输出工具。
| 工具 | 方法 | 产出 | 适合回答的问题 | 注意点 |
|---|---|---|---|---|
TradeAnalysis | run_strategy_returns_stats(trading_model, engine="finmarketpy") | 收益统计表 | 这个策略的收益、波动、回撤大致什么水平 | 统计口径由 BacktestRequest 决定,换口径要重跑 |
TradeAnalysis | run_excel_trade_report(trading_model, excel_file='model.xlsx') | Excel 文件 | 要把结果交给别人看 / 存档 | 默认文件名就是 model.xlsx,注意别覆盖上一轮结果 |
TradeAnalysis | run_day_of_month_analysis(trading_model, resample_freq='B') | 按日/按月的分解 | 收益是不是集中在某几天(月末效应、数据发布日) | resample_freq 用 pandas 频率串,写错会静默给出不同分组 |
BacktestComparison | plot_pnl(diff=True, silent_plot=False, reduce_plot=True) | PnL 对比图 | 改一个参数后,策略差异出在哪 | diff=True 看差值,False 看两条曲线 |
BacktestComparison | plot_sharpe(silent_plot=False, reduce_plot=True) | Sharpe 对比图 | 风险调整后哪个更好 | Sharpe 依赖年化因子与收益频率,比较前先确认两边口径一致 |
findatapy Calculations | calculate_individual_trade_gains(signals, returns) | 逐笔交易收益 | 收益是不是靠少数几笔撑起来的 | 这是判断「结论是否稳健」最直接的一步 |
| chartpy | Chart().plot(port, style=style) | 图片文件 / 交互图 | 要看曲线形态 | 出图失败通常是 chartpy 引擎或字体问题,不是回测问题 |
回测链路常见问题
类名、方法与参数以 0.11.19 源码与官方示例为准;本站未实机运行。
我没有 FRED key,能不能跑官方示例?
官方 backtest_example.py 默认用 data_source='alfred',需要一把 免费的 FRED API key(在 FRED 网站申请)。没有 key 时最简单的替代是自己准备一份 DataFrame 行情表,绕过取数步骤直接调用 calculate_trading_PnL;也可以改用 findatapy 支持的其它免费源。具体可用源与凭证路径见数据源与凭证。
回测里怎么算「交易成本」才算合理?
官方示例给的是 br.spot_tc_bp = 2.5,也就是按 2.5 个基点计买卖价差,另有 spot_rc_bp 用于展期成本(外汇远期持有成本)。合理值取决于你的品种与频率:日频 G10 现货用个位数 bp 量级、日内高频要更高。本站不给具体推荐值——写清你用了多少、为什么用这个值,比选一个「标准值」更重要。
回测结果里出现非常大的杠杆,正常吗?
先检查波动率目标这一组参数。signal_vol_adjust=True 时,仓位由「目标波动率 ÷ 估计波动率」决定,如果没设 signal_vol_max_leverage,低波动期算出的仓位会非常大。官方示例里设的是 3。此外 max_net_exposure / max_abs_exposure 与 position_clip_* 也参与约束,这一组要一起看。
我想用 RSI 或布林带,需要额外装指标库吗?
不需要。TechIndicator 内置 9 种:SMA、SMA2、EMA、ROC、GMMA、RSI、BB、ATR、VWAP,参数集中在 TechParams(如 rsi_period、rsi_lower、rsi_upper、bb_period、bb_mult)。注意 RSI 的阈值语义——源码注释写的是「退出超买时买、退出超卖时卖」,和常见教科书写法不完全一样,用之前先读一眼实现。
怎么把两个策略放在一张图上比?
用 BacktestComparison:plot_pnl() 看 PnL 差异(diff=True 看差值曲线),plot_sharpe() 看风险调整后的对比。前提是两次回测的统计口径一致(年化因子、成本、信号延迟都要写同样的值),否则比出来的是「口径差异」而不是「策略差异」。这也正是本站强调先做口径体检的原因。
它能不能做参数敏感性扫描(比如 SMA 从 50 到 300)?
源码与发布说明里能看到并行与敏感性相关的能力:calculate_trading_PnL 带 run_in_parallel 参数,TradeAnalysis 也有针对性的分析入口,官方发布说明里提到过为回测与敏感性分析加并行特性「在 Linux 上表现更好」。但要注意 marketconstants.py 里 backtest_thread_no 的平台默认值是 linux: 8 / windows: 1 / mac: 8——Windows 下默认只有 1 个线程,大规模扫描会很慢,详见报错与版本边界。
回测出来的收益能代表以后吗?
不能。回测只说明「在这段数据、这套参数、这个成本假设下会发生什么」。它与实盘之间还差着滑点、容量、数据可得性、执行时点等一堆现实因素。本站不提供任何收益预期,也不评价第三方文章的具体数字;看到数字时请按体检清单自己复现一遍再采信。