↓ 跳过正文
  1. Posts/

预测正确,策略为何仍可能亏钱?——从 Markout 到执行感知回测

目录

上一篇写到,预测对价格方向,不等于交易能赚钱。主动成交要付执行成本,挂单等成交又可能筛出对自己不利的行情。

顺着这个问题往下想,我还有一个疑问:如果把手续费算进去,回测里也只让一部分挂单成交,是不是就离真实收益很近了?

后来发现,“一部分”还不够。成交的是哪些订单,以什么价格、多少数量成交,以及成交之后价格怎么走,都需要各自的依据。执行感知回测对我来说,就是把这些假设从收益曲线背后拿出来检查。

预测之后,我还要选一次怎么成交
#

换一组盘口,后面的数字都从这里出发,价格单位是 USDT/BTC:

1Best Bid = 100,000
2Best Ask = 100,010
3Mid      = 100,005

模型预测未来 10 秒 Mid 上涨 5 bps,换成价格约为 50 USDT。这里完整 Spread 是 10 USDT,相对 Mid 约 1 bp;Half-Spread 是 5 USDT,约 0.5 bp。

看起来预测空间比 Spread 宽裕。如果在 Ask 买入,单看入场就比 Mid 多付 5 USDT;如果在 Bid 挂买单并成交,则比当时 Mid 少付 5 USDT。两者的成交价相差 10 USDT,但后者要等卖方来。

这里的“上涨 5 bps”如果只是模型的点预测,还带着预测误差。即便为了讨论先假设它准确,也只能确定终点的 Mid,不能确定中途有没有足够的卖单来到 Bid。起点和终点相同的两段行情,可以经过完全不同的成交路径,挂单得到的结果也会不同。

我最初容易把“预测涨幅够不够”和“挂单能不能成交”合成一道判断。其实,即使这次预测完全兑现,Bid 上的买单也可能一直没成交;即使价格没有上涨,买单也可能先被卖方打中。

Taker 买入 Ask;主动卖单消耗 Bid 队列后与 Maker 买单成交

图里 Maker 成交价为 100,000,Mid 仍然可以是 100,005。只要这笔卖单没有耗尽最佳 Bid,且 Ask 没变,成交后盘口仍是 100,000/100,010。最近成交价与盘口中点可以同时不同,这和订单簿那篇里的价格定义是一致的。

当然,Taker 也有盘口变化、延迟和深度不足的问题。上面的 Ask 买入只假设订单足够小,抵达时该档仍有足够数量;它不能保证任意数量都按这个价格成交,更不能保证平仓收益。

成交把哪些信号留了下来
#

前文用了一个构造例子,我把它压缩成表格:每个信号都预测上涨,正确与否按相同的未来窗口判断,每单数量相同,成交均视为整单成交。

信号结果信号数成交数
预测正确6010
预测错误4030
合计10040

全量准确率是 60%,成交子集只有 25%。这组数字只是说明筛选机制,不是对 BTC 的实证估计;准确率也还没有计入涨跌幅度和费用。

为了把这个问题和收益接上,我把信号时刻记为 \(t_s\),固定预测窗口为 \(H\),定义方向调整后的参考收益:

$$ R = s\frac{m_{t_s+H}-m_{t_s}}{m_{t_s}} $$

其中 \(m\) 是 Mid,买入信号 \(s=+1\),卖出信号 \(s=-1\)。这里的 \(R\) 衡量信号之后的行情,还没有放入成交价。Filled 表示该信号对应的订单在预定有效期内获得成交,于是可能有:

$$ E[R\mid\mathrm{Filled}] \neq E[R] $$

成交改变了观察收益时的条件。 不是给原始预测收益乘一个成交率,就自动得到了可实现收益;部分成交时,还得考虑成交数量如何与后续行情关联。

如果只统计被保留信号的参考收益,把未成交记为零、成交数量固定,那么平均值可以写成成交概率乘以成交条件下的平均收益。这个乘法里需要的是 \(E[R\mid\mathrm{Filled}]\),并不是全量的 \(E[R]\)。它仍然只是信号层面的统计,变成交易收益还要接上实际入场时刻和成交价。前面的表格正好说明,替换掉这个条件会怎样改变判断。

还有一种时间上的错位:预测的是从信号出现开始的 10 秒,订单却在第 9 秒才成交。原先预期的价格变化可能已经发生得差不多了,剩余持仓机会也变了。我会同时保留信号时间和成交时间,而不是用成交后的表现悄悄替换原来的预测标签。

