01-微观结构理论

预计学习时间:3 小时

难度:⭐⭐⭐

核心问题:订单簿里正在发生什么?价格为什么会有买卖价差?知情交易者如何影响价格?


从一个直觉出发

你在某只股票的行情软件上看到:买一 10.00 元,卖一 10.02 元

你点了一个”市价买入”,系统以 10.02 元成交。你觉得”就这么简单”。

但在这 0.01 秒内,背后发生了一系列事情:

  1. 你的市价单到达交易所,匹配了卖一的挂单
  2. 卖一的 10.02 元被吃掉,卖二 10.03 元变成新的卖一
  3. 买一和卖一之间的 0.01 元——这个”价差”——就是做市商的利润空间
  4. 如果此时一个”知情交易者”也在买(他刚看到利好消息),他会把价格推得更高
  5. 做市商发现价格在动,会迅速调整报价来保护自己

微观结构理论就是研究这些过程的学科。


一、订单簿(Order Book)

1.1 订单簿长什么样

订单簿是交易所的核心数据结构。每一档都记录了”在这个价格上,有多少人想买/卖”。

卖五  10.08  │  3,200 股
卖四  10.07  │  1,500 股
卖三  10.06  │  2,800 股
卖二  10.05  │  1,000 股
卖一  10.04  │    800 股  ← 最优卖价(Ask)
─────────────┼──────────
卖一  10.02  │  1,200 股  ← 最优买价(Bid)  ← 这是更常见的写法
买二  10.01  │  2,000 股
买三  10.00  │  3,500 股
买四   9.99  │  1,800 股
买五   9.98  │  2,600 股

上面画反了,标准格式如下:

          卖盘(Ask/Sell)
  ──────────────────────────
  卖五  10.06  │  2,800 股
  卖四  10.05  │  1,000 股
  卖三  10.04  │    500 股
  卖二  10.03  │  1,200 股
  卖一  10.02  │    800 股  ← 最低卖出价(Best Ask)
  ════════════╪═══════════  ← 价差 = 10.02 - 10.00 = 0.02
  买一  10.00  │  1,200 股  ← 最高买入价(Best Bid)
  买二   9.99  │  2,000 股
  买三   9.98  │  3,500 股
  买四   9.97  │  1,800 股
  买五   9.96  │  2,600 股
  ──────────────────────────
          买盘(Bid/Buy)

中间价(Mid Price) = (Best Ask + Best Bid) / 2 = (10.02 + 10.00) / 2 = 10.01

1.2 价格优先、时间优先

撮合引擎按两个原则匹配订单:

  1. 价格优先:买价高的优先成交,卖价低的优先成交
  2. 时间优先:同价位的订单,先挂单的优先成交
# 模拟价格优先原则
class OrderBook:
    """简化的订单簿模拟"""
    def __init__(self):
        self.bids = []  # 买单列表 [(价格, 数量, 时间戳)]
        self.asks = []  # 卖单列表 [(价格, 数量, 时间戳)]
 
    def add_bid(self, price, volume, timestamp):
        self.bids.append((price, volume, timestamp))
        # 价格优先(降序),时间优先(升序)
        self.bids.sort(key=lambda x: (-x[0], x[2]))
 
    def add_ask(self, price, volume, timestamp):
        self.asks.append((price, volume, timestamp))
        # 价格优先(升序),时间优先(升序)
        self.asks.sort(key=lambda x: (x[0], x[2]))
 
    def best_bid(self):
        return self.bids[0] if self.bids else None
 
    def best_ask(self):
        return self.asks[0] if self.asks else None
 
    def spread(self):
        if self.bids and self.asks:
            return self.asks[0][0] - self.bids[0][0]
        return None
 
# 示例
ob = OrderBook()
ob.add_bid(10.00, 1200, 1)  # 时间戳 1
ob.add_bid(10.00, 500, 2)   # 同价,后到
ob.add_bid(9.99, 2000, 0)   # 价格低,排在后面
ob.add_ask(10.02, 800, 1)
ob.add_ask(10.03, 1200, 0)
 
print(f"最优买价: {ob.best_bid()}")  # (10.00, 1200, 1)
print(f"最优卖价: {ob.best_ask()}")  # (10.02, 800, 1)
print(f"买卖价差: {ob.spread()}")    # 0.02

1.3 限价单 vs 市价单

类型特点风险适用场景
限价单指定价格,不保证成交不确定性风险(可能不成交)不急于交易,想控制价格
市价单按最优价格立即成交价格风险(可能滑点大)急于交易,接受当前价格

