---
description: "10 枚骰子和：中价 35、sd $\\approx 5.4$ 定 spread；被成交本身是信息，按 $E[S \\mid \\text{对手买}]$ 移价"
---

# 做市游戏：10 枚骰子的和

## 题

面试官掷 10 枚骰子，盖住不看，点数和记为 $S$。他说："给我报一个买价和卖价（bid / ask），每点 1 块钱，我可以在你的价格上买或卖。"你怎么报？他在你的卖价上买了一手之后，你怎么改价？

## 一句话

**中价报 35，价差（spread）取在一个标准差附近——比如 32 @ 38**；被成交之后不要原地不动：**对手愿意买，说明真值更可能在你的卖价之上**，把报价往成交方向挪。

## 关键技巧

**一、公允价 = 期望。** 期望线性，$E[S] = 10 \times 3.5 = 35$。不需要知道分布长什么样。

**二、价差由不确定性决定。** 单枚骰子方差 $\frac{35}{12}$，独立相加：

$$
\mathrm{Var}(S) = 10 \times \frac{35}{12} = \frac{175}{6} \approx 29.17, \qquad \mathrm{sd} \approx 5.40
$$

对完全没信息的对手，每成交一手你稳赚半个价差——这笔钱是用来贴补**有信息的对手**从你身上赚走的部分。不确定性越大，对手可能掌握的信息越值钱，价差越要宽。

**三、成交是信息：逆向选择（adverse selection）。** 对手不是随机成交的。若他比你多知道一点，他只在对他有利时成交——你被买走的时刻，恰恰是真值高于卖价的时刻。你该关心的不是 $E[S]$，而是

$$
E[S \mid \text{对手在 ask 买}]
$$

报价合理的条件是这个条件期望不超过 ask。这就是 Glosten–Milgrom 模型的做市零利润条件：**ask 取在 $a = E[S \mid \text{在 } a \text{ 被买}]$ 的不动点上**。

## 解

**第一步：报初始价。** 精确分布下，不同宽度的区间覆盖真值的概率：

| 报价 | 32 @ 38 | 30 @ 40 | 27 @ 43 |
| --- | --- | --- | --- |
| 半宽 | 3（约 0.6 sd） | 5（约 0.9 sd） | 8（约 1.5 sd） |
| $P(\text{bid} \le S \le \text{ask})$ | 47.8% | 68.7% | 88.4% |

面试里报 **32 @ 38** 或 **30 @ 40** 都说得通——要能说出"这是大约一个标准差"。报 34 @ 36 太窄，稍有信息的对手就能吃你；报 20 @ 50 太宽，没人跟你做，等于没做市。

```echarts
{
  "height": 300,
  "grid": {"left": 55, "right": 30, "top": 30, "bottom": 40},
  "xAxis": {"type": "category", "name": "S", "nameLocation": "middle", "nameGap": 26,
            "data": ["20","21","22","23","24","25","26","27","28","29","30","31","32","33","34","35","36","37","38","39","40","41","42","43","44","45","46","47","48","49","50"]},
  "yAxis": {"type": "value", "name": "P(S)"},
  "series": [{
    "type": "bar", "barWidth": "70%", "itemStyle": {"color": "#4e79a7"},
    "data": [0.0014,0.0024,0.004,0.0063,0.0095,0.0137,0.019,0.0254,0.0326,0.0405,0.0485,0.0561,0.0629,0.0682,0.0715,0.0727,0.0715,0.0682,0.0629,0.0561,0.0485,0.0405,0.0326,0.0254,0.019,0.0137,0.0095,0.0063,0.004,0.0024,0.0014],
    "markLine": {
      "symbol": "none",
      "lineStyle": {"color": "#e15759", "width": 2, "type": "dashed"},
      "label": {"position": "end"},
      "data": [{"xAxis": "32", "label": {"formatter": "bid 32"}}, {"xAxis": "38", "label": {"formatter": "ask 38"}}]
    }
  }]
}
```

**第二步：被成交后更新。** 先看最极端的情形——对手偷看了一枚骰子 $d$，他眼里的期望是 $d + 31.5$，只在这比你的 ask 高时才买：

- ask = 37：只有 $d = 6$ 时他才买。成交条件下 $E[S] = 37.5$——**你每手亏 0.5**。
- ask = 36：$d \in \{5, 6\}$ 时买，成交条件下 $E[S] = 37$，每手亏 1。
- ask = 38：他永远不买。

**只要被成交，你就站在了错的一边。** 更现实的设定是你不知道对手有没有信息。设对手有 $\pi$ 的概率是知道 $S$ 的内幕交易者（$S > $ ask 才买），否则是噪声交易者（一半概率买）。在 ask = 37 被买之后：

| $\pi$ | 0.1 | 0.3 | 0.5 |
| --- | --- | --- | --- |
| $E[S \mid \text{在 37 被买}]$ | 35.40 | 36.31 | 37.37 |
| 零利润 ask | 35.44 | 36.34 | 37.37 |

$\pi = 0.5$ 时 37 已经不够——被买一次，新的公允价就跳到 37.4，下一轮报价应该整体上移到 34 @ 40 一带。面试里说清楚"被买就上移、被卖就下移，移多少取决于我认为对手有多大信息优势"，比算出精确数重要。

## 延伸

- **逐枚揭开的版本**：面试官常在中途翻开一枚骰子。翻出 $d$ 后，剩下 9 枚的期望 31.5、sd $\sqrt{9 \times 35/12} \approx 5.12$，新中价 $31.5 + d$，价差按新 sd 收窄。每揭开一枚，不确定性按 $\sqrt{n}$ 缩小。
- **库存风险**：连续被买 10 手后你空了 10 手，盈亏的标准差是 $10 \times 5.4 = 54$。即使没有信息不对称，也该把报价往上偏——提高 bid 更愿意买回来、提高 ask 更不愿再卖。Avellaneda–Stoikov 模型把这写成保留价格 $r = s - q\gamma\sigma^2$（$q$ 为持仓，空头时 $q<0$，$r$ 上移）。
- **信息 vs 库存，两种移价别混**：逆向选择移的是**中价的估计**（你对 $S$ 的看法变了）；库存管理移的是**报价相对中价的偏斜**（你对风险的承受力变了）。两者方向常常一致，原因完全不同。
- **常见错误一**：被成交后原价继续报。对手连着在同一个价位买，是在告诉你价格错了。
- **常见错误二**：追求价差最窄。面试官考的是风险意识——宽价差加上能讲清楚的更新规则，远比窄价差被反复吃单好。
- **标的换成估算题**：做市游戏的标的常是 [Fermi 估算](fermi-estimation.md) 类的问题（"世界上有多少架飞机"）。这时 sd 来自你拆解的误差，价差应在对数尺度上对称——报 $x/3$ @ $3x$ 而不是 $x \pm c$。

??? note "数值验证"

    精确卷积出 $S$ 的分布，再算偷看一枚和 Glosten–Milgrom 两个情形：

    ```python
    from fractions import Fraction as F
    import math

    dist = {0: F(1)}
    for _ in range(10):                        # 10 次卷积，精确有理数
        new = {}
        for s, p in dist.items():
            for d in range(1, 7):
                new[s + d] = new.get(s + d, 0) + p / 6
        dist = new
    mu = sum(s * p for s, p in dist.items())
    var = sum((s - mu) ** 2 * p for s, p in dist.items())
    print(mu, var, math.sqrt(var))             # 35 175/6 5.400617248673217
    P = lambda cond: float(sum(p for s, p in dist.items() if cond(s)))
    for w in (3, 5, 8):
        print(35 - w, 35 + w, round(P(lambda s: 35 - w <= s <= 35 + w), 3))
    # 32 38 0.478
    # 30 40 0.687
    # 27 43 0.884

    for ask in (36, 37, 38):                   # 对手偷看了 1 枚骰子
        buys = [d for d in range(1, 7) if d + 31.5 > ask]
        print(ask, buys, sum(d + 31.5 for d in buys) / len(buys) if buys else None)
    # 36 [5, 6] 37.0
    # 37 [6] 37.5
    # 38 [] None

    def E_given_buy(ask, pi):
        """比例 pi 的对手知道 S（S > ask 才买），其余一半概率买。"""
        up = sum(p for s, p in dist.items() if s > ask)
        num = pi * sum(s * p for s, p in dist.items() if s > ask) + (1 - pi) * F(1, 2) * mu
        return float(num / (pi * up + (1 - pi) * F(1, 2)))

    for pi in (F(1, 10), F(3, 10), F(1, 2)):
        a = min((abs(x / 100 - E_given_buy(x / 100, pi)), x / 100) for x in range(3500, 4500))[1]
        print(float(pi), round(E_given_buy(37, pi), 2), a)
    # 0.1 35.4 35.44
    # 0.3 36.31 36.34
    # 0.5 37.37 37.37

    print(round(math.sqrt(9 * 35 / 12), 2))   # 5.12
    ```
