第二十五章:高级话题六:做市策略的源码解读与复现
做市策略这东西,光看论文和公式是远远不够的。我见过太多人拿着白皮书里的数学模型,往实盘里一丢,结果亏得连手续费都赚不回来。为什么?因为理论到代码之间,隔着无数个坑。
今天我们就来干一件实在事——把一套经典的做市策略源码,从头到尾拆开揉碎,然后亲手复现一遍。嗯,这章可能会有点长,但相信我,看完你就能自己动手改策略了。
25.1 选哪套策略来解读?
我个人习惯,教学用的策略不能太复杂,但也不能是玩具。太简单的策略(比如固定价差)学了没用,太复杂的(比如深度强化学习)又容易劝退。
我选的是 Avellaneda-Stoikov 做市模型 的一个变体。为什么选它?
- 它有一个清晰的数学框架,不是黑盒
- 它在高频做市和低频做市之间找到了平衡点
- 我在实盘里跑过这个策略的改进版,效果还不错
说白了,这是一个「能打」的策略,不是纸上谈兵。
25.2 核心逻辑:先看懂数学,再看代码
Avellaneda-Stoikov 模型的核心思想其实很简单:根据库存风险动态调整报价。你想想看,如果你手里已经囤了一堆货,还敢继续低价卖吗?肯定要抬高卖价,压低买价,先把库存消化掉。
数学上,它用了一个公式来计算最优报价偏移量:
δ_b = (1/2) * γ * σ² * T + (1/γ) * ln(1 + γ/q)
δ_a = (1/2) * γ * σ² * T + (1/γ) * ln(1 + γ/q)
别被公式吓到。我来翻译成人话:
- γ:你的风险厌恶系数。越大越保守
- σ:波动率。市场越动荡,价差越大
- T:持仓时间。你打算拿多久
- q:当前库存。货越多,报价越狠
我在项目中遇到过一个问题:很多人直接把公式抄进代码,结果发现报价偏移量算出来是负数。为什么?因为 ln(1 + γ/q) 在 q 很大的时候会变成负值。嗯,这里要注意,实际实现时通常要加一个下限保护。
25.3 源码结构:从数据到订单
我们来看一段简化后的核心代码。这是策略引擎的骨架:
class AvellanedaStoikovMM:
def __init__(self, gamma=0.1, sigma=0.02, T=60, q_max=100):
self.gamma = gamma # 风险厌恶系数
self.sigma = sigma # 波动率估计
self.T = T # 持仓时间窗口(秒)
self.q_max = q_max # 最大库存限制
self.position = 0 # 当前持仓
self.mid_price = 100.0 # 最新中间价
def calculate_spread(self):
"""计算最优价差"""
spread = self.gamma * self.sigma**2 * self.T + \
(2/self.gamma) * np.log(1 + self.gamma/self.q_max)
return max(spread, 0.01) # 最小价差保护
def calculate_quote_prices(self):
"""计算买卖报价"""
spread = self.calculate_spread()
half_spread = spread / 2
# 库存调整因子
inventory_skew = -self.position / self.q_max * half_spread
bid_price = self.mid_price - half_spread + inventory_skew
ask_price = self.mid_price + half_spread + inventory_skew
return bid_price, ask_price
这段代码看起来简单,但有几个关键点我要强调:
- 最小价差保护:我吃过亏,有一次波动率突然归零,价差算出来只有0.0001,结果被交易所当垃圾订单给拒了。从那以后,我所有策略都加了最小价差保护。
- 库存调整方向:注意 inventory_skew 的符号。持仓为正(做多)时,bid_price 要降低,ask_price 也要降低——说白了就是不想再买了,想赶紧卖出去。
- 中间价的更新:这个 mid_price 怎么来?我建议用加权中间价,而不是简单的 (bid+ask)/2。为什么?因为盘口深度不一样,浅的那一边更容易被吃掉。
核心要点:做市策略的本质不是预测价格涨跌,而是管理库存风险。你赚的是买卖价差,亏的是库存方向不对。所以一切调整,都围绕「如何让库存回到安全区间」来设计。
25.4 复现步骤:从零开始搭一套回测
好,理论讲完了,我们动手复现。我会用 Python + Pandas 来做,因为这是最通用的组合。
第一步:准备数据。你需要的是 Level 2 行情数据,至少包含:
- 时间戳(精确到毫秒)
- 买一价、卖一价
- 买一量、卖一量
- 最新成交价
第二步:实现策略逻辑。上面那段代码就是核心,但你需要加上订单管理:
def on_tick(self, tick_data):
# 更新中间价
self.mid_price = (tick_data['bid_price'] + tick_data['ask_price']) / 2
# 计算报价
bid, ask = self.calculate_quote_prices()
# 检查是否要撤单(如果价格变化太大)
if abs(self.current_bid - bid) > self.min_tick:
self.cancel_order('bid')
self.place_order('bid', bid, self.order_size)
if abs(self.current_ask - ask) > self.min_tick:
self.cancel_order('ask')
self.place_order('ask', ask, self.order_size)
# 更新持仓
self.update_position(tick_data)
第三步:回测引擎。这里我建议用事件驱动的方式,而不是向量化回测。为什么?因为做市策略的订单是逐笔成交的,向量化会忽略很多细节。
小技巧:回测时一定要模拟订单簿的撮合逻辑。我曾经用「看到成交价就认为成交了」的简化方式,结果回测收益翻了一倍,实盘直接打脸。真实情况是:你的订单可能排在队列后面,根本成交不了。
25.5 避坑指南:我踩过的三个大坑
做市策略的复现,坑比想象的多。我挑三个最典型的说说:
- 手续费没算对:做市策略的利润本来就薄,手续费稍微算错一点,结果就从盈利变成亏损。我曾经用「万分之二」的手续费率回测,结果实盘是「万分之二点五」,差了0.5个基点,一年下来亏了十几万。
- 滑点被忽略:回测时假设每次都能以报价成交,但实盘里你的订单可能只成交一部分,或者成交价滑了几个 tick。我建议回测时至少加 0.5 个 tick 的滑点。
- 波动率估计太粗糙:很多人直接用历史波动率,但市场环境一变,参数就失效了。我后来改用 EWMA(指数加权移动平均)来实时估计波动率,效果好了很多。
警告:不要用「全成交」假设来回测做市策略。真实市场中,你的订单可能只有 30%-50% 的成交率。如果你回测时假设 100% 成交,那结果就是虚高的。
25.6 可视化:看看策略到底在干什么
光看数字不够直观。我习惯把策略的报价轨迹画出来,一眼就能看出问题。
下面这张 SVG 图展示了策略在一天内的报价行为:
你看,当库存增加时(橙色区域),卖报价和买报价都往下移——这就是库存调整在起作用。策略在主动降低报价,吸引对手方来成交,把库存降下去。
25.7 参数调优:别迷信默认值
Avellaneda-Stoikov 模型有三个关键参数:γ、σ、T。默认值不一定适合你的市场。
| 参数 | 默认值 | 调优方向 | 我的经验 |
|---|---|---|---|
| γ(风险厌恶) | 0.1 | 波动大时增大,波动小时减小 | BTC市场我用0.3,ETH用0.15 |
| σ(波动率) | 0.02 | 用EWMA实时估计,窗口5-10分钟 | 别用固定值,市场会变 |
| T(持仓时间) | 60秒 | 流动性好时缩短,差时延长 | 我一般设30-120秒之间 |
我个人习惯的做法是:先用历史数据做一次网格搜索,找到一组不错的初始值,然后上线后每天自动微调。说白了,参数不是死的,要跟着市场走。
25.8 复现后的验证:怎么知道做对了?
代码写完了,回测跑完了,怎么验证结果靠谱?我一般看三个指标:
- 夏普比率:做市策略的夏普通常比趋势策略高,目标应该在 2-5 之间。如果低于 1,说明策略有问题。
- 最大回撤:做市策略的回撤应该很小,一般不超过 5%。如果回撤超过 10%,说明库存管理出了问题。
- 成交率:这个很关键。如果回测成交率超过 80%,我反而会怀疑——真实市场不可能这么理想。合理的成交率在 30%-60% 之间。
最后说一句:源码解读和复现,不是为了让你照抄。而是让你理解每个参数、每行代码背后的逻辑。只有理解了,你才能在实盘遇到问题时,知道该改哪里。
好了,这一章的内容就到这里。代码我已经放在课程配套的资源包里了,建议你打开 IDE,一行一行跟着跑一遍。遇到问题别怕,做市策略的坑,我基本都踩过了,你大概率不会比我更惨。
交易系统化学习资料 微信Strategy888888