yuqi-zheng

TSE Itayose Auction Pricing: The Five-Condition Algorithm


The Tokyo Stock Exchange (TSE) opens and closes every trading day with an auction — not continuous matching. This auction mechanism, called Itayose (板寄せ, literally “gathering orders on a board”), collects all orders and determines a single clearing price that maximizes execution volume while respecting a precise sequence of five tiebreaking conditions.

Most exchanges use some form of auction at open and close, but TSE’s five-condition specification is uniquely detailed and deterministic. Understanding it is essential for anyone building TSE market data systems, and the algorithm itself is a fascinating case study in how financial microstructure balances competing objectives.

This article walks through the complete Itayose algorithm based on TSE’s official specification, with a production C++ implementation that processes real FLEX Full MBO historical data from pcap files.


Itayose vs. Continuous Matching

In continuous matching (Zaraba), orders execute immediately when they cross the spread. Price is determined order-by-order.

In Itayose, no execution happens until the exchange collects all orders, then computes a single Indicative Auction Price (IAP) and Indicative Auction Volume (IAV). Every order that can be filled at the IAP is executed simultaneously.

TSE uses Itayose in three scenarios:

  • Opening auction (9:00 AM)
  • Closing auction (3:30 PM)
  • Special quote during intraday halts (circuit breakers, volatility interruptions)

The FLEX market data feed signals Itayose mode via the O (Order Status) tag with pricing_method = 1.


The FLEX Protocol: Building the Order Book

Before we can compute the Itayose price, we need to reconstruct the order book from raw network packets.

Packet Structure

TSE FLEX Full MBO data arrives over UDP multicast. Each packet has a FlexPacketHeader followed by TLV (Type-Length-Value) encoded tags:

┌─────────────────────────────────────────┐
│ FlexPacketHeader (25 bytes)             │
├─────────────────────────────────────────┤
│ Tag 1: [length][type][data...]          │
│ Tag 2: [length][type][data...]          │
│ ...                                     │
└─────────────────────────────────────────┘

The header fields:

FieldSizeEndianPurpose
multicast_group1 byteChannel identifier
sys_reboots1 byteReboot counter (for dedup)
seq_num4 bytesBigSequence number
issue_code12 bytesASCIISymbol, left-padded with spaces
msg_count1 byteNumber of tags in this packet

Tag Types

TagNameAction
AAdd OrderInsert into order book (limit or market)
DDelete OrderRemove (cancel, expire, or qty-reduction)
EExecutionPartial fill
CExecution with PriceFill at a specific price
OOrder StatusTrading mode (Itayose/Zaraba) + reference price
TTimeTimestamp
RResetClear all order books
LCommunication ControlInformational

MBO Order Book

The FLEX Full feed provides Market By Order (MBO) data — every individual order is visible, not just aggregated price levels. We maintain an unordered_map<orderId, Order>:

struct Order {
    uint64_t order_id;
    uint64_t price;
    uint64_t qty;
    char     side;       // 'B' or 'S'
    uint32_t time_usec;  // priority timestamp
};

Market orders use a sentinel price 0xFFFFFFFFFFFFFFFF — they participate at every price level.

Modification Handling

TSE encodes order modifications as a Delete + Add pair with modification_flag = 1:

  1. D(orderId, flag=1) — saves the original order’s time_usec in a PendingMod struct
  2. A(orderId, flag=1) — inherits the saved time_usec, preserving queue priority

This is crucial: partial quantity reductions keep the original time priority, while price changes or full cancels do not.

Packet Deduplication

FLEX packets may be retransmitted. We track seen packets using a composite key:

struct PacketKey {
    uint8_t  group;    // multicast_group
    uint8_t  reboots;  // sys_reboots
    uint32_t seq;      // sequence number
};

VLAN Support

Some capture environments insert 802.1Q VLAN tags (4 extra bytes in the Ethernet header). We detect this by checking the EtherType at offset 12:

uint16_t ether_type = ntohs(*(const uint16_t*)(packet + 12));
if (ether_type == 0x8100) return 18;  // VLAN-tagged
return 14;                             // Standard Ethernet

Price Precision: Integer × 10,000

TSE wire format represents prices as 64-bit big-endian integers scaled by 10,000. This eliminates floating-point errors entirely:

Wire ValueActual Price
17770000¥1,777.0000
34565000¥3,456.5000
6746000¥674.6000

All arithmetic in the Itayose algorithm operates on these scaled integers. We only convert to decimal at output time.

Market Order Sentinel

Market orders carry price = 0xFFFFFFFFFFFFFFFF (the maximum uint64_t value). They participate in volume computation at every price level but are never selected as the IAP themselves.


TSE Tick Size Tables

TSE has two tick size schedules depending on the instrument’s classification:

Table 3: TOPIX500 Constituents (Finer Ticks)