做市商主要用限价单提供流动性,普通投资者常用市价单消费流动性。


二、买卖价差(Bid-Ask Spread)

2.1 价差的三种来源

价差不是交易所”收”的,而是由三个因素自然形成的:

买卖价差 = 信息不对称成本 + 库存风险成本 + 订单处理成本
  1. 信息不对称成本:做市商怕遇到知情交易者(知道股票即将涨跌的人)。为了补偿这种风险,做市商会加宽价差。
  2. 库存风险成本:做市商买了股票后,价格可能下跌。持有存货是有风险的。
  3. 订单处理成本:交易所手续费、技术成本、人力成本。

2.2 有效价差的衡量

简单价差:

有效价差(考虑实际成交在中间价的哪一侧):

其中 是实际成交价, 是当时的中间价。

Rolled 价差(不需要逐笔数据,只需要日频数据):

直觉:如果收益率序列存在负自相关(今天涨明天跌),说明你在”被价差来回收割”。

import numpy as np
 
def rolled_spread(returns):
    """Rolled 价差估计"""
    # 收益率的一阶自协方差
    autocov = np.cov(returns[:-1], returns[1:])[0, 1]
    if autocov >= 0:
        return 0.0  # 正自相关说明不是价差效应
    return 2 * np.sqrt(-autocov)
 
# 模拟:带价差的收益率(负自相关)
np.random.seed(42)
n = 10000
mid_returns = np.random.normal(0, 0.02, n)  # 中间价的真实收益率
# 加入买卖价差效应:买家付 ask,卖家收 bid
# 每次交易"损失"半个价差
half_spread = 0.001  # 半价差
trade_returns = mid_returns - half_spread * np.random.choice([-1, 1], n)
 
print(f"Rolled 价差估计: {rolled_spread(trade_returns):.6f}")
print(f"真实半价差: {half_spread:.6f}")
# 输出接近 2 * half_spread

CORWIN-SCHULTZ 价差估计(基于日内最高价和最低价):

def corwin_schultz(high, low):
    """CORWIN-SCHULTZ 价差估计器
    基于 Snover (2006)、Corwin & Schultz (2012)
    high, low: numpy 数组,日最高价和最低价
    """
    n = len(high)
    spreads = []
    for t in range(1, n):
        # 两天各自的对数高低价差
        beta_1 = (np.log(high[t-1] / low[t-1])) ** 2
        beta_2 = (np.log(high[t] / low[t])) ** 2
        # 两天合并的对数高低价差
        beta = (np.log(max(high[t-1], high[t]) / min(low[t-1], low[t]))) ** 2
 
        gamma = np.log(beta) - np.log(beta_1 + beta_2 + 1e-10)
        # 限制 gamma 范围
        gamma = max(gamma, -37)
 
        #alpha = (np.sqrt(2 * beta_1) - np.sqrt(beta_1 * beta_2)) / (3 - 2 * np.sqrt(beta_1 / (beta_1 + 1e-10)))
        alpha_1 = np.sqrt(2 * beta_1) - np.sqrt(beta)
        alpha_2 = np.sqrt(3 * beta_1) - np.sqrt(2 * beta)
 
        # 简化估计
        s_squared = 2 * (np.exp(alpha_1) - 1) / (1 + np.exp(alpha_2))
        if s_squared > 0:
            spreads.append(np.sqrt(s_squared))
 
    return np.mean(spreads) if spreads else 0.0

2.3 为什么价差会变化

  • 信息事件前后:财报发布前价差变宽(做市商怕知情交易)
  • 交易活跃度:流动性好的股票价差窄,冷门股票价差宽
  • 波动率:市场波动大时做市商加宽价差
  • 订单簿深度:挂单多,价差窄;挂单少,价差宽

三、Kyle (1985) 模型:知情交易者如何影响价格

3.1 核心直觉

Kyle 模型是微观结构最重要的模型之一。它回答了一个关键问题:

一个知道内幕信息的交易者,他的交易会如何影响价格?

场景:一个股票的真实价值是 (只有知情交易者知道),市场中有一个做市商,以及大量噪音交易者(随机买卖)。

  • 做市商根据观察到的总订单流设定价格
  • 知情交易者线性地提交订单:,其中 是做市商的预期
  • 噪音交易者随机提交订单

做市商看到总订单流 ,设定价格为:

其中 价格冲击系数(Kyle’s Lambda)。

3.2 Lambda 的直觉

越大,说明每一单位交易量对价格的推动越大

价格冲击 = lambda * 交易量

