第30章:课程总结与项目实战:构建一个完整的做市策略回测与优化系统

终于到了最后一章。说实话,写到这里我挺感慨的。从第一行代码开始,我们一步步搭建了做市策略的整个知识体系。这一章,咱们不聊新概念,而是把之前所有东西串起来,亲手构建一个完整的回测与优化系统。

我个人习惯,在项目收尾时一定要做一次“全景复盘”。你想想看,做市策略的核心无非三件事:报价逻辑、库存管理、风险控制。这三件事拧在一起,才是一个能跑的系统。

系统架构总览

先看整体结构。我画了一张图,帮你快速建立全局视角:

做市策略回测与优化系统架构 数据层 行情数据(Tick/Bar) | 订单簿快照 | 交易记录 策略层 报价引擎 | 库存管理器 | 风控模块 参数:价差宽度 | 挂单深度 | 再平衡阈值 | 最大持仓 回测与优化层 模拟撮合 | 绩效评估 | 参数扫描 | 贝叶斯优化 输出:最优参数组合 + 绩效报告

这张图我反复改了好几版。你会发现,数据在最底层,策略在中间,回测优化在最上层。为什么这么排?因为数据是地基,策略是骨架,回测是检验手段。我在项目中遇到过不少团队,一上来就调参数,结果数据没清洗干净,全白干。

核心模块实现

咱们拆开来看。每个模块我都给出关键代码,你直接拿去用。

1. 数据加载与预处理

数据质量决定回测质量。我习惯用Level2 Tick数据,但如果没有,1分钟Bar也能凑合。

import pandas as pd
import numpy as np

def load_market_data(file_path, freq='1min'):
    """
    加载行情数据,支持Tick和Bar两种格式
    """
    df = pd.read_csv(file_path, parse_dates=['timestamp'])
    df.set_index('timestamp', inplace=True)

    # 重采样到指定频率
    if freq != 'tick':
        df = df.resample(freq).agg({
            'bid_price1': 'last',
            'ask_price1': 'last',
            'bid_volume1': 'sum',
            'ask_volume1': 'sum',
            'last_price': 'last',
            'volume': 'sum'
        }).fillna(method='ffill')

    return df
我的经验:数据里经常有空值,尤其是非活跃时段。我曾经因为没处理NaN,回测结果看起来年化收益50%,实盘直接亏成狗。记住:fillna(method='ffill') 是你的好朋友。

2. 做市策略引擎

这是核心。我实现了一个带库存管理的做市策略,支持动态调整价差。

class MarketMakingStrategy:
    def __init__(self, spread=0.001, position_limit=1000, rebalance_threshold=0.5):
        self.spread = spread  # 价差宽度(百分比)
        self.position_limit = position_limit  # 最大持仓
        self.rebalance_threshold = rebalance_threshold  # 再平衡阈值
        self.position = 0  # 当前持仓
        self.cash = 1000000  # 初始资金

    def generate_quotes(self, mid_price, inventory):
        """
        根据当前库存调整报价
        """
        # 库存偏移量:持仓越多,报价越偏向卖出
        inventory_skew = inventory / self.position_limit
        adjusted_spread = self.spread * (1 + abs(inventory_skew))

        bid_price = mid_price * (1 - adjusted_spread / 2 - inventory_skew * 0.0001)
        ask_price = mid_price * (1 + adjusted_spread / 2 - inventory_skew * 0.0001)

        return bid_price, ask_price

    def check_rebalance(self):
        """
        检查是否需要再平衡
        """
        return abs(self.position) / self.position_limit > self.rebalance_threshold

说白了,这个策略的核心逻辑就是:库存多了就压低买价、抬高卖价,让市场帮你把库存消化掉。我刚开始做的时候,总想着靠预测方向赚钱,后来发现做市商赚的是流动性差价,不是方向性收益。

3. 模拟撮合引擎

回测的关键在于撮合逻辑。不能太理想化,否则结果失真。

class SimulatedExchange:
    def __init__(self, slippage=0.0001):
        self.slippage = slippage  # 滑点
        self.trades = []

    def match_order(self, quote_bid, quote_ask, market_bid, market_ask, volume):
        """
        模拟撮合:市价单与做市报价的交互
        """
        # 买单成交条件:做市卖价 <= 市场买价
        buy_volume = 0
        if quote_ask <= market_bid:
            buy_volume = volume
            fill_price = quote_ask * (1 + self.slippage)

        # 卖单成交条件:做市买价 >= 市场卖价
        sell_volume = 0
        if quote_bid >= market_ask:
            sell_volume = volume
            fill_price = quote_bid * (1 - self.slippage)

        return buy_volume, sell_volume, fill_price