Price Range (¥)Tick Size (¥)
≤ 1,0000.1
≤ 3,0000.5
≤ 10,0001.0
≤ 30,0005.0
≤ 100,00010.0
≤ 300,00050.0
≤ 1,000,000100.0
≤ 3,000,000500.0
≤ 10,000,0001,000.0
≤ 30,000,0005,000.0
> 30,000,00010,000.0

Table 1: Other Issues (Coarser Ticks)

Price Range (¥)Tick Size (¥)
≤ 3,0001.0
≤ 5,0005.0
≤ 30,00010.0
≤ 50,00050.0
≤ 300,000100.0
≤ 500,000500.0
≤ 3,000,0001,000.0
≤ 5,000,0005,000.0
≤ 30,000,00010,000.0
≤ 50,000,00050,000.0
> 50,000,000100,000.0

Key implication: tick size is price-dependent. A ¥1,000 stock under Table 3 has a ¥0.1 tick, but a ¥3,000 stock has a ¥0.5 tick. The Itayose algorithm must look up the correct tick size at each candidate price level.

Source: TSE Tick Size Schedule


The Five Itayose Conditions

Now the core algorithm. Given the current order book, Itayose determines the IAP by applying five conditions in strict order. Each condition narrows the set of candidate prices until a single price remains.

Condition 1: Valid Price Range

The IAP must be within one tick above the highest order price to one tick below the lowest order price.

This defines the range where at least some cross can occur:

max_valid = highest_order_price + tick_at(highest_order_price)
min_valid = lowest_order_price  - tick_at(lowest_order_price)

“Order price” includes both bids and asks. Market orders don’t count for range calculation (they have no limit price).

Special case: If only market orders exist (no limit orders at all), no price can satisfy Condition 1, so there is no transaction.

Condition 2: Maximum Executable Volume

Among prices in the valid range, select those where the executable volume is maximized.

At any price P:

  • Executable ask volume = market ask qty + all asks at price ≤ P
  • Executable bid volume = market bid qty + all bids at price ≥ P
  • Executable volume = min(ask_volume, bid_volume)

We compute this efficiently using prefix sums:

// Ascending pass: cum_ask_le[i] = mkt_ask + asks at price ≤ 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;
    }
}

// Descending pass: cum_bid_ge[i] = mkt_bid + bids at price ≥ 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;
    }
}

Both passes are O(N + M) where N = number of price levels and M = number of distinct order prices. This is far more efficient than a naive O(N × M) approach.

Condition 3: Minimum Imbalance (Surplus)

Among prices with maximum volume, select those with the smallest surplus.

Surplus (imbalance) at price P:

surplus = |ask_volume_at_P - bid_volume_at_P|

We track both the magnitude and direction:

int dir = (bid_vol > ask_vol) ? 1 :    // buy-heavy
          (ask_vol > bid_vol) ? -1 :    // sell-heavy
          0;                             // perfectly balanced
uint64_t imb = abs_diff(ask_vol, bid_vol);

Condition 4: Imbalance Direction

If all remaining candidates have the same imbalance direction, choose:

  • All sell-heavy → lowest candidate price
  • All buy-heavy → highest candidate price

Intuition: if every candidate has more supply than demand (sell-heavy), pick the lowest price to maximize buyer participation. If every candidate has more demand than supply (buy-heavy), pick the highest price to maximize seller participation.

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};

Condition 5: Reference Price Fallback

When imbalances are mixed (some buy-heavy, some sell-heavy), narrow the range and use the reference price (base price).

First, narrow the candidate range:

narrowed_low  = lowest sell-heavy price
narrowed_high = highest buy-heavy price

Then apply the reference price (base_price):

CaseRule
base_price > narrowed_highChoose narrowed_high
base_price < narrowed_lowChoose narrowed_low
base_price within rangeChoose base_price

The reference price is initially the previous day’s closing price (from the venue JSON), but gets updated in real-time when the exchange broadcasts an O tag with 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);
    }
}

This is critical for intraday halts: after a circuit breaker, the reference price changes and the exchange broadcasts the new value.


Candidate Price Construction

A subtle but important detail: the Itayose algorithm must evaluate not just the prices where orders sit, but also the boundary prices and prices in gaps wider than one tick.

// Start with: min_valid, all bid prices, all ask prices, 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);

// Fill gaps wider than one 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);   // Price one tick above lower boundary
        uint64_t hi_tick = getTickSizeForPrice(hi, tick_table);
        if (hi >= hi_tick)
            extras.push_back(hi - hi_tick); // Price one tick below upper boundary
    }
}

Without gap filling, we might miss the optimal Itayose price that sits between two order levels.


Volume Rounding

The IAV (Indicative Auction Volume) must be a multiple of the instrument’s lot size (unitOfTrading):

uint64_t matched_vol = (raw_matched_volume / lot_size) * lot_size;

This truncates downward — you can’t execute a partial lot.


Worked Example

Consider this simplified order book:

SidePrice (¥)Quantity
BidMarket200
Bid1,000300
Bid990500
AskMarket100
Ask1,010400
Ask1,020300