为什么有人愿意主动卖给 Bid 上的我?如果他认为旧报价偏高,等大家更新报价以后可能卖得更低,那么现在支付 Spread 换取及时成交就有意义。信息也有时效性,执行速度会影响它能换到多少收益。但普通调仓、现金需求同样会产生主动卖单,不能仅凭交易方向断定对方有信息优势。

我也得把库存单独想一遍。买单成交会增加多头敞口;连续成交可能触发仓位上限,改变后续报价,甚至迫使策略主动减仓。库存带来的价格风险和清仓成本,不能全部归到信息不对称上。

买得便宜,为什么后续走势仍可能不利
#

我第一次看到 Markout 时,容易把它理解成“成交后的收益”。这个说法少了参照物,也少了是否真的卖出这一层。

Fill 是一笔实际成交,有时间、价格、方向和数量。Mark 是用于估值的参考价格,这里选同一市场的 Mid。Markout 则是在成交后固定时间窗口,比较参考价格与某个起点的差异。不同资料的起点、符号和归一化方式可能不同,横向比较前要先对齐定义。

记成交时刻为 \(t_f\),成交价为 \(p_f\),买入取 \(s=+1\),卖出取 \(s=-1\)。本文用下面这个 Trade-to-Mid 口径,正值表示后续 Mid 对成交方有利:

$$ M_{\mathrm{trade}}(\Delta)=s\frac{m_{t_f+\Delta}-p_f}{p_f}\times10^4\quad\text{bps} $$

窗口从成交时刻开始,可以分别取 1s、10s、60s。它和前面从信号时刻开始的 10 秒预测窗口不是同一个时钟:挂单等了 3 秒才成交,就不能把两个终点当成同一时刻。

接着用一笔 1 BTC 买入拆数字。100,000 成交,成交时 Mid 为 100,005,10 秒后 Mid 为 100,002。先按每 BTC 的价格差分解:

$$ m_{t_f+10s}-p_f=(m_{t_f}-p_f)+(m_{t_f+10s}-m_{t_f})=5+(-3)=2 $$

初始成交价相对 Mid 的优势是 +5 USDT,成交后 Mid 的变化是 −3 USDT,合起来仍有 +2 USDT。成交价有优势,与成交后走势不利,可以同时成立。

成交与持仓事件、盘口参考价格的双轨记录,以及三个价格差的测量关系

这一笔 10 秒 Trade-to-Mid Markout 为 \(2/100{,}000\times10^4=+0.2\) bps。若改看从成交时 Mid 出发的 Mid-to-Mid,则是:

$$ M_{\mathrm{mid}}(10s)=\frac{100{,}002-100{,}005}{100{,}005}\times10^4\approx-0.3000\quad\text{bps} $$

两个比例的分母不同,不能直接拿它和以成交价为分母的初始优势相加。前面的价格差分解是精确的;如果要在 bps 中相加,就得统一分母。

卖出方向也可以顺手核对一下:若在 100,010 卖出,之后 Mid 降到 100,002,价格差原本是 −8,乘上卖出的符号 −1,Markout 就变成正值。这样一来,买卖两边都能用“正值对成交方有利”来读,避免把买单和卖单直接混在一起求平均,抵消了方向。

这个 +2 USDT 只是按 Mid 估值的毛收益。如果此时卖出,要面对实际 Bid 和可用深度,随后再扣费用。Markout 既适用于 Maker,也适用于 Taker;它能描述成交质量,却不是已经落袋的平仓收益,也不能单靠一个负值证明对手掌握了私有信息。

如果 10 秒后盘口是 Bid 99,997/Ask 100,007,Mid 同样为 100,002。此时若深度足够,按 Bid 主动卖出,1 BTC 的往返毛收益反而是 −3 USDT。按 Mid 看有 +2,按可执行价格平仓却为负,并没有矛盾。为了比较报价好坏,我会看 Markout;为了判断策略挣钱没有,我还要看真实退出方式。这也解释了为什么不能把一组 Markout 均值直接当成策略收益曲线。

IEX 的执行质量分析也使用成交后固定窗口的 Trade-to-Mid Markout,不过它讨论的是美股特定订单类型,并采用自己的归一化口径。我借用的是测量思路,没有把其中的统计结论搬到 BTC。

历史价格碰到了,为什么还轮不到我
#

如果回测把下面这个判断直接当作买单全部成交,隐藏的假设就比较强了:

1if candle.low <= limit_buy_price:
2    filled = True

这只是待检查的简化规则。OHLCV 里的一根 K 线没有告诉我:低点发生时订单是否已到交易所,该价位成交了多少,以及我的单前面还有多少量。总成交量也不能替代某一个价位、某一段时间的可成交数量。

