FAQ¶
常见问题和坑。如果碰到本页没覆盖的,开个 issue。
回测¶
为什么我的策略跑不过 Buy & Hold?¶
大多数简单策略都跑不过。Buy & Hold 以零佣金、零择时风险捕获标的的趋势。要击败它,你的策略需要满足以下之一:
避开回撤 —— 在最差的时段空仓(择时)。
增加分散 —— 即便每个资产收益和 B&H 接近,多资产也能降低方差。
使用杠杆 —— 通过
margin_ratio或仓位规模显式实现。有真正 edge —— 做空收益、均值回归等。
如果你的策略 Sharpe 为正但跑不过 B&H,作为组合的一个分量仍可能有价值(和被动多头不相关)。
怎么避免未来函数?¶
QTrade 在结构上阻断最常见的一种:self.data(和 self.data_by_asset[asset])被切到 [:current_time],所以你在 on_bar_close 里根本读不到未来的 bar。
未来函数仍可能从这些地方溜进来:
在
prepare()里你能访问完整 DataFrame。如果你用了未来信息计算指标(比如df['Close'].shift(-1)),on_bar_close会读到它。Limit / stop 成交假设价格在 bar 内确实触及你的价位。对流动性差的资产,这高估了成交概率。
trade_on_close=True让市价单在当前 bar 的收盘价成交 —— 也就是你计算信号使用的那根 bar。对收盘类信号是合理的,对其他信号实际上等于作弊。不确定的话设trade_on_close=False,让成交发生在下一根 bar 的开盘。
trade_on_close=True 和 False 有什么区别?¶
设置 |
在第 N 根 bar 的 |
|---|---|
|
第 N 根 bar 的收盘价(同一根 bar) |
|
第 N+1 根 bar 的开盘价 |
True 适合信号确实在收盘时到达的回测(比如你用收盘价计算)。False 更接近现实,因为信号在实盘中通常需要一个 tick 才能行动 —— 偏保守,没特别理由就用这个。
我的策略一笔交易都没下,为什么?¶
几个常见原因:
开头的指标是 NaN —— 前
window_size根 bar 是热身。Backtest._start_idx自动跳过它们;如果数据比热身期还短,就一笔交易都没有。available_margin是 0 ——margin_ratio < 1允许你加杠杆,但堆叠交易后available_margin缩水很快。降低 size 或调高 margin_ratio。size=0静默无效 ——Order(size=0)现在会抛错(好事!),但如果你用size = available_margin // close算出来 size=0(现金太少),默认的Strategy.buy()会触发 Order 构造时报错。
为什么 SL/TP 触发的价格和我设的不一样?¶
SL/TP 在 bar 的 low(多头止损)或 high(多头止盈)触及你设置的价位时触发。成交价记录的是你设置的 SL/TP 价位,不是 bar 的收盘价。所以你设 sl=99、bar 跌到 low=98,成交价被记录为 99,哪怕这根 bar 收盘在 102。
注意:通过
Order(stop=...)下的普通 stop 订单目前在 bar 收盘价成交,而不是触发价 —— 这是已知的真实度差距,留作未来修复。挂在 Trade 上的 SL/TP 按上面描述工作。
参数优化¶
为什么 optimize() 里看起来很美的策略实盘很差?¶
样本内过拟合。optimize() 在同一份数据上既挑参又测试 —— 这是教科书级别的「拟合噪声」配方。
改用 walk_forward_optimize —— 详见 Walk-forward 优化。每组参数只在策略从未调过参的数据上评估。
如果 walk-forward 的 Sharpe 远低于 optimize 的 Sharpe,差距就是你的过拟合程度。很常见、很正常 —— 把 walk-forward 数字当成实际表现。
怎么选择 maximize 指标?¶
Total Return [%]:简单,但完全不惩罚风险。回撤巨大的策略可能胜出。
Sharpe Ratio:最常见的默认选择。适合接近正态的收益分布。
Sortino Ratio:适合非对称 / 厚尾收益(趋势跟随)。
Calmar Ratio:奖励平滑的净值曲线;适合关心回撤体验的场景。
没有特殊理由就从 Sharpe 开始。
我的网格搜索太慢了。¶
两条路:
缩小网格 —— 多数策略只有一两个真正重要的参数;每个参数 4–6 个候选值通常够。不要把所有东西都细粒度扫一遍。
改用 vectorbt —— 上千参数组合时,QTrade 的事件循环不是合适工具。详见 COMPARISON.md。
多资产¶
怎么对齐不同来源的数据?¶
QTrade 要求每个资产的 DataFrame 共享完全一致的 DatetimeIndex。传入前用 pandas 在索引上做 join:
common = aapl.index.intersection(msft.index)
data = {"AAPL": aapl.loc[common], "MSFT": msft.loc[common]}
对于有缺口的数据(公司行为、流动性差的日子),你可能更想要 union + 前向填充 —— 但小心:前向填充的 bar 会让没有的流动性看起来像有。
可以为每个资产设置不同佣金吗?¶
暂不支持。Commission 是 broker 共享的单一实例。如要近似按资产收费,在下单前手动在策略逻辑里加上资产特定的滑点。
为什么多资产模式下 self.position 会抛错?¶
因为没有单一 Position 可返回 —— 每个资产一个。改用 self.positions["AAPL"](dict 访问)。self.data 同理 → self.data_by_asset["AAPL"]。
强化学习¶
我的 agent 学会了什么都不做(reward = 0)。¶
默认的 RewardScheme 只在有交易平仓的 bar 给已实现盈亏的奖励。如果 agent 从不平仓,reward 一直是 0,梯度信号过于稀疏。
试试以下方案之一:
基于权益的奖励,每根 bar 都触发(详见自定义交易环境)。
在 episode 结束时强制平仓的 action scheme(保证交易最终都关闭)。
增加 episode 数量 —— 稀疏奖励 + 长 episode 很难学。
env.step() 返回的 info 里有什么?¶
TradingEnv.step() 的每步 dict:
键 |
含义 |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
所有已平仓交易盈亏之和 |
|
平均 exit_index − entry_index(按 bar 数) |
|
|
is_success 是给 SB3 EvalCallback 的成功率日志用的。
random_start=True 报了一条看不懂的错误。¶
v0.3.0 已修复 —— 现在 len(data) <= window_size + max_steps 时抛 ValueError。默认 max_steps=3000 需要至少 ~3000 根 bar;数据较短就调小这个值。
杂项¶
怎么把回测结果保存起来稍后用?¶
没有内置序列化,但 broker 持有一切。Pickle 是简单粗暴的保存方式:
import pickle
with open("run.pkl", "wb") as f:
pickle.dump(bt.broker, f)
如果想要更稳健(跨版本安全),先把需要的内容拉成 DataFrame:bt.broker.equity_history、bt.get_trade_history()、calculate_stats(bt.broker)。
怎么提 bug?¶
https://github.com/gguan/qtrade/issues。最小复现 + QTrade 版本号(qtrade-lib --version 不存在;用 pip show qtrade-lib)会帮上大忙。