Condition 1: Valid range = [1,000 + 1 tick, 1,010 - 1 tick] = [1,001, 1,009]

Condition 2: Evaluate executable volume at each candidate price:

PriceCum AskCum BidExec VolSurplusDirection
1,0011001,000100900Buy
1,0001001,000100900Buy
1,0105005005000Balanced
1,009100500100400Buy

Wait — let me redo this correctly. At price 1,010:

  • Cum ask (price ≤ 1,010) = 100 (mkt) + 400 = 500
  • Cum bid (price ≥ 1,010) = 200 (mkt) + 300 = 500
  • Exec vol = 500, surplus = 0

At price 1,000:

  • Cum ask (price ≤ 1,000) = 100 (mkt) = 100
  • Cum bid (price ≥ 1,000) = 200 (mkt) + 300 + 500 = 1,000
  • Exec vol = 100, surplus = 900

Maximum volume is 500 at price 1,010, with zero surplus. Condition 3 immediately resolves: IAP = ¥1,010, IAV = 500.


Itayose vs. A-Share Call Auction

TSE’s Itayose differs from the A-Share call auction in important ways:

FeatureTSE ItayoseA-Share Call Auction
Condition 1Tick-based range from order extremesSame principle
Condition 2Maximum executable volumeSame
Condition 3Minimum surplusSame
Condition 4Direction check with specific rulesSimilar but different tiebreaker
Condition 5Reference price with three casesClose price of previous day
Tick sizePrice-dependent, two tablesFixed per instrument class
Market ordersExplicit sentinel priceNot supported in call auction
MBO dataFull order visibilityOnly MBP in call auction

The most distinctive TSE feature is the price-dependent tick size combined with explicit market order handling — neither exists in the A-Share call auction.


Complete Processing Pipeline

Putting it all together:

1. Load venue JSON → symbol info (base_price, tick_table, lot_size)
2. For each pcap file:
   a. Parse Ethernet → IP → UDP → FLEX payload
   b. Deduplicate by (group, reboots, seq_num)
   c. For each tag in packet:
      A → addOrder()
      D → deleteOrder()
      E/C → executeOrder()
      O → updateBasePrice() if Itayose
      R → reset() all books
3. For each symbol's order book:
   a. calculateIndicativeMatch() → (IAP, IAV)
4. Write indicative_match.csv

Common Pitfalls

1. Price-Dependent Tick Size at Boundaries

Using a fixed tick size at the max_valid / min_valid boundaries is wrong. The tick size at ¥1,000 is different from ¥1,001 under Table 3. Always call getTickSizeForPrice() at the specific price level.

2. Market Order Handling in Volume Calculation

Market orders must be added to both the ask cumulative sum (ascending) and bid cumulative sum (descending). A common mistake is to add them only once.

3. Gap Filling Between Price Levels

If bids sit at ¥1,000 and asks at ¥1,050 with a ¥1 tick, there are 49 candidate prices between them that must be evaluated. Missing them can produce the wrong IAP.

4. Volume Truncation

The IAV must be rounded down to a multiple of lot_size, not rounded to nearest. Rounding up would imply executing more shares than are actually matchable.

5. Base Price Updates During Intraday Halts

The reference price isn’t static — it changes when the exchange broadcasts an O tag during Itayose mode. Using only the initial venue JSON base price will produce wrong results for intraday auctions.

6. Big-Endian Fields

All multi-byte fields in FLEX packets are big-endian. Forgetting __builtin_bswap32 / __builtin_bswap64 will silently produce wrong data on little-endian machines.


Output

The application produces indicative_match.csv with one row per symbol:

symbol, iap, iav
6954, 4165.0000, 332200
6857, 8779.0000, 294200
6486, 2010.0000, 1900
1382, 1743.0000, 300

Where:

  • iap: Indicative Auction Price in yen (4 decimal places)
  • iav: Indicative Auction Volume in shares (multiple of lot_size)

A price of 0.0000 means the symbol had no Itayose session in the processed pcap data (e.g., only continuous trading).


Summary

The TSE Itayose algorithm is a five-level cascade:

  1. Condition 1 narrows to the valid price range
  2. Condition 2 picks prices with maximum executable volume
  3. Condition 3 picks prices with minimum surplus
  4. Condition 4 uses imbalance direction (all-buy → highest, all-sell → lowest)
  5. Condition 5 falls back to the reference price within the narrowed range

Each condition acts as a filter, and the algorithm terminates as soon as a single price remains. The combination of price-dependent tick sizes, market order handling, and the reference price mechanism makes TSE’s Itayose more nuanced than typical exchange call auctions.

For trading firms operating on TSE, correctly implementing Itayose is not optional — the indicative auction price directly affects order routing, pre-trade analytics, and post-trade compliance. A single tick error in Condition 5 can cascade into millions of yen of execution difference at the opening auction.


References

  • TSE Itayose Conditions and Pricing Examples (official specification document)
  • TSE Tick Size Schedule
  • TSE FLEX Full MBO Historical Data Format Specification