假设按价格优先、同价时间优先撮合,100,000 前面有 3 BTC,我挂买 1 BTC。随后只有 2 BTC 主动卖到这一档,没有撤单或其他事件。历史上确实在 100,000 成交了,但消耗的全是前方队列,我仍然一笔没买到。如果卖出量是 3.4 BTC,则轮到我的只有 0.4 BTC,不能记成整单成交。

我起初把触价和穿价放在一起,后来觉得这里值得区分。触及挂单价,只证明该价位发生过交易;严格穿过挂单价,是更强的成交证据。

比如同一市场的主动卖单已经成交到 99,999,而我的 100,000 买单此前已有效入簿、持续存续,并按价格优先参与连续撮合,那么更高价买单应当先被满足。但这仍以撮合规则、订单资格和事件顺序为前提;别的市场的低点、集合竞价价格,或者订单抵达前的穿价都不能拿来套用。回放加入新订单后,历史成交路径是否还能保持,也仍是一项假设。

即使拿到了逐档深度 L2,队列也不一定能复原。L2 主要提供每个价位的聚合数量;同样减少 1 BTC,撤掉的是我前面的 A,还是后面的 E,对我意义不同。

同价位六笔各 1 BTC 的订单,A 或 E 撤单产生相同 L2 总量变化,却导致不同成交结果

图中初始顺序是 A、B、C、我、D、E,每笔 1 BTC。A 撤单后,前方剩 2 BTC,随后主动卖出 3 BTC,我全部成交;E 撤单后,前方仍有 3 BTC,同样卖出 3 BTC,恰好只吃完前方,我没有成交。两种情况下,该价位总量都先从 6 变成 5,再变成 2 BTC。即使结合这笔成交量,也还缺少撤单位置。

L3 的逐笔订单信息能缩小这个缺口。例如 Coinbase 官方文档中,L2 更新给出价位数量,Full/L3 则有订单标识以及新增、成交、撤销等事件。不过“有 L3”也不等于知道全部真实队列:仍需完整快照和无缺失的事件序列,正确处理修改订单的优先级,并受限于可见订单范围和具体撮合规则。

查 HftBacktest 的成交模型文档时,我才把“缺信息”与“怎么补假设”联系起来。它给出的保守队列模型不让撤单或减量推动自己的位置,只用该价位的成交推进队列;概率模型则按队列位置估计减量落在前方或后方的概率。概率分配来自模型,不是恢复出了历史真相。

这份文档还提醒了回放的边界:模拟订单不会改变历史深度和成交。即使模型支持部分成交,也不代表能重建吃单后的市场反应。订单足够小、冲击可以忽略,是这类回放常用的前提;数量变大时,需要另行处理冲击,不能靠调整队列参数掩盖。

我会怎样检查成交假设
#

想到这里,我对“回测已经考虑成交率”的要求变具体了。一个总成交率,解释不了模型究竟在哪些订单上判断错了。

检查项我想确认的问题
成交订单比例有过成交的订单数/有效提交订单数,是否把部分成交算入?
成交量比例成交总数量/提交总数量,同一标的下是否大量只成交一小部分?
各窗口 Markout1s、10s、60s 的条件均值和分布如何,按笔还是按量加权?
实际平仓 Net PnL按真实买卖价格配对后,扣除费用及适用的持仓成本还有多少?
假设敏感性换队列模型、增加下单和撤单延迟后,收益判断是否改变?

撤单重挂会让“订单数”的分母变化,统计时也得固定口径。成交后的观察窗不足、行情缺失,不能悄悄记为零收益;同样不能只留下能算出 60 秒 Markout 的顺利样本。

如果有实盘日志可用于校准,我会按相近的信号强度、盘口深度、排队条件和订单大小比较,再看不同延迟下的成交时间与数量。总体成交率一样,仍可能是模型漏掉了好成交、补进了坏成交,刚好数量抵消。

未成交订单也要留下记录。只保留成交日志,能分析已成交的质量,却看不见模型把多少订单错误地判成未成交。一次提交到最后撤销的过程,至少应当能对应上订单何时被接受、每次部分成交及剩余数量;本地发出撤单请求,也不代表交易所已经停止撮合。

对于分批成交,我倾向于从每一笔 Fill 的实际时间和价格计算 Markout,再按明确的权重聚合。不能把第一笔成交时间当作整单时间,也不能用最后一笔价格替代整单价格。数量不同的两笔成交,按笔平均与按量平均本来就回答不同的问题。

例如一个订单成交 0.1 BTC,另一个成交 1 BTC,即使两笔 Markout 一正一负、数值恰好相反,按笔均值为零也不意味着这 1.1 BTC 的估值变化为零。只有把每笔的数量和价格基准接回去,才能算出金额。比较不同回测时,我会先确认这些统计是否用了同一套权重。

延迟本身也不只是“价格加一点滑点”。行情到达策略时已经多旧、订单何时真正入簿、撤单何时生效,会影响队列与风险暴露。尤其不能用下单之后才出现的整根 K 线低点,倒过来决定更早的执行选择。

还有一个我容易顺手接受的说法:成交率提高,总收益就会更高。

在一个受控的玩具实验里,这可以成立:原有成交保持不变,新增成交的期望净收益固定为正,各笔互不影响,也没有资金或仓位约束,那么新增成交确实增加累计预期收益。

但真实策略不满足这些条件。新增成交可能集中在下跌之前,还会占用资金、增加库存,改变后面的报价与平仓。所以保守队列模型虽然降低成交机会,却未必给出收益下界;乐观成交假设也未必给出收益上界。我会同时看成交质量和收益的变化,不能拿成交率给回测结果排好坏。

如果稍微增加延迟,或者换一个同样合理的队列假设,净收益就从正变负,我会把它理解为当前结论依赖执行假设,还缺少证据。反过来,多组假设下都盈利能提高信心,也不能代替校准:几组模型可能共享同一个遗漏,例如都没有考虑自己的订单对盘口的影响。

校准也不能变成对着同一段收益调参数。更合适的做法,是用一段日志检查成交误差,在另一个时间段继续验证。市场活跃程度变了,原先合理的排队概率可能失效;如果只挑最终赚钱的一组参数,执行模型同样会过拟合。

我会把预测模型和执行参数的版本一起记录下来。否则一次收益改善,可能来自信号变好,也可能只是成交判断放宽了。把这两种变化混在同一条曲线里,后面即使发现实盘与回测不一致,也很难追溯差异从哪里开始。

回到最初那个 BTC 信号
#

再看未来 10 秒上涨 5 bps 的预测,我现在会沿着这样一条概念链检查它:

预测 → 下单选择 → 成交筛选 → 成交质量 → 实际收益。

这是一条理解上的依赖关系,不是严格的运行时顺序。策略持仓期间还会继续预测、撤单和下单,反馈会回到前面。

SOXL 那篇让我把主动成交量和价格冲击分开看;这一篇又让我把价格预测和可实现收益分开看。两次都涉及订单簿如何承接交易,但回答的问题不同。

把它放回量化交易的系统框架,Alpha 预测只交出了一部分答案。预测质量不保证可执行的 Alpha,因为价格、等待时间和数量共同决定能拿到什么头寸;成交改变条件收益分布,全量信号的表现不能直接代替成交子集;执行模型也是需要校准的模型,队列和延迟假设值得和预测模型一起接受检验。

这里的构造例子还不能给队列参数定值。但至少再看到一条漂亮的回测曲线时,我知道该追问哪一层假设,而不只是继续调整预测模型。

参考资料
#

相关文章

从价格发现到流动性螺旋:股票市场为什么需要“国家队”?

在很长一段时间里,我对股票市场的理解非常机械,甚至带着一种理所当然的工程偏见。 作为一个长期和分布式系统、性能优化与反馈控制打交道的工程师,我习惯把撮合引擎看作一个纯粹的分布式信息处理系统:全市场的参与者带着各自的信息、算力和风险偏好,用真金白银在订单簿上挂单出清。价格上涨,说明对未来的盈利预期转好;价格下跌,说明基本面恶化或风险溢价上升。

大量主动卖出,股价为什么不跌?——从 SOXL 成交统计理解流动性与价格冲击

10月16日,美东时间上午,我在看 SOXL 的成交统计时看到了一组让我反复盯着屏幕的数字。 SOXL 是 3 倍做多半导体 ETF,那天跌了超过 6%。我打开成交统计,本来预期应该看到一个主动卖出量远超买入量的页面——毕竟已经跌成这样了。结果是这样的:

从成交量到资金净流入:重新理解股票价格是如何形成的

之前写量化交易系统那篇时提到过 execution 层——每笔 order 多大、对手盘的流动性够不够、是挂盘口还是主动吃价差。当时一笔带过,没有认真想 order book 这一层到底怎么运转。 最近第一次认真看市场微观结构(market microstructure)的一些材料,想搞清楚几个一直模模糊糊的问题。结果发现,不是模糊,是错的。