注意:滑点参数别设成0。我见过有人回测时滑点为0,结果实盘每笔交易都亏在滑点上。建议至少设0.01%,高频做市可以设0.005%。

参数优化:从手动到自动

参数优化是回测的终极目标。我提供两种方法:网格搜索和贝叶斯优化。

网格搜索

简单粗暴,适合参数少的情况。

def grid_search(strategy_class, data, param_grid):
    best_params = None
    best_sharpe = -np.inf

    for spread in param_grid['spread']:
        for limit in param_grid['position_limit']:
            strategy = strategy_class(spread=spread, position_limit=limit)
            result = backtest(strategy, data)
            sharpe = result['sharpe_ratio']

            if sharpe > best_sharpe:
                best_sharpe = sharpe
                best_params = {'spread': spread, 'position_limit': limit}

    return best_params, best_sharpe

贝叶斯优化

参数多的时候,网格搜索太慢了。贝叶斯优化能更快找到最优区域。

from skopt import gp_minimize

def objective(params):
    spread, limit, threshold = params
    strategy = MarketMakingStrategy(
        spread=spread,
        position_limit=int(limit),
        rebalance_threshold=threshold
    )
    result = backtest(strategy, data)
    return -result['sharpe_ratio']  # 最小化负夏普

res = gp_minimize(
    objective,
    dimensions=[(0.0005, 0.005), (500, 5000), (0.3, 0.8)],
    n_calls=50,
    random_state=42
)

print(f"最优参数:价差={res.x[0]:.4f}, 持仓限制={int(res.x[1])}, 再平衡阈值={res.x[2]:.2f}")
我的建议:先用网格搜索跑一遍,了解参数的大致范围。然后用贝叶斯优化精细调优。别一上来就贝叶斯,容易陷入局部最优。我曾经在一个项目上直接跑贝叶斯,结果最优参数是价差0.0001,实盘根本没法做——太窄了,手续费都覆盖不了。

绩效评估指标

回测完了,怎么判断好坏?我一般看这几个指标:

指标 计算公式 说明
夏普比率 (年化收益 - 无风险利率) / 年化波动率 风险调整后收益,做市策略通常 > 2
最大回撤 max(峰值 - 谷值) / 峰值 做市策略应 < 5%,否则风控有问题
胜率 盈利交易数 / 总交易数 做市策略胜率通常较高,但单笔盈利小
库存周转率 总交易量 / 平均库存 越高说明流动性提供越活跃

你想想看,如果一个做市策略夏普比率不到1,最大回撤超过10%,那还不如去买ETF。我见过最离谱的一个策略,回测夏普3.5,实盘两周爆仓——后来发现是回测时没算手续费。

完整回测流程

最后,我把所有模块串起来,形成一个完整的回测流程:

def run_full_backtest(config):
    # 1. 加载数据
    data = load_market_data(config['data_path'], config['freq'])

    # 2. 初始化策略和交易所
    strategy = MarketMakingStrategy(
        spread=config['spread'],
        position_limit=config['position_limit'],
        rebalance_threshold=config['rebalance_threshold']
    )
    exchange = SimulatedExchange(slippage=config['slippage'])

    # 3. 回测主循环
    for timestamp, row in data.iterrows():
        mid_price = (row['bid_price1'] + row['ask_price1']) / 2
        bid, ask = strategy.generate_quotes(mid_price, strategy.position)

        buy_vol, sell_vol, fill_price = exchange.match_order(
            bid, ask, row['bid_price1'], row['ask_price1'], config['trade_volume']
        )

        # 更新持仓和资金
        strategy.position += buy_vol - sell_vol
        strategy.cash -= buy_vol * fill_price
        strategy.cash += sell_vol * fill_price

        # 检查再平衡
        if strategy.check_rebalance():
            # 执行再平衡逻辑(略)
            pass

    # 4. 计算绩效
    result = calculate_performance(strategy, data)
    return result

核心要点:

  • 数据质量 > 策略复杂度。先花70%时间清洗数据。
  • 回测必须包含手续费、滑点、冲击成本。
  • 参数优化要避免过拟合,建议用滚动窗口验证。
  • 库存管理是做市策略的生命线,别忽视。

嗯,到这里,整个课程的内容就全部结束了。从第一行的K线基础,到最后一行的完整回测系统,我们走过了30章。说实话,做市策略这条路没有终点,市场在变,参数要调,系统要迭代。但只要你掌握了这套方法论,无论市场怎么变,你都能快速适应。

最后送你一句话:回测是过去,实盘是未来。别让回测的幻觉,毁了实盘的信心。

无相订单流研究社 微信Lucian808555