lambda 大 → 市场流动性差 → 大单很难不推高价格
lambda 小 → 市场流动性好 → 大单可以悄悄完成

在 Kyle 模型的均衡中:

  • :资产价值的不确定性(知情交易者的信息优势越大,lambda 越大)
  • :噪音交易的波动(噪音交易越多,知情交易者越容易”隐藏”)

3.3 Python 模拟价格冲击

import numpy as np
import matplotlib.pyplot as plt
 
np.random.seed(42)
 
# ============================================================
# Kyle (1985) 模型模拟
# ============================================================
n_rounds = 200  # 交易轮次
sigma_v = 2.0   # 资产真实价值的标准差
sigma_u = 5.0   # 噪音交易量的标准差
mu = 100        # 资产真实价值的均值
 
# Kyle 模型参数
# lambda = sigma_v / (2 * sigma_u)
lam = sigma_v / (2 * sigma_u)
# 知情交易者的最优交易策略系数
beta = sigma_u / sigma_v
 
# 模拟
true_values = np.random.normal(mu, sigma_v, n_rounds)
noise_trades = np.random.normal(0, sigma_u, n_rounds)
informed_trades = beta * (true_values - mu)
total_orderflow = informed_trades + noise_trades
 
# 做市商设定的价格
prices = mu + lam * total_orderflow
 
# 知情交易者的利润
informed_profit = informed_trades * (true_values - prices)
 
# 可视化
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
 
# 价格 vs 真实价值
axes[0, 0].plot(true_values, label='真实价值', alpha=0.5)
axes[0, 0].plot(prices, label='做市商价格', alpha=0.8)
axes[0, 0].set_title(f'价格 vs 真实价值 (lambda={lam:.4f})')
axes[0, 0].legend()
 
# 订单流 vs 价格变动
axes[0, 1].scatter(total_orderflow, prices - mu, alpha=0.3, s=10)
axes[0, 1].plot(total_orderflow, lam * total_orderflow, 'r-', label=f'斜率=lambda={lam:.4f}')
axes[0, 1].set_title('价格冲击:订单流 vs 价格变动')
axes[0, 1].legend()
 
# 知情交易者利润
axes[1, 0].hist(informed_profit, bins=50, edgecolor='black', alpha=0.7)
axes[1, 0].axvline(np.mean(informed_profit), color='red', linestyle='--')
axes[1, 0].set_title(f'知情交易者利润 (均值={np.mean(informed_profit):.2f})')
 
# 信息不对称的影响
sigmas_u = np.linspace(1, 20, 100)
lambdas = [sv / (2 * su) for su in sigmas_u]
axes[1, 1].plot(sigmas_u, lambdas)
axes[1, 1].set_xlabel('噪音交易波动 (sigma_u)')
axes[1, 1].set_ylabel('价格冲击系数 (lambda)')
axes[1, 1].set_title('噪音越多,知情交易者越容易隐藏')
 
plt.tight_layout()
plt.show()

关键洞察

  • 知情交易者永远不会一次性全仓买入(那会暴露自己),而是线性地分批交易
  • 噪音交易者越多,知情交易者越容易”混在其中”
  • 价格是部分反映信息的——做市商只能从订单流中推断出部分信息

四、Glosten-Milgrom 模型

4.1 做市商如何设价

Glosten-Milgrom (1985) 模型从一个更直观的角度出发:

做市商不知道对手方是”知情”还是”不知情”的,他如何设价?

假设:

  • 无消息概率 ,好消息概率 ,坏消息概率
  • 如果是好消息,股票值 ;坏消息值 ;无消息值

做市商看到一笔买入,更新他对好消息概率的判断(Bayes 法则):

做市商的最优报价:

4.2 白话理解

当一笔买单到来时,做市商会想:

“这笔买单是知情交易者看到利好来买的概率有多大?如果概率大,我就应该提高卖价来保护自己。”

这就是信息不对称如何转化为价差的机制。


五、Avellaneda-Stoikov 做市模型

5.1 核心思想

Avellaneda-Stoikov (2008) 回答的问题是:做市商应该把买卖报价挂在什么位置?

关键变量:

  • :当前中间价
  • :做市商当前持仓(正=多头,负=空头)
  • :做市商的交易时间窗口
  • :当前时间
  • :做市商的风险厌恶程度
  • :订单到达速率参数

最优报价:

直觉:持仓越多,卖价越激进(更接近中间价)以减少库存;买价越保守(远离中间价)以避免加仓

5.2 Python 模拟做市商行为

import numpy as np
import matplotlib.pyplot as plt
 
np.random.seed(42)
 
# ============================================================
# Avellaneda-Stoikov 做市模拟
# ============================================================
T = 1.0        # 交易时间窗口(天)
dt = 0.001     # 时间步长
n_steps = int(T / dt)
gamma = 0.1    # 风险厌恶系数
sigma = 0.3    # 资产波动率
k = 1.5        # 订单到达速率参数
 
# 模拟中间价(几何布朗运动)
mid_price = 100.0
inventory = 0   # 初始持仓
cash = 0.0      # 现金
 
mid_prices = [mid_price]
inventories = [inventory]
ask_prices = []
bid_prices = []
pnl = [0.0]
 
for t_idx in range(n_steps):
    t = t_idx * dt
    tau = T - t  # 剩余时间
 
    # Avellaneda-Stoikov 最优报价
    half_spread_base = 1.0 / (2 * k)
    inventory_adj = gamma * sigma**2 * tau * inventory
 
    ask = mid_price + half_spread_base + inventory_adj + gamma * sigma**2 * tau * 0.5
    bid = mid_price - half_spread_base + inventory_adj - gamma * sigma**2 * tau * 0.5
 
    ask_prices.append(ask)
    bid_prices.append(bid)
 
    # 模拟订单到达(简化:每步有一定概率成交)
    if np.random.random() < 0.3:  # 有 30% 概率有对手方
        if np.random.random() < 0.5:
            # 买入(我们卖出,减少库存)
            inventory -= 1
            cash += ask
        else:
            # 卖出(我们买入,增加库存)
            inventory += 1
            cash -= bid
 
    # 中间价变动
    mid_price *= np.exp(-0.5 * sigma**2 * dt + sigma * np.sqrt(dt) * np.random.normal())
    mid_prices.append(mid_price)
    inventories.append(inventory)
 
    # 计算 PnL
    pnl.append(cash + inventory * mid_price)
 
# 可视化
fig, axes = plt.subplots(3, 1, figsize=(14, 10))
 
axes[0].plot(mid_prices[:200], label='中间价', linewidth=0.5)
axes[0].plot(ask_prices[:200], label='Ask', alpha=0.5, linewidth=0.5)
axes[0].plot(bid_prices[:200], label='Bid', alpha=0.5, linewidth=0.5)
axes[0].set_title('Avellaneda-Stoikov 做市报价')
axes[0].legend()
 
axes[1].plot(inventories[:200])
axes[1].set_title('做市商库存变化')
 
axes[2].plot(pnl[:200])
axes[2].set_title('做市商 PnL')
 
plt.tight_layout()
plt.show()

六、知情交易概率(PIN)

6.1 什么是 PIN

PIN(Probability of Informed Trading)衡量的是:在当前市场中,一笔交易是知情交易的概率有多大?

PIN 越高,做市商面临的逆向选择风险越大,价差应该越宽。

6.2 简化计算

import numpy as np
from scipy.optimize import minimize
 
def estimate_pin_simple(buys, sells, n_days=252):
    """简化的 PIN 估计
    buys: 每日主动买入笔数
    sells: 每日主动卖出笔数
    """
    total_buys = np.sum(buys)
    total_sells = np.sum(sells)
 
    # 总交易量
    total = total_buys + total_sells
 
    # 买入占比偏斜(简化版 PIN)
    buy_frac = total_buys / total
    sell_frac = total_sells / total
 
    # 简化估计:PIN ≈ 信息不对称导致的不平衡程度
    # 实际 PIN 需要最大似然估计
    imbalance = np.abs(buys - sells) / (buys + sells + 1e-10)
 
    # 日均不平衡
    daily_imbalance = np.mean(imbalance)
 
    return daily_imbalance
 
# 模拟
np.random.seed(42)
n_days = 252
# 正常交易:买卖大致平衡
noise_buys = np.random.poisson(100, n_days)
noise_sells = np.random.poisson(100, n_days)
# 加入知情交易(买入偏多)
informed_buys = np.random.poisson(30, n_days)
informed_sells = np.random.poisson(10, n_days)
 
total_buys = noise_buys + informed_buys
total_sells = noise_sells + informed_sells
 
pin = estimate_pin_simple(total_buys, total_sells)
print(f"简化 PIN 估计: {pin:.4f}")

七、订单流毒性(VPIN)

7.1 什么是 VPIN

VPIN(Volume-Synchronized Probability of Informed Trading)是 Easley, Lopez de Prado 和 O’Hara (2012) 提出的指标。

核心思想:当订单流变得”有毒”(不平衡程度异常高)时,通常预示着价格即将大幅变动。

VPIN = 基于成交量桶(volume bucket)计算的知情交易概率,比传统 PIN 更适合高频数据。

7.2 Python 简化计算

import numpy as np
 
def compute_vpin(prices, volumes, bucket_size=10000):
    """简化的 VPIN 计算
    prices: 成交价格序列
    volumes: 成交量序列
    bucket_size: 每个桶的目标成交量
    """
    # 判断买卖方向:价格高于前一笔为买入,低于为卖出
    directions = np.sign(np.diff(prices))
    directions = np.concatenate([[0], directions])
    # 去掉方向为 0 的
    mask = directions != 0
    dirs = directions[mask]
    vols = volumes[mask]
 
    # 按成交量分桶
    cum_vol = 0
    buckets = []
    current_bucket = []
 
    for v, d in zip(vols, dirs):
        current_bucket.append((v, d))
        cum_vol += v
        if cum_vol >= bucket_size:
            buckets.append(current_bucket)
            current_bucket = []
            cum_vol = 0
 
    # 计算每个桶的买卖不平衡
    imbalances = []
    for bucket in buckets:
        buy_vol = sum(v for v, d in bucket if d > 0)
        sell_vol = sum(v for v, d in bucket if d < 0)
        total = buy_vol + sell_vol
        if total > 0:
            imb = abs(buy_vol - sell_vol) / total
            imbalances.append(imb)
 
    # VPIN = 最近 L 个桶的平均不平衡
    L = min(50, len(imbalances))
    vpin = np.mean(imbalances[-L:]) if imbalances else 0.0
 
    return vpin, imbalances
 
# 模拟
np.random.seed(42)
n_trades = 50000
base_price = 100
prices = [base_price]
volumes = np.random.exponential(100, n_trades)
 
for i in range(1, n_trades):
    # 正常波动
    ret = np.random.normal(0, 0.001)
    # 在某些时段加入大单(模拟知情交易)
    if 20000 < i < 21000:
        ret += 0.005  # 有利好的知情交易
    prices.append(prices[-1] * (1 + ret))
 
prices = np.array(prices)
vpin_val, imbalances = compute_vpin(prices, volumes, bucket_size=5000)
print(f"VPIN: {vpin_val:.4f}")

八、价格发现(Hasbrouck 模型)

8.1 核心思想

Hasbrouck (1991) 问的是:信息如何逐步反映到价格中?

在真实市场中,信息不是瞬间反映的。一笔新的信息可能出现后:

  • 第一秒:少部分交易者反应
  • 第二秒:更多交易者看到并反应
  • 第三秒:做市商调整报价
  • …持续数秒到数分钟

Hasbrouck 用 VAR 模型捕捉这个过程:

其中 是收益率, 是带符号的交易量(买入为正、卖出为负)。

信息份额(Information Share):衡量订单流对价格发现贡献的比例。在多市场(如沪深两市同时交易)中,可以衡量哪个市场主导了价格发现。


九、Ho-Stoll 库存模型

9.1 做市商如何管理库存风险

做市商的核心难题:他必须不断买卖来提供流动性,但每笔交易都会改变他的持仓,持仓越大,价格反向变动的风险越高。

Ho-Stoll (1981) 模型刻画了做市商如何在价差和库存之间做权衡。

做市商的最优报价:

其中 是做市商当前库存(多头为正,空头为负), 是库存调整系数。

直觉

  • 当库存为正(持有多头)时,做市商会降低 Ask 和 Bid(降低卖价吸引买家,降低买价阻止卖家)
  • 当库存为负(持有空头)时,做市商会提高 Ask 和 Bid
  • 目标:把库存拉回零

这就是为什么做市商的报价不是对称的——他的库存会”扭曲”报价。


十、本文件核心要点

模型核心问题关键参数
订单簿价格如何匹配?价格优先、时间优先
价差理论为什么买价和卖价不同?信息不对称、库存风险
Kyle (1985)知情交易者如何影响价格?lambda(价格冲击系数)
Glosten-Milgrom做市商如何应对信息不对称?Bayes 更新
Avellaneda-Stoikov做市商应该挂什么价?库存、风险厌恶、时间窗口
PIN交易对手是否知情?知情交易概率
VPIN订单流是否有毒?成交量桶不平衡度
Hasbrouck信息如何反映到价格?信息份额
Ho-Stoll做市商如何管理库存?库存调整系数

一句话总结:微观结构理论的核心是理解”交易的真实成本”,以及这些成本从何而来、如何变化。