TSE 板寄せ拍卖定价:五条件算法详解
东京证券交易所(TSE)每个交易日的开盘和收盘都使用拍卖机制——而非连续竞价。这种拍卖机制称为板寄せ(Itayose),收集所有委托后确定一个清盘价格,在最大化成交量的同时遵循精确的五条件判定序列。
大多数交易所在开盘和收盘时都有某种形式的拍卖,但 TSE 的五条件规范是独一无二且完全确定性的。理解它对构建 TSE 行情系统至关重要,算法本身也是金融市场微观结构如何平衡竞争目标的精彩案例研究。
本文基于 TSE 官方规范,完整讲解 Itayose 算法,并附带一个从 pcap 文件处理真实 FLEX Full MBO 历史数据的生产级 C++ 实现。
板寄せ vs. 连续竞价
在连续竞价(Zaraba,ザラバ)中,订单穿越买卖价差时立即成交,价格逐笔确定。
在板寄せ中,交易所先收集所有委托,然后计算唯一的理论拍卖价格(IAP, Indicative Auction Price)和理论拍卖成交量(IAV, Indicative Auction Volume)。所有能在 IAP 成交的订单同时撮合。
TSE 在三种场景使用板寄せ:
- 开盘拍卖(9:00 AM)
- 收盘拍卖(3:30 PM)
- 盘中暂停时的特别报价(熔断、波动中断等)
FLEX 行情通过 O(Order Status)标签的 pricing_method = 1 标识板寄せ模式。
FLEX 协议:构建订单簿
在计算板寄せ价格之前,我们需要从原始网络包重建订单簿。
数据包结构
TSE FLEX Full MBO 数据通过 UDP 组播传输。每个数据包包含 FlexPacketHeader 和 TLV(Type-Length-Value)编码的标签:
┌─────────────────────────────────────────┐
│ FlexPacketHeader (25 字节) │
├─────────────────────────────────────────┤
│ 标签 1: [长度][类型][数据...] │
│ 标签 2: [长度][类型][数据...] │
│ ... │
└─────────────────────────────────────────┘
头部字段:
| 字段 | 大小 | 字节序 | 用途 |
|---|---|---|---|
multicast_group | 1 字节 | — | 通道标识 |
sys_reboots | 1 字节 | — | 重启计数(用于去重) |
seq_num | 4 字节 | 大端 | 序列号 |
issue_code | 12 字节 | ASCII | 证券代码,左填充空格 |
msg_count | 1 字节 | — | 数据包中的标签数量 |
标签类型
| 标签 | 名称 | 操作 |
|---|---|---|
A | 新增委托 | 插入订单簿(限价或市价) |
D | 删除委托 | 移除(撤单、到期或减量) |
E | 成交 | 部分成交 |
C | 带价成交 | 指定价格成交 |
O | 委托状态 | 交易模式(板寄せ/ザラバ)+ 参考价格 |
T | 时间 | 时间戳 |
R | 重置 | 清空所有订单簿 |
L | 通信控制 | 通知信息 |
MBO 订单簿
FLEX Full 行情提供**逐笔委托(MBO, Market By Order)**数据——每个订单单独可见,而非仅显示聚合的价格档位。我们维护 unordered_map<orderId, Order>:
struct Order {
uint64_t order_id;
uint64_t price;
uint64_t qty;
char side; // 'B' 或 'S'
uint32_t time_usec; // 优先级时间戳
};
市价单使用哨兵价格 0xFFFFFFFFFFFFFFFF——它们参与每个价格档位的成交量计算。
修改处理
TSE 将订单修改编码为 Delete + Add 配对,带 modification_flag = 1:
D(orderId, flag=1)—— 保存原订单的time_usec到PendingMod结构A(orderId, flag=1)—— 继承保存的time_usec,保留队列优先级
这很关键:部分减量保持原始时间优先级,而价格变更或完全撤销则不保留。
数据包去重
FLEX 数据包可能被重传。我们使用复合键跟踪已见数据包:
struct PacketKey {
uint8_t group; // multicast_group
uint8_t reboots; // sys_reboots
uint32_t seq; // 序列号
};
VLAN 支持
某些抓包环境会插入 802.1Q VLAN 标签(以太网头部多 4 字节)。通过检查偏移量 12 处的 EtherType 检测:
uint16_t ether_type = ntohs(*(const uint16_t*)(packet + 12));
if (ether_type == 0x8100) return 18; // 带 VLAN 标签
return 14; // 标准以太网
价格精度:整数 × 10000
TSE 线路格式将价格表示为缩放 10000 倍的 64 位大端整数。这完全消除了浮点误差:
| 线路值 | 实际价格 |
|---|---|
17770000 | ¥1,777.0000 |
34565000 | ¥3,456.5000 |
6746000 | ¥674.6000 |
Itayose 算法中的所有运算都在缩放整数上进行。仅在输出时转换为小数。
市价单哨兵值
市价单的 price = 0xFFFFFFFFFFFFFFFF(uint64_t 最大值)。它们参与每个价格档位的成交量计算,但本身永远不会被选为 IAP。
TSE 最小价格变动单位表
TSE 根据证券分类有两个最小价格变动(tick size)表:
表 3:TOPIX500 成分股(更细 tick)
| 价格区间(¥) | Tick(¥) |
|---|---|
| ≤ 1,000 | 0.1 |
| ≤ 3,000 | 0.5 |
| ≤ 10,000 | 1.0 |
| ≤ 30,000 | 5.0 |
| ≤ 100,000 | 10.0 |
| ≤ 300,000 | 50.0 |
| ≤ 1,000,000 | 100.0 |
| ≤ 3,000,000 | 500.0 |
| ≤ 10,000,000 | 1,000.0 |
| ≤ 30,000,000 | 5,000.0 |
| > 30,000,000 | 10,000.0 |
表 1:其他证券(更粗 tick)
| 价格区间(¥) | Tick(¥) |
|---|---|
| ≤ 3,000 | 1.0 |
| ≤ 5,000 | 5.0 |
| ≤ 30,000 | 10.0 |
| ≤ 50,000 | 50.0 |
| ≤ 300,000 | 100.0 |
| ≤ 500,000 | 500.0 |
| ≤ 3,000,000 | 1,000.0 |
| ≤ 5,000,000 | 5,000.0 |
| ≤ 30,000,000 | 10,000.0 |
| ≤ 50,000,000 | 50,000.0 |
| > 50,000,000 | 100,000.0 |
关键含义:tick 大小是价格相关的。表 3 下 ¥1,000 的股票 tick 为 ¥0.1,但 ¥3,000 的股票 tick 为 ¥0.5。Itayose 算法必须在每个候选价格档位查找正确的 tick 大小。
五条件算法详解
现在进入核心算法。给定当前订单簿,Itayose 严格按照五个条件的顺序确定 IAP。每个条件逐步缩小候选价格集,直到只剩一个价格。
条件 1:有效价格区间
IAP 必须在最高委托价格加 1 个 tick 到最低委托价格减 1 个 tick 的范围内。
这定义了至少能发生部分交叉的价格区间:
max_valid = highest_order_price + tick_at(highest_order_price)
min_valid = lowest_order_price - tick_at(lowest_order_price)
“委托价格”包括买方和卖方。市价单不计入区间计算(它们没有限价)。
特殊情况:如果只存在市价单(没有任何限价单),则没有价格能满足条件 1,因此不会产生成交。
条件 2:最大成交量
在有效区间内的价格中,选择可成交数量最大的价格。
在任意价格 P:
- 可成交卖量 = 市价卖量 + 所有价格 ≤ P 的限价卖量
- 可成交买量 = 市价买量 + 所有价格 ≥ P 的限价买量
- 可成交数量 = min(卖量, 买量)
使用前缀和高效计算:
// 升序遍历:cum_ask_le[i] = 市价卖量 + 价格 ≤ price_levels[i] 的限价卖量
std::vector<uint64_t> cum_ask_le(N);
{
uint64_t running = mkt_ask_qty;
auto it = asks.begin();
for (size_t i = 0; i < N; ++i) {
while (it != asks.end() && it->first <= price_levels[i]) {
running += it->second;
++it;
}
cum_ask_le[i] = running;
}
}
// 降序遍历:cum_bid_ge[i] = 市价买量 + 价格 ≥ price_levels[i] 的限价买量
std::vector<uint64_t> cum_bid_ge(N);
{
uint64_t running = mkt_bid_qty;
auto it = bids.rbegin();
for (int i = (int)N - 1; i >= 0; --i) {
while (it != bids.rend() && it->first >= price_levels[i]) {
running += it->second;
++it;
}
cum_bid_ge[i] = running;
}
}
两趟遍历都是 O(N + M),其中 N = 价格档位数,M = 不同委托价格数。远优于朴素 O(N × M) 方法。
条件 3:最小不平衡量(剩余量)
在最大成交量的候选价格中,选择剩余量(surplus)最小的。
价格 P 处的剩余量:
surplus = |可成交卖量 - 可成交买量|
同时跟踪大小和方向:
int dir = (bid_vol > ask_vol) ? 1 : // 买方偏重
(ask_vol > bid_vol) ? -1 : // 卖方偏重
0; // 完全平衡
uint64_t imb = abs_diff(ask_vol, bid_vol);
条件 4:不平衡方向
如果所有剩余候选的不平衡方向相同,则:
- 全部卖方偏重 → 选最低候选价格
- 全部买方偏重 → 选最高候选价格
直觉:如果每个候选都是供大于求(卖方偏重),选最低价格以最大化买方参与;如果都是求大于供(买方偏重),选最高价格以最大化卖方参与。
bool all_buy_imb = std::all_of(c3_cands.begin(), c3_cands.end(),
[](const LevelStat& s){ return s.imbalance_dir == 1; });
bool all_sell_imb = std::all_of(c3_cands.begin(), c3_cands.end(),
[](const LevelStat& s){ return s.imbalance_dir == -1; });
if (all_sell_imb) return {lowest_price, matched_vol};
if (all_buy_imb) return {highest_price, matched_vol};
条件 5:参考价格兜底
当不平衡方向混合(部分买方偏重、部分卖方偏重)时,缩小区间并使用参考价格(基准价格)。
首先缩小候选区间:
narrowed_low = 最低卖方偏重价格
narrowed_high = 最高买方偏重价格
然后应用参考价格(base_price):
| 情况 | 规则 |
|---|---|
base_price > narrowed_high | 选 narrowed_high |
base_price < narrowed_low | 选 narrowed_low |
base_price 在区间内 | 选 base_price |
参考价格初始值为前收盘价(来自 venue JSON),但会在交易所广播 O 标签且 isItayose() == true 时实时更新:
void processOrderStatus(const uint8_t* tag_ptr, ..., const std::string& symbol) {
const TradingStatus* ts = reinterpret_cast<const TradingStatus*>(tag_ptr);
if (ts->isItayose()) {
uint64_t bcp = ts->getBookCenterPrice();
if (bcp != 0) market[symbol].setBasePrice(bcp);
}
}
这对盘中暂停至关重要:熔断后参考价格会变化,交易所通过此字段广播新值。
候选价格构建
一个微妙但重要的细节:Itayose 算法不仅要评估有订单的价格,还必须评估边界价格和间距大于 1 tick 的中间价格。
// 起点:min_valid、所有买方价格、所有卖方价格、max_valid
std::vector<uint64_t> price_levels;
price_levels.push_back(min_valid);
for (const auto& [px, _] : bids) price_levels.push_back(px);
for (const auto& [px, _] : asks) price_levels.push_back(px);
price_levels.push_back(max_valid);
// 填充间距大于 1 tick 的空隙
for (size_t i = 0; i + 1 < n; ++i) {
uint64_t lo = price_levels[i];
uint64_t hi = price_levels[i + 1];
uint64_t gap_tick = getTickSizeForPrice(lo, tick_table);
if (hi > lo + gap_tick) {
extras.push_back(lo + gap_tick); // 下边界 + 1 tick
uint64_t hi_tick = getTickSizeForPrice(hi, tick_table);
if (hi >= hi_tick)
extras.push_back(hi - hi_tick); // 上边界 - 1 tick
}
}
不填充空隙可能会错过位于两个委托价格之间的最优 Itayose 价格。
成交量取整
IAV 必须是合约乘数(unitOfTrading)的整数倍:
uint64_t matched_vol = (raw_matched_volume / lot_size) * lot_size;
向下截断——不能执行零碎手数。
完整示例
考虑这个简化订单簿:
| 方向 | 价格(¥) | 数量 |
|---|---|---|
| 买 | 市价 | 200 |
| 买 | 1,000 | 300 |
| 买 | 990 | 500 |
| 卖 | 市价 | 100 |
| 卖 | 1,010 | 400 |
| 卖 | 1,020 | 300 |
条件 1:有效区间 = [1,000 + 1 tick, 1,010 - 1 tick] = [1,001, 1,009]
条件 2:在各候选价格计算可成交数量:
在价格 1,010:
- 累计卖量(价格 ≤ 1,010)= 100(市价)+ 400 = 500
- 累计买量(价格 ≥ 1,010)= 200(市价)+ 300 = 500
- 成交量 = 500,剩余量 = 0
在价格 1,000:
- 累计卖量(价格 ≤ 1,000)= 100(市价)= 100
- 累计买量(价格 ≥ 1,000)= 200(市价)+ 300 + 500 = 1,000
- 成交量 = 100,剩余量 = 900
最大成交量为 500,在价格 1,010,剩余量为零。条件 3 直接解决:IAP = ¥1,010,IAV = 500。
板寄せ vs. A 股集合竞价
TSE 的板寄せ与 A 股集合竞价有重要区别:
| 特征 | TSE 板寄せ | A 股集合竞价 |
|---|---|---|
| 条件 1 | 基于 tick 的委托极值区间 | 同一原则 |
| 条件 2 | 最大可成交量 | 相同 |
| 条件 3 | 最小剩余量 | 相同 |
| 条件 4 | 方向判定有特定规则 | 类似但平局规则不同 |
| 条件 5 | 参考价格三种情况 | 取前收盘价 |
| Tick 大小 | 价格相关,两张表 | 按证券类别固定 |
| 市价单 | 显式哨兵价格 | 集合竞价不支持 |
| MBO 数据 | 完整逐笔委托可见 | 集合竞价期间仅 MBP |
TSE 最独特的特征是价格相关的 tick 大小结合显式市价单处理——A 股集合竞价中两者都不存在。
完整处理流程
整合在一起:
1. 加载 venue JSON → 证券信息(base_price, tick_table, lot_size)
2. 对每个 pcap 文件:
a. 解析 Ethernet → IP → UDP → FLEX 载荷
b. 按 (group, reboots, seq_num) 去重
c. 对数据包中的每个标签:
A → addOrder()
D → deleteOrder()
E/C → executeOrder()
O → 若板寄せ模式则 updateBasePrice()
R → reset() 所有订单簿
3. 对每个证券的订单簿:
a. calculateIndicativeMatch() → (IAP, IAV)
4. 输出 indicative_match.csv
常见陷阱
1. 边界处的价格相关 Tick
在 max_valid / min_valid 边界使用固定 tick 是错误的。表 3 下 ¥1,000 和 ¥1,001 的 tick 不同。必须在具体价格档位调用 getTickSizeForPrice()。
2. 市价单的成交量计算
市价单必须同时加入卖方累计和(升序)和买方累计和(降序)。常见错误是只加一次。
3. 价格档位间的空隙填充
如果买价在 ¥1,000、卖价在 ¥1,050,tick 为 ¥1,则中间有 49 个候选价格需要评估。遗漏它们可能产生错误的 IAP。
4. 成交量截断
IAV 必须向下取整到 lot_size 的倍数,而非四舍五入。向上取整意味着执行了不可匹配的份额。
5. 盘中暂停时的基准价格更新
参考价格不是静态的——交易所在板寄せ模式下广播 O 标签时会更新。仅使用初始 venue JSON 的 base_price 会导致盘中拍卖结果错误。
6. 大端序字段
FLEX 数据包中所有多字节字段都是大端序。在小端机器上忘记 __builtin_bswap32 / __builtin_bswap64 会静默产生错误数据。
输出
应用程序生成 indicative_match.csv,每个证券一行:
symbol, iap, iav
6954, 4165.0000, 332200
6857, 8779.0000, 294200
6486, 2010.0000, 1900
1382, 1743.0000, 300
其中:
- iap:理论拍卖价格,单位日元(4 位小数)
- iav:理论拍卖成交量,单位股(lot_size 的整数倍)
价格为 0.0000 表示该证券在处理的 pcap 数据中没有板寄せ时段(例如仅连续交易)。
总结
TSE 板寄せ算法是五级级联筛选:
- 条件 1 缩小到有效价格区间
- 条件 2 选择最大可成交量的价格
- 条件 3 选择最小剩余量的价格
- 条件 4 使用不平衡方向(全买偏 → 最高,全卖偏 → 最低)
- 条件 5 在缩小区间内回退到参考价格
每个条件充当过滤器,一旦只剩一个价格算法就终止。价格相关 tick 大小、市价单处理和参考价格机制的组合,使 TSE 的板寄せ比典型的交易所集合竞价更加精细。
对于在 TSE 运营的交易公司,正确实现板寄せ不是可选项——理论拍卖价格直接影响订单路由、盘前分析和盘后合规。条件 5 中一个 tick 的错误可能在开盘拍卖中导致数百万日元的执行偏差。
参考
- TSE 板寄せ条件与定价示例(官方规范文档)
- TSE Tick Size Schedule
- TSE FLEX Full MBO 历史数据格式规范