yuqi-zheng

上交所与深交所集合竞价:最终定价规则的分歧与撮合引擎的实现缺口


沪市(上交所 SSE)与深市(深交所 SZSE)每个交易日都以集合竞价(call auction)而非连续竞价的方式开盘与收盘。两者的机制几乎一模一样:在一个固定时间窗内累积委托,再一次性计算出一个使成交量为最大的统一清算价格。但两家交易所在一处细微却关键的地方分道扬镳——当不止一个价格满足”最大成交量”条件时,最终定价用的 tie-break 规则不同。若跨市场引擎在这里写错,一只沪市股票可能悄无声息地按深市规则成交。

本文依次梳理官方规则、给出一个具体的价差实例,并剖析我在阅读某生产级 A 股撮合引擎 call_auction() 例程时发现的实现缺口。


集合竞价的时间窗

阶段时间说明
开盘集合竞价9:15 – 9:259:20–9:25 不接受撤单
连续竞价9:30 – 11:30、13:00 – 14:57标准逐笔撮合
收盘集合竞价14:57 – 15:00不接受撤单;上交所于 2018 年引入

撤单限制很关键:9:20–9:25 以及整个收盘集合竞价期间,主机都拒绝撤单。窗口关闭那一刻你看到的盘口,就是真正用于定价的盘口——不存在最后一毫秒撤单的可能。


两市共用的核心三步

两家交易所都依据完全相同的前三个基础条件确定开盘价/收盘价:

  1. 可实现最大成交量——在所有合格价格中,选取成交量最大的那些。
  2. 高于该价的买入与低于该价的卖出全部成交——任何高于清算价的买委托、任何低于清算价的卖委托都必须完全成交。
  3. 同价至少一方全部成交——若清算价等于某个买价或卖价,则该侧必须被耗尽。

至此两市完全一致。凡是闯过条件③的价格,都称为候选价格。两市的唯一分歧,就在于如何在这些候选价格中做最后的选择。


分歧所在:最后的 tie-break

这就是全部差异。上面的一切都相同,只有最后一步不同。

上海(上交所)《交易规则》§3.5.2(2026 修订版):

两个以上申报价格符合上述条件的,使未成交量最小的申报价格为成交价格;仍有两个以上使未成交量最小的申报价格符合上述条件的,其中间价为成交价格。

即:若多个价格符合条件,先取未成交量最小的那个;若此举留下多个价格,则取这些剩余价格的中间价(median)

所以上交所是两段式 tie-break:

最大成交量 → 未成交量最小 → 剩余候选价格取中间价

深圳(深交所)《交易规则》:

两个以上申报价格符合上述条件的,取距前收盘价最近的价格为成交价格。

即:若多个价格符合条件,取距前收盘价最近的那个。

深交所坍缩为单一 tie-break:

最大成交量 → 距前收盘价最近

两家交易所互不借用对方的最后一步。上交所的”中间价”与深交所的”距前收盘价最近”是两套互斥的裁决逻辑。

步骤上交所 SSE深交所 SZSE
最大成交量最大成交量
高于价全买 / 低于价全卖高于价全买 / 低于价全卖
同价至少一方全成交同价至少一方全成交
未成交量最小
剩余价格取中间价距前收盘价最近

深交所规则与 TSE 板寄せ异曲同工

读过我们姊妹篇《TSE 板寄せ拍卖定价》的读者会发现,深交所的最后一步并非深圳独有——它正是驱动 TSE 条件 5(参考价回退)的同一直觉。

在板寄せ中,前四个条件(有效价格区间 → 最大成交量 → 最小失衡 → 失衡方向)先把候选集收窄到 [最低卖重价, 最高买重价] 这一带,随后条件 5 套用参考价base_price,初始为前一日收盘价):

情形TSE 取值
base_price 落于收窄带内base_price
base_price 高于收窄带narrowed_high
base_price 低于收窄带narrowed_low

这其实就是”把前收盘价夹进可行价格区间”。深交所的规则——“取距前收盘价最近的候选价格”——是同一操作,区别只是可行区间已经被约简为最大成交量价格集合。对于连续的候选带,二者等价:被夹进来的前收盘价,正是最近的那个候选。

共通的哲学才是重点:当委托流均衡、本身无法决定价格时,两家交易所都把开盘价锚定在前收盘价上,而不是让它漂到某个极端端点。前收盘价是一根对抗”零未成交量”的稳压锚。

两点提醒,使其不至于被误认为完全相同:

  • 动态 vs 静态锚点。 TSE 的 base_price 可以实时更新——熔断触发时,交易所通过 O 标签广播一个新的参考价,拍卖随之重新锚定。深交所的前收盘价是整个交易时段固定的。
  • 区间如何计算。 TSE 用四条更丰富的中间条件(含最小失衡、失衡方向检查)收窄,并使用价格依赖的 tick。深交所的候选集仅仅是”所有达到最大成交量的价格”。

一个有趣的推论:在我们覆盖的三家交易所里,深交所才是行为像 TSE 的那一个,而非上交所。上交所的中间价(步骤⑤)不依赖前收盘价——它把候选区间从正中劈开,全然不顾前收盘在哪。所以,若你要在不同交易所间移植拍卖逻辑,深交所与 TSE 共享”回退到前收盘”的反射;上交所则不然。


一个实例

假设前收盘价为 ¥10.13,最小价格变动单位(tick)为 ¥0.01,盘口是一份”缺口型”均衡簿——所有买委托都挂在 ¥10.20 及以上,所有卖委托都挂在 ¥10.10 及以下,中间空白:

方向价格数量
10.20500
10.30500
10.00500
10.10500

对于区间 [10.10, 10.20] 内的任意价格 P,可成交量均为 min(总买量, 总卖量) = 1000,而未成交量( residual)均为 0——整段区间完全均衡。因此 ¥10.10 到 ¥10.20 之间的每一个价格都是候选价格,且全部以最大成交量、零未成交量并列。

套用两套规则:

  • 上交所:未成交量处处已是最小,直接跳到步骤⑤——候选区间的中间价[10.10, 10.20] 的中点为 ¥10.15
  • 深交所:落在区间内、且距前收盘价 ¥10.13 最近的价格就是 ¥10.13 本身(距离为 0)。因此开盘价为 ¥10.13

仅因一条规则之差,就产生 两个 tick(¥0.02) 的偏差。对于一只 ¥100 的活跃股,这个缺口会蔓延到所有依赖开盘价的信号——VWAP 基准、盯市估值、盘前分析。


撮合引擎内部

我阅读过的一套生产级 A 股撮合引擎,其 call_auction() 采用”贪心最大成交量 + tie-break 链”实现。以下为精简、改名后的代表版本:

// 某生产级 A 股引擎集合竞价定价逻辑的代表性还原(call_auction)
double call_auction(OrderQueue& bidq, OrderQueue& askq,
                    double pre_close, double tick) {
    // --- 阶段一:贪心最大成交量撮合 -----------------------------
    long long dealed = 0;
    double prev_best_bid = 0, prev_best_ask = 0;
    while (askq.best_price() <= bidq.best_price()) {
        long long v = std::min(askq.best_vol(), bidq.best_vol());
        dealed += v;
        prev_best_ask = askq.best_price();
        prev_best_bid = bidq.best_price();
        askq.simu_deal_best(v);
        bidq.simu_deal_best(v);
    }

    // --- 阶段二:开盘价 tie-break -------------------------------
    double open;
    if (dealed == 0) {
        open = pre_close;                              // 空盘兜底
        if (askq.best_price() < pre_close) open = askq.best_price();
        if (bidq.best_price() > pre_close) open = bidq.best_price();
        return open;
    }

    double best_ask = askq.best_price();
    double best_bid = bidq.best_price();

    // (a) 某一侧在上次成交档位仍有挂量
    if (askq.non_empty() && best_ask == prev_best_ask) return prev_best_ask;
    if (bidq.non_empty() && best_bid == prev_best_bid) return prev_best_bid;

    // (b) 未成交量最小代理:取剩余量较小的一侧
    long long ask_left = askq.best_vol();
    long long bid_left = bidq.best_vol();
    double ask_bound = prev_best_bid;                  // 区间下界
    double bid_bound = prev_best_ask;                  // 区间上界
    if (askq.non_empty()) ask_bound = best_ask - tick;
    if (bidq.non_empty()) bid_bound = best_bid + tick;

    if (ask_left != bid_left) {
        return (ask_left < bid_left) ? best_ask : best_bid;
    }

    // (c) 最终裁决:区间内距前收盘价最近。
    //     这正是深交所规则。上交所的"中间价"在此缺失。
    open = pre_close;
    if (pre_close < bid_bound)      open = bid_bound;
    else if (pre_close > ask_bound) open = ask_bound;
    return open;
}

两点值得注意:

  1. 没有交易所分支。 该函数从不判断标的属于上交所还是深交所,同一段代码给沪市股和深市股定价。
  2. 最终裁决被硬编码为”距前收盘价最近”。 这正是深交所规则。上交所的”中间价”步骤(⑤)根本不存在。

缺失的上交所步骤,量化一下

该引擎对深交所天然正确。对上交所,它在”未成交量最小”(步骤④)已把候选集坍缩到单一价格这种退化情形下才正确。在我们的实例中——未成交量在整段 [10.10, 10.20] 上恒为 0——步骤④无法收窄任何东西,于是引擎落入深交所裁决逻辑,打出 ¥10.13,而非上交所规定的中间价 ¥10.15。

更精确地说,该缺陷在**“以最大成交量且最小未成交量并列的价格集合跨越多于一个 tick”**时才触发。在这个区域内:

  • 上交所要求取候选区间的中间价
  • 引擎没有任何代码路径能计算中间价——它自始至终只比较两个极端 residual 队列,最后用前收盘价裁决。

所以,一只处于均衡、带缺口盘口的沪市股票,会被按深市逻辑定价。该缺口在实践中较窄(只在宽幅均衡开盘、熔断复牌等情形下触发),但它是一个货真价实的合规缺陷:该沪市证券打印出的开盘价,将与实际交易所不一致。


一个正确的参考实现

修复很小且局部。与其即时裁决,不如在成交量扫描阶段收集完整的候选集,再按交易所在最后一步分支:

// 收集每一个"以最大成交量且最小未成交量"达成的价格。
std::vector<double> candidates = scan_candidate_prices(bidq, askq, tick);

double open;
if (exchange == Exchange::SSE) {
    // 步骤⑤:候选区间的中间价(中间价)。
    std::sort(candidates.begin(), candidates.end());
    open = candidates[candidates.size() / 2];        // 按 §3.5.4 取整到 tick
} else { // 深交所
    // 步骤④:距前收盘价最近。
    open = *std::min_element(candidates.begin(), candidates.end(),
        [=](double a, double b) {
            return std::abs(a - pre_close) < std::abs(b - pre_close);
        });
}

对于连续候选区间,中间价就是中点;偶数个时由交易所按最近 tick 取整(上交所 §3.5.4)。关键改动是结构性的:引擎必须显式构造候选集,而不是从两个剩余队列反推价格。


为什么这很重要

跨市场引擎——尤其是从单一 A 股参考实现改造而来的那些——经常把一家交易所的 tie-break 套用到两家。代价是静默的:

  • 不崩溃、不抛异常、不记日志;
  • 在最有意义的那些日子(宽幅开盘、波动熔断复牌、低流动性证券),开盘打印价只是与真实行情差一两个 tick;
  • 对做盘后核对或发布指示性价格的机构而言,系统性的沪/深错配是合规与 P&L 问题,而非表面瑕疵。

唯一可靠的保障,是一组回归用例:强制构造”多价格、最小未成交量”的情形,并断言上交所中间价与深交所最近前收价的差异。没有它,这个 bug 会堂而皇之地藏起来,因为常见的单价格情形永远通过。


小结

  • 两家交易所共用”最大成交量 + 全量成交 + 一侧全成交”的核心(步骤①–③)。
  • 上交所是两段式 tie-break:未成交量最小(④),再取剩余价格中间价(⑤)。
  • 深交所是单一 tie-break:距前收盘价最近(④)。
  • 常见引擎把深交所裁决逻辑硬编码,并遗漏上交所中间价——对深市正确,对沪市在候选集跨多 tick 时静默错误。
  • 局部修复:在成交量扫描阶段收集候选价格集,最后按交易所分支裁决。

参考

  • 《上海证券交易所交易规则》(2026 修订版)§3.5.2
  • 《深圳证券交易所交易规则》——集合竞价成交价格确定
  • 延伸阅读:TSE 板寄せ拍卖定价:五条件算法详解,了解另一家交易所的五条件级联