MatchingEngineSimulator#
In high-frequency strategies, it is common to encounter cases where a strategy that performs well in backtesting delivers subpar results in live trading. One major reason for this discrepancy is the underestimation of transaction costs. To more accurately reflect real trading costs, a matching engine simulator can be introduced during backtesting. Python Swordfish provides a matching engine simulator for high-frequency market data, enabling users to better evaluate and anticipate strategy performance in live trading, and make corresponding optimizations.
Order Matching Simulation#
The matching engine simulator operates as a plugin service to simulate order placement and canceling at specific time points. The engine processes these actions and generates corresponding trade outcomes.
The engine takes two primary inputs: the market data and the orders placed by the user. It simulates order processing delays by modifying the timestamp of each incoming order, adding a preset latency value. Execution results—including partial fills, rejections, and cancellations—are written to the trade detail table, while unfilled portions are retained for subsequent matching or cancellation.
Key features of the matching engine simulator include:
Configurable parameters such as fill ratio and matching delay.
When multiple orders of the same direction (buy/sell) exist, the engine follows the price-time priority principle.
In tick-by-tick mode, the engine builds the order book in real time. When a user order arrives, it is immediately matched against the order book to generate fills. Any unfilled portion will continue to match with subsequent market and user orders, still following price-time priority.
In snapshot mode, user orders are matched against the current order book. Depending on configuration, unfilled portions may continue to match with subsequent snapshots. Two matching modes are available:
Matching mode 1: Match against the latest trade price and the counterparty order book according to a configured ratio.
Matching mode 2: Match against a list of trades within a specified interval and the counterparty order book.
Supported order types include Limit Orders, Market Orders, and Cancel Orders. Different combinations of order type and market data type correspond to different matching rules, which are described in later sections.
Market Data Requirements for the Matching Engine#
When creating a matching engine simulator, you must specify the schema of the input tables (market data and user orders) and the output table (trade details).
For input tables, the engine requires specific field names, but the column order is not restricted. If the field names in a user-defined market data table differ from those required by the engine, you can use quote_col_map in the config to define the mapping. Similarly, if the field names in a user-defined user order table differ, you can define the mapping with user_order_col_map.
For output tables, the simulated matching engine does not require specific field names, but it will validate whether the field data types and structure are correct.
The following sections describe the mandatory fields or data types for input and output tables. The schema may vary slightly across different asset classes—please refer to the manual for details.
Market Snapshot Data#
In snapshot mode, the input table must include the following columns. The columns tradePrice and tradeQty are required only in matching mode 2; they are optional in matching mode 1.
Name |
Type |
Description |
|---|---|---|
symbol |
SYMBOL |
Security identifier |
symbolSource |
SYMBOL |
Securities market (e.g., XSHE, XSHG, CFFEX_BOND) |
timestamp |
TIMESTAMP |
Timestamp |
lastPrice |
DOUBLE |
Last price |
upLimitPrice |
DOUBLE |
Upper limit price |
downLimitPrice |
DOUBLE |
Lower limit price |
totalBidQty |
LONG |
Total bid quantity within the interval |
totalOfferQty |
LONG |
Total offer quantity within the interval |
bidPrice |
DOUBLE[] |
List of bid prices |
bidQty |
LONG[] |
List of bid quantities |
offerPrice |
DOUBLE[] |
List of offer prices |
offerQty |
LONG[] |
List of offer quantities |
tradePrice |
DOUBLE[] |
List of trade prices within the interval |
tradeQty |
LONG[] |
List of trade quantities within the interval |
Examples:
quote_schema = {
'symbol': "STRING",
'symbolSource': "STRING",
'timestamp': "TIMESTAMP",
'lastPrice': "DOUBLE",
'highestPrice2': "DOUBLE",
'lowestPrice2': "DOUBLE",
'highestPrice': "DOUBLE",
'lowestPrice': "DOUBLE",
'openPrice': "DOUBLE",
'preClosePrice': "DOUBLE",
'upLimitPrice': "DOUBLE",
'downLimitPrice': "DOUBLE",
'avgBidPrice': "DOUBLE",
'avgOfferPrice': "DOUBLE",
'totalBidQty': "LONG",
'totalOfferQty': "LONG",
'bidPrice': "DOUBLE[]",
'bidQty': "LONG[]",
'offerPrice': "DOUBLE[]",
'offerQty': "LONG[]",
'tradePrice': "DOUBLE[]",
'tradeQty': "LONG[]",
}
Tick Market Data#
The market data table must include the following fields:
Name |
Type |
Description |
|---|---|---|
symbol |
SYMBOL |
Security identifier |
symbolSource |
SYMBOL |
Securities market (e.g., XSHE, XSHG, CFFEX_BOND) |
timestamp |
TIMESTAMP |
Timestamp |
sourceType |
INT |
Source type: 0 = order; 1 = transaction |
orderType |
INT |
Order/Transaction type:
|
price |
DOUBLE |
Order price |
qty |
LONG |
Order quantity |
buyNo |
LONG |
For transaction: corresponding original order; For order: populated with buyNo |
sellNo |
LONG |
For transaction: corresponding original order; For order: populated with sellNo |
direction |
INT |
Trade direction: 1 = Buy; 2 = Sell |
seqNum |
LONG |
Sequence number of tick-by-tick data |
Examples:
quote_schema = {
'symbol': "STRING",
'symbolSource': "STRING",
'timestamp': "TIMESTAMP",
'sourceType': "INT",
'orderType': "INT",
'price': "DOUBLE",
'qty': "LONG",
'buyNo': "LONG",
'sellNo': "LONG",
'direction': "INT",
'seqNum': "LONG",
}
Stock/Futures & Options Snapshot Quotes#
For stock snapshots, when matching_mode=2 is specified, the market data table must include the fields tradePrice (trade price) and tradeQty (trade quantity). When matching_mode=1 is specified, the fields tradePrice and tradeQty are optional.
For futures and options snapshots, the table is required to include the fields highPrice (highest price) and lowPrice (lowest price).
Name |
Type |
Description |
|---|---|---|
symbol |
SYMBOL |
Stock ID |
symbolSource |
SYMBOL |
Exchange or market source |
timestamp |
TIMESTAMP |
Timestamp |
lastPrice |
DOUBLE |
Last traded price |
upLimitPrice |
DOUBLE |
Upper limit price |
downLimitPrice |
DOUBLE |
Lower limit price |
totalBidQty |
LONG |
Total buy quantity in the interval |
totalOfferQty |
LONG |
Total sell quantity in the interval |
bidPrice |
DOUBLE[] |
List of buy prices |
bidQty |
LONG[] |
List of buy quantities |
offerPrice |
DOUBLE[] |
List of sell prices |
offerQty |
LONG[] |
List of sell quantities |
tradePrice |
DOUBLE[] |
List of trade prices (required when matching_mode=2) |
tradeQty |
LONG[] |
List of trade quantities (required when matching_mode=2) |
highPrice |
DOUBLE |
Highest price (required for futures and options) |
lowPrice |
DOUBLE |
Lowest price (required for futures and options) |
Interbank Spot Bond Snapshot#
Name |
Type |
Description |
|---|---|---|
symbol |
SYMBOL |
Instrument code |
symbolSource |
SYMBOL |
Market: Interbank
|
timestamp |
TIMESTAMP |
Timestamp |
bidSettlType |
INT[] |
Buy settlement type |
bidMDEntrySize |
LONG[] |
Buy order quantity (CNY) |
bidMDEntryPx |
DOUBLE[] |
Buy order price (clean price, CNY) |
bidYield |
DOUBLE[] |
Buy yield to maturity |
bidParty |
columnar tuple |
Buy quoting party (SSE bonds only) |
askSettlType |
INT[] |
Sell settlement type |
askMDEntrySize |
LONG[] |
Sell order quantity (CNY) |
askMDEntryPx |
DOUBLE[] |
Sell order price (clean price, CNY) |
askYield |
DOUBLE[] |
Sell yield to maturity |
askParty |
columnar tuple |
Sell quoting party (SSE bonds only) |
tradePrice |
DOUBLE[] |
List of trade prices in the interval |
tradeQty |
LONG[] |
List of trade quantities in the interval |
settlType |
INT[] |
Settlement type |
yield |
DOUBLE[] |
Yield to maturity |
Crypto Snapshot Market Data#
Name |
Type |
Description |
|---|---|---|
symbol |
SYMBOL |
Instrument code |
symbolSource |
SYMBOL |
Exchange |
timestamp |
TIMESTAMP |
Timestamp |
lastPrice |
DECIMAL128 |
Last traded price |
upLimitPrice |
DECIMAL128 |
Upper limit price |
downLimitPrice |
DECIMAL128 |
Lower limit price |
totalBidQty |
DECIMAL128 |
Total buy quantity in the interval |
totalOfferQty |
DECIMAL128 |
Total sell quantity in the interval |
bidPrice |
DECIMAL128[] |
List of buy prices |
bidQty |
DECIMAL128[] |
List of buy quantities |
offerPrice |
DECIMAL128[] |
List of sell prices |
offerQty |
DECIMAL128[] |
List of sell quantities |
highPrice |
DECIMAL128 |
Highest price |
lowPrice |
DECIMAL128 |
Lowest price |
tradePrice |
DECIMAL128[] |
List of trade prices (matching mode 2) |
tradeQty |
DECIMAL128[] |
List of trade quantities (matching mode 2) |
Minute-level/Daily Market Data#
For cryptocurrency minute-level and daily market data, the symbolSource field is required. For futures and stock minute-level and daily data, the symbolSource field is optional.
For cryptocurrency data, both price and volume fields use the DECIMAL128 data type.
Field |
Type |
Description |
|---|---|---|
symbol |
SYMBOL |
Instrument code |
symbolSource |
SYMBOL |
Market (required for minute-level and daily cryptocurrency data) |
tradeTime |
TIMESTAMP |
Timestamp |
open |
DOUBLE |
Opening price |
low |
DOUBLE |
Lowest price |
high |
DOUBLE |
Highest price |
close |
DOUBLE |
Closing price |
volume |
LONG |
Trading volume |
amount |
DOUBLE |
Trading value (turnover) |
upLimitPrice |
DOUBLE |
Upper limit price |
downLimitPrice |
DOUBLE |
Lower limit price |
User Order Schema for the Matching Engine#
Similar to market schema, user order schema depends on instrument type:
Stock user orders
Futures/options user orders
Crypto user orders
Interbank bond user orders
Stock#
Name |
Type |
Description |
|---|---|---|
symbol |
STRING |
Stock symbol. Invalid for canceled orders; use |
symbolSource |
SYMBOL |
Exchange |
timestamp |
TIMESTAMP |
Timestamp |
orderType |
INT |
Order type:
|
price |
DOUBLE |
Order price |
orderQty |
LONG |
Order quantity |
direction |
INT |
1 = Buy, 2 = Sell |
orderId |
LONG |
User order ID, only relevant for cancellations |
Futures & Options Snapshot#
Name |
Type |
Description |
|---|---|---|
symbol |
SYMBOL |
Futures contract symbol |
symbolSource |
SYMBOL |
Exchange code |
timestamp |
TIMESTAMP |
Timestamp |
orderType |
INT |
Order type (default 5 = Limit order):
|
price |
DOUBLE |
Order price |
stopPrice |
DOUBLE |
Stop-loss or take-profit price |
orderQty |
LONG |
Order quantity |
direction |
INT |
Buy/sell direction:
|
timeInForce |
INT |
Order validity:
|
orderId |
LONG |
User order ID, only relevant for cancellations |
Crypto User Orders Table#
Name |
Type |
Description |
|---|---|---|
symbol |
SYMBOL |
Instrument code |
symbolSource |
SYMBOL |
Exchange code |
timestamp |
TIMESTAMP |
Timestamp |
orderType |
INT |
Minute/Daily frequency mode:
|
price |
DECIMAL128 |
Order price |
stopPrice |
DECIMAL128 |
Stop-loss or take-profit price |
orderQty |
DECIMAL128 |
Order quantity |
direction |
INT |
Buy/sell direction:
|
orderId |
LONG |
User order ID, only relevant for cancellations |
Interbank Spot Bond#
Name |
Type |
Description |
|---|---|---|
symbol |
SYMBOL |
Instrument |
time |
TIMESTAMP |
Timestamp |
orderType |
INT |
|
settlType |
INT |
Settlement type |
bidYield |
DOUBLE |
Bid order yield to maturity |
bidPrice |
DOUBLE |
Bid order price |
bidQty |
LONG |
Bid order quantity |
askYield |
DOUBLE |
Ask order yield to maturity |
askPrice |
DOUBLE |
Ask order price |
askQty |
LONG |
Ask order quantity |
direction |
INT |
|
orderID |
LONG |
User order ID, only relevant for cancellations |
channel |
STRING |
Matching channel: currently supports |
Output Table Schema for the Matching Engine#
The trade detail output table tradeOutputTable stores the execution results of orders written by the engine. The table can be defined following the order below (the table name can be modified, but each field has a specific meaning, so the order of field types must not be changed). Note that when the configuration output_queue_position is set to 1, the following five fields are enabled: openVolumeWithBetterPrice, openVolumeWithWorsePrice, openVolumeAtOrderPrice, priorOpenVolumeAtOrderPrice, and depthWithBetterPrice; the last three fields are only enabled if output_time_info is set to 1.
Name |
Type |
Description |
|---|---|---|
orderId |
LONG |
User order ID for the executed order |
symbol |
STRING |
Security ID |
direction |
INT |
1 (Buy), 2 (Sell) |
sendTime |
TIMESTAMP |
Order send time |
orderPrice |
DOUBLE |
Order price |
orderQty |
LONG |
Order quantity |
tradeTime |
TIMESTAMP |
Trade time |
tradePrice |
DOUBLE |
Trade price |
tradeQty |
LONG |
Trade quantity |
orderStatus |
INT |
User order completion status:
|
sysReceiveTime |
NANOTIMESTAMP |
System time when order is received |
openVolumeWithBetterPrice |
LONG |
Total open volume at prices better than the order price |
openVolumeWithWorsePrice |
LONG |
Total open volume at prices worse than the order price |
openVolumeAtOrderPrice |
LONG |
Total open volume at the same price as the order |
priorOpenVolumeAtOrderPrice |
LONG |
Total open volume at the same price as the order and earlier than this order |
depthWithBetterPrice |
INT |
Depth levels of open orders better than the order price |
receiveTime |
TIMESTAMP |
Latest market time when order is received |
startMatchTime |
NANOTIMESTAMP |
Match start time |
endMatchTime |
NANOTIMESTAMP |
Match end time |
Examples:
order_detail_output = sf.table(types={
'orderId': "LONG",
'symbol': "STRING",
'direction': "INT",
'sendTime': "TIMESTAMP",
'orderPrice': "DOUBLE",
'orderQty': "LONG",
'tradeTime': "TIMESTAMP",
'tradePrice': "DOUBLE",
'tradeQty': "LONG",
'orderStatus': "INT",
'sysReceiveTime': "NANOTIMESTAMP",
})
Configuration Class#
The MatchingEngineSimulatorConfig class defines simulator behavior and is
passed via the config parameter. You can set configuration in three ways:
Dictionary
config = MatchingEngineSimulatorConfig({
"orderbook_matching_ratio": 0.12,
"matching_mode": 2
})
Attribute assignment
config = MatchingEngineSimulatorConfig()
config.latency = 0
Key-value access
config = MatchingEngineSimulatorConfig()
config["latency"] = 0
Configuration Options#
Option |
Description |
|---|---|
quote_col_map |
Maps user quote table column names to internal field names. Only needed if column names differ from system expectations. |
user_order_col_map |
Maps user order table columns to internal engine fields. Keys are internal names, values are user field names. Only needed if names differ. |
depth |
Order book depth (5-50) |
output_interval |
Minimum interval for order book output, in milliseconds |
latency |
Simulated delay (ms) to mimic order processing:
|
orderbook_matching_ratio |
Percentage of trades matched against order book; must be ≥ 0 |
matching_mode |
Matching mode. Snapshot/minute/daily modes handled separately:
|
matching_ratio |
Interval trade ratio in snapshot mode. Defaults to orderbook_matching_ratio |
order_details_and_snapshot_output |
Output composite table including order details and snapshots |
snapshot_output |
Output order book snapshots |
output_time_info |
Include quote receive time, match start time, and match end time. Default = False |
output_reject_details |
Include rejection reasons for order/cancel rejections. Default = False |
output_queue_position |
Queue position output mode:
Adds 5 metrics: unfilled volumes better/worse/equal to order price, earlier equal-price volume, number of better levels |
output_order_confirmation |
Output order confirmations. Default = True |
output_order_trade_flag |
If True, indicates whether order is passive (maker) or aggressive (taker). Default = False |
cpu_id |
CPU core ID to bind engine thread (first received quote/order) |
user_defined_order_id |
If True, user-specified orderId used as external ID. Adds userOrderId column. Default = False |
order_by_price |
Interbank bond mode: True = match by price; False = match by yield-to-maturity |
immediate_order_confirmation |
If True, return order confirmation immediately (interbank bond snapshot only). Default = False |
immediate_cancel |
If True, cancel immediately (interbank bond snapshot only). Default = False |
trade_in_lots |
Interbank bond snapshot:
|
outputSeqNum |
If True, adds seqNum column to order detail table (auto-increment 1…N) |
Matching Rules#
The matching engine simulator adheres to exchange-compliant matching rules while offering flexible configuration options to accurately model order execution processes. It enables users to create realistic simulations of order fulfillment, considering factors such as latency, order book, percentage of order filled, etc.
The following sections describe the matching rules for limit orders and market orders.
Limit Order Matching Rules#
Limit orders are matched according to the specific rules based on the type of market data.
Snapshot Market Data#
When the market data is provided as snapshots, the matching process proceeds as follows:
Limit Order Matching for Level N:
Buy Orders: When the order price ≥ Level 1 ask price, the order is matched sequentially against the levels of the ask side.The trade price is taken from the corresponding ask level, and the trade quantity is the minimum of: (ask level quantity * level fill ratio) and (order quantity - already filled quantity). If the order is not fully filled, it enters the pending order queue. At this price level, the order is placed at the front (i.e., preceding quantity is 0), and then enters the pending order matching phase.
Sell Orders: When the order price ≤ Level 1 bid price, the order is matched sequentially against the levels of the bid side. The trade price is taken from the corresponding bid level, and the trade quantity is the minimum of: (bid level quantity * level fill ratio) and (order quantity - already filled quantity). If the order is not fully filled, it enters the pending order queue. At this price level, the order is placed at the front (i.e., preceding quantity is 0), and then enters the pending order matching phase.
Pending Orders:
If an order’s price matches the levels on the same side of the order book, it enters the pending queue. The quantity ahead of this order equals the total quantity at the same price level. The order then proceeds to the pending matching phase.
Pending Order Matching (Unfilled or Partially Filled Orders):
Matching Mode 1:
If the matching ratio is set to 0, he order is matched against the opposite side in the order book following the same procedure as in step 1. The trade price equals the order price. Matching continues until the order is fully executed or the market closes. Remaining quantities can be canceled on request.
If the matching ratio is greater than 0, the order is matched based on the latest price and best N levels of the order book:
If the latest price equals the order price, the system first deducts the quantity ahead of the order by (interval traded volume - preceding quantity), which is marked as the current volume. Once preceding quantity reaches zero, the order begins execution. Trade price equals the order price; trade quantity is the smaller of the user order quantity and current volume * matching ratio.
If the latest price is less favorable than the order price, trade price equals the order price; trade quantity is the smaller of the user order quantity and interval actual volume * matching ratio.
The remaining unfilled portion of the order is matched against the corresponding levels on the opposite side of the order book at the order’s specified price.
The matching continues until the order is fully executed or the market closes for the day. Any unfilled or partially filled orders may be canceled according to a cancellation request.
Matching Mode 2: Matching is performed based on the interval trade list and the latest top levels of the snapshot order book, following the price-time priority principle:
The user’s buy order is matched against trades in the interval whose prices are less than or equal to the order price. First, the quantity ahead of the user’s order at the same price level (including both resting orders and earlier trades) is subtracted, defining the available executable volume. When the available volume is greater than zero, the user’s order can be executed at the order price. The executed quantity is the minimum of the remaining order quantity and the available volume.
The user’s sell order is matched against trades in the interval whose prices are greater than or equal to the order price. The quantity ahead of the user’s order at the same price level is deducted as the available executable volume. If the available volume is positive, execution occurs at the order price. The executed quantity is the minimum of the remaining order quantity and the available volume.
Any unfilled portion is then matched against the opposite side of the order book at the user’s order price. The executed quantity is the minimum of the remaining order quantity and the product of the matching quantity and the fill ratio.
If the order is still not fully filled, the remaining quantity ahead of the user’s order at the same price level is updated.
Matching continues until the order is fully executed or the market closes. Any unfilled or partially filled orders may be canceled according to a cancellation request.
Tick Market Data#
When the market data is tick-by-tick, the matching process proceeds as follows:
Immediate matching: Limit orders are immediately matched against the level N of the order book. Execution can follow a configurable matching ratio.
Unfilled or partially filled orders: Remaining quantities are matched according to the price-time priority principle:
For subsequent tick data, user orders are matched against incoming orders at the opposite side of the book following price-time priority.
Unfilled or partially filled orders can be canceled according to cancellation requests.
Matching continues until the order is fully executed or market close.
Market Order Matching Rules#
Market orders are executed immediately against the level N of the order book, following the specific trading rules of the Shanghai Stock Exchange (SSE) or Shenzhen Stock Exchange (SZSE).
SSE Matching Rules#
The SSE market order types are defined as follows:
Five Best Orders Immediate or Cancel (IOC): an order that is executed in sequence against the current five best prices on the opposite side, with the unfilled portion, if any, cancelled automatically.
Five Best Orders Immediate to Limit: an order that is executed in sequence against the current five best prices on the opposite side, with the unfilled portion, if any, converted to a limit order whose limit price is set to the last execution price on the same side. If the Five Best Orders Immediate to Limit order cannot be filled at all, it is either converted to a limit order whose limit price is set to the best quotation on the same side, or, in the absence of such a quotation, cancelled.
Same-Side Best Price Order: an order whose quotation price will be the best price on the same side in the central order book when such order enters the SSE trading system. If there is no quotation on the same side in the central order book when the Same-Side Best Price Order enters the SSE trading system, the order will be automatically cancelled.
Opposite-Side Best Price Order: an order whose quotation price will be the best price on the opposite side in the central order book when such order enters the SSE trading system. If there is no quotation on the opposite side in the central order book when the Opposite-Side Best Price Order enters the SSE trading system, the order will be automatically cancelled.
SZSE Matching Rules#
The SZSE market order types are defined as follows:
Fill or Kill (FOK): an order that must be executed in its entirety against all the orders on the opposite side in the central order book at the time the order is routed into the Exchange trading system, otherwise the entire order shall be cancelled automatically.
Five Best Orders Immediate or Cancel (IOC): executed in sequence against the current five best prices on the opposite side. In case that part of the order cannot be executed, the unfilled part of the order shall be cancelled automatically
Immediate or Cancel (IOC): an order that is executed in sequence against all the orders on the opposite side in the central order book at the time the order is routed into the Exchange trading system. In case that part of the order cannot be executed, the unfilled part of the order shall be cancelled automatically
Same-Side Best Price: an order whose quotation price is set at the best price on the same side in the central order book at the time the order is routed into the Exchange trading system.
Opposite-side Best Price: an order whose quotation price is set at the best price on the opposite side in the central order book at the time the order is routed into the Exchange trading system.
Examples#
Example 1: Simulation Matching Based on Tick-by-Tick Trade Data
Using the simulation matching engine involves five core steps: matching configuration, table schema definitions (market ticks, user orders, trade output), engine creation, data ingestion and execution, and result inspection. The following describes the workflow using tick-by-tick market data:
Matching configuration
import swordfish as sf
from swordfish.plugins.matching_engine_simulator import MatchingEngineSimulator
from swordfish.plugins.matching_engine_simulator import MatchingEngineSimulatorConfig
# config the engine
config = MatchingEngineSimulatorConfig()
config.latency = 0 # User order latency = 0
config.orderbook_matching_ratio = 1 # Trade fill ratio when matching against the order book
Table schema definitions
# Define the trade detail output table
order_detail_output = sf.table(types={
'orderId': "LONG",
'symbol': "STRING",
'direction': "INT",
'sendTime': "TIMESTAMP",
'orderPrice': "DOUBLE",
'orderQty': "LONG",
'tradeTime': "TIMESTAMP",
'tradePrice': "DOUBLE",
'tradeQty': "LONG",
'orderStatus': "INT",
'sysReceiveTime': "NANOTIMESTAMP",
})
# Define the market quote table
quote_schema = {
'symbol': "STRING",
'symbolSource': "STRING",
'timestamp': "TIMESTAMP",
'sourceType': "INT",
'orderType': "INT",
'price': "DOUBLE",
'qty': "LONG",
'buyNo': "LONG",
'sellNo': "LONG",
'direction': "INT",
'seqNum': "LONG",
}
# Define the user order table
user_order_schema = {
'symbol': "STRING",
'timestamp': "TIMESTAMP",
'orderType': "INT",
'price': "DOUBLE",
'orderQty': "LONG",
'direction': "INT",
'orderId': "LONG",
}
Engine creation
dataType = 0
engine = MatchingEngineSimulator.create(
"engine_trade", "XSHE",dataType , order_detail_output, quote_schema, user_order_schema
).config(config).submit()
Data ingestion (Note: market data must be sorted in chronological order)
# Insert market data
symbol = "000001"
sourceType = 0 # 0 represents order; 1 represents transaction
orderType = 2 # In the order type: 1 = market order, 2 = limit order, 3 = best bid/offer order, 10 = cancellation
# In the transaction type: 0 = trade execution, 1 = order cancellation
orderSell = 2
orderBuy = 1
engine.insert_market(sf.any_vector([
symbol, "XSHE", sf.scalar("2021.01.08 09:14:01.100", type="TIMESTAMP"), sourceType, orderType, 7.0, 100, 1, 1, orderBuy, 1
]))
engine.insert_market(sf.any_vector([
symbol, "XSHE", sf.scalar("2021.01.08 09:14:01.100", type="TIMESTAMP"), sourceType, orderType, 6.0, 100, 2, 2, orderBuy, 1
]))
engine.insert_market(sf.any_vector([
symbol, "XSHE", sf.scalar("2021.01.08 09:14:01.100", type="TIMESTAMP"), sourceType, orderType, 5.0, 100, 3, 3, orderBuy, 1
]))
# Insert orders
userOrderType = 5
# One buy order
engine.insert_order(sf.any_vector([
symbol, sf.scalar("2021.01.08 09:14:01.400", type="TIMESTAMP"), userOrderType, 6.0, 100, orderBuy, 1
]))
Result inspection
# The unfilled order can be viewed in the pending orders table
res = engine.get_open_orders()
print(res)
# orderId timestamp symbol price totalQty openQty direction isMatching
# ------- ----------------------- ------ ----- -------- ------- --------- ----------
# 1 2021.01.08T09:14:01.400 000001 6 100 100 1 0
# No record in the output table since no trade occurred
print(order_detail_output)
# orderId symbol direction sendTime orderPrice orderQty tradeTime tradePrice tradeQty orderStatus sysReceiveTime
# ------- ------ --------- -------- ---------- -------- --------- ---------- -------- ----------- --------------
Since all orders in the above code are buy orders, and the user’s submitted order is also a buy order, there are no sell orders available for matching. Now we submit a sell order:
# One sell order
engine.insert_order(sf.any_vector([
symbol, sf.scalar("2021.01.08 09:14:01.400", type="TIMESTAMP"), userOrderType, 6.0, 100, orderSell, 1
]))
Let’s check whether it has been executed. The unfilled user order can be seen in the pending orders table, indicating that it has not been matched yet.
# The unfilled order can be viewed in the pending orders table
res = engine.get_open_orders()
print(res)
# orderId timestamp symbol price totalQty openQty direction isMatching
# ------- ----------------------- ------ ----- -------- ------- --------- ----------
# 1 2021.01.08T09:14:01.400 000001 6 100 100 1 0
# 2 2021.01.08T09:14:01.400 000001 6 100 100 2 0
The order has not been executed because the latency is set to 0. A user order is eligible for matching only when the latest market timestamp satisfies: latest market time ≥ order time + latency. In the current scenario, the latest market tick is 2021-01-08T09:14:01.100, whereas the order time is 2021-01-08T09:14:01.400. Consequently, no market tick has triggered the execution of this order. Now, we insert a new market tick and observe again:
# Insert a market tick at 2021-01-08T09:14:01.500
engine.insert_market(sf.any_vector([
symbol, "XSHE", sf.scalar("2021.01.08 09:14:01.500", type="TIMESTAMP"), sourceType, orderType, 7.0, 100, 1, 1, orderBuy, 1
]))
# The pending orders table is now empty
res = engine.get_open_orders()
print(res)
# orderId timestamp symbol price totalQty openQty direction isMatching
# ------- ----------------------- ------ ----- -------- ------- --------- ----------
Now the trade output table shows the matched trades for both limit orders.
# The trade output table contains the matched trade record
print(order_detail_output)
# orderId symbol direction sendTime orderPrice orderQty tradeTime tradePrice tradeQty orderStatus sysReceiveTime
# ------- ------ --------- ----------------------- ---------- -------- ----------------------- ---------- -------- ----------- -----------------------------
# 1 000001 1 2021.01.08T09:14:01.400 6 100 2021.01.08T09:14:01.500 0 0 4 2025.07.22T11:54:17.289997497
# 1 000001 1 2021.01.08T09:14:01.400 6 100 2021.01.08T09:14:01.500 0 0 -1 2025.07.22T11:54:17.289997497
# 2 000001 2 2021.01.08T09:14:01.400 6 100 2021.01.08T09:14:01.500 0 0 4 2025.07.22T11:54:23.562496049
# 2 000001 2 2021.01.08T09:14:01.400 6 100 2021.01.08T09:14:01.500 0 0 -1 2025.07.22T11:54:23.562496049
Example 2: Simulation Matching Based on Level 2 Snapshot Data
According to the fields contained in the snapshot data, the matching engine simulator provides two distinct matching modes:
Matching Mode 1: User orders are matched against the latest price in the snapshot and the latest order book.
Matching Mode 2: When snapshot data is combined with tick-level trade details within a time interval, orders are matched against the list of trade prices in the interval and the latest order book.
Using the snapshot of the order book at a specific time (2022-04-14T09:35:00.040) as reference (Table 1), the latest traded price at this moment is 16.34. At this point, the user submits a sell order with a price of 16.32 and a quantity of 50,000. The matching of this order is demonstrated separately under Matching Mode 1 and Matching Mode 2. The order book at time t is shown in Table 2. The depth and construction of Market Table 1 and Market Table 2 are as follows:
Table 1:
bidQty |
bidPrice |
askPrice |
askQty |
|---|---|---|---|
10100 |
16.33 |
16.34 |
5400 |
22000 |
16.32 |
16.35 |
197300 |
18300 |
16.31 |
16.36 |
246400 |
113200 |
16.30 |
16.37 |
183400 |
3900 |
16.29 |
16.38 |
313800 |
12800 |
16.28 |
16.39 |
454600 |
16600 |
16.27 |
16.40 |
696100 |
17800 |
16.26 |
16.41 |
49000 |
39054 |
16.25 |
16.42 |
59400 |
4400 |
16.24 |
16.43 |
76300 |
The sample data for Table 1 is as follows:
import swordfish as sf
t1 = sf.table({
'SecurityID': ["000001.SZ"],
'symbolSource': ["XSHE"],
'DateTime': [sf.scalar("2022.04.15T09:55:15.000", type="TIMESTAMP")],
'LastPx': [16.34],
'up_limit_px': [17.64],
'down_limit_px': [14.44],
'TotalBidQty':[6683254],
'TotalOfferQty': [14644870],
'BidPrice': sf.array_vector([[16.33, 16.32, 16.31, 16.30, 16.29, 16.28, 16.27, 16.26, 16.25, 16.24]], type="DOUBLE"),
'BidOrderQty': sf.array_vector([[10100, 22000, 18300, 113200, 3900, 12800, 16600, 17800, 39054, 4400]], type="LONG"),
'OfferPrice': sf.array_vector([[16.34, 16.35, 16.36, 16.37, 16.38, 16.39, 16.40, 16.41, 16.42, 16.43]], type="DOUBLE"),
'OfferOrderQty': sf.array_vector([[5400, 197300, 246400, 183400, 313800, 454600, 696100, 49000, 59400, 76300]], type="LONG"),
'tradePrice': sf.array_vector([[16.34]], type="DOUBLE"),
'tradeQty': sf.array_vector([[50000]], type="LONG"),
})
Table 2:
bidQty |
bidPrice |
askPrice |
askQty |
|---|---|---|---|
28900 |
16.33 |
16.34 |
1700 |
22000 |
16.32 |
16.35 |
224800 |
18300 |
16.31 |
16.36 |
241100 |
113200 |
16.30 |
16.37 |
183500 |
3900 |
16.29 |
16.38 |
313800 |
12800 |
16.28 |
16.39 |
454600 |
16600 |
16.27 |
16.40 |
696000 |
17800 |
16.26 |
16.41 |
49000 |
39054 |
16.25 |
16.42 |
59400 |
4400 |
16.24 |
16.43 |
76300 |
The sample data for Table 2 is as follows:
t2 = sf.table({
'SecurityID': ["000001.SZ"],
'symbolSource': ["XSHE"],
'DateTime': [sf.scalar("2022.04.15T09:55:18.000", type="TIMESTAMP")],
'LastPx': [16.34],
'up_limit_px': [17.64],
'down_limit_px': [14.44],
'TotalBidQty':[25500],
'TotalOfferQty': [25500],
'BidPrice': sf.array_vector([[16.33,16.32,16.31,16.30,16.29,16.28,16.27,16.26,16.25,16.24]], type="DOUBLE"),
'BidOrderQty': sf.array_vector([[28900,22000,18300,113200,3900,12800,16600,17800,39054,4400]], type="LONG"),
'OfferPrice': sf.array_vector([[16.34,16.35,16.36,16.37,16.38,16.39,16.40,16.41,16.42,16.43]], type="DOUBLE"),
'OfferOrderQty': sf.array_vector([[1700,224800,241100,183500,313800,454600,696000,49000,59400,76300]], type="LONG"),
'tradePrice': sf.array_vector([[16.34,16.33,16.34]], type="DOUBLE"),
'tradeQty': sf.array_vector([[300,1000,24200]], type="LONG"),
})
Matching Orders Using Mode 1
In Matching Mode 1, limit orders are immediately matched against the top N levels of the order book. Any unfilled portion of the order will subsequently be matched first against the latest trade price within the interval, and then sequentially against the latest order book at the current time. In this scenario, the parameter matchingMode is set to 1, the interval fill ratio (matchingRatio) is 10%, and other specific configurations are as follows:
import swordfish as sf
from swordfish.plugins.matching_engine_simulator import MatchingEngineSimulator
from swordfish.plugins.matching_engine_simulator import MatchingEngineSimulatorConfig
# Configure the simulation matching engine
config = MatchingEngineSimulatorConfig()
config.latency = 0 # User order latency set to 0
config.orderbook_matching_ratio = 1 # Execution ratio when matching against the order book
config.depth = 10 # Matching depth of the order book, range 5 - 50
config.output_interval = 1 # Execution ratio with the order book
config.matching_mode = 1 # Matching mode for snapshot data, can be set to 1 or 2
config.matching_ratio = 0.1 # Interval execution ratio in snapshot mode; by default, equal to orderBookMatchingRatio
# Define the trade detail output table
order_detail_output = sf.table(types={
'orderId': "LONG",
'symbol': "STRING",
'direction': "INT",
'sendTime': "TIMESTAMP",
'orderPrice': "DOUBLE",
'orderQty': "LONG",
'tradeTime': "TIMESTAMP",
'tradePrice': "DOUBLE",
'tradeQty': "LONG",
'orderStatus': "INT",
'sysReceiveTime': "NANOTIMESTAMP",
})
# Define the market quote table
quote_schema = sf.table( types={
'symbol': "STRING",
'symbolSource': "STRING",
'timestamp': "TIMESTAMP",
'lastPrice': "DOUBLE",
'upLimitPrice': "DOUBLE",
'downLimitPrice': "DOUBLE",
'totalBidQty': "LONG",
'totalOfferQty': "LONG",
'bidPrice': "DOUBLE[]",
'bidQty': "LONG[]",
'offerPrice': "DOUBLE[]",
'offerQty': "LONG[]"
}
)
# Define the user order table
user_order_schema = {
'symbol': "STRING",
'timestamp': "TIMESTAMP",
'orderType': "INT",
'price': "DOUBLE",
'orderQty': "LONG",
'direction': "INT",
'orderId': "LONG",
}
# Create the engine
dataType = 1 # Snapshot mode
engine = MatchingEngineSimulator.create(
"engine_snapshot1", "XSHE",dataType , order_detail_output, quote_schema, user_order_schema
).config(config).submit()
At time t, the price of the sell order is 16.32 and the quantity is 50000 shares. The order is immediately matched against the counterparty orders at price levels 16.33 and 16.32, with executed volumes of 10100 and 22000, respectively. The remaining unfilled quantity is 50000 - 10100 - 22000 = 17900, positioned at the first level on the sell side of the order book.
# Insert Market Table 1
engine.insert_market(t1)
# User submits a sell order with a price of 16.32 and quantity of 50,000 shares
engine.insert_order(sf.any_vector([
"000001.SZ", sf.scalar("2022.04.15T09:55:15.000", type="TIMESTAMP"), sf.scalar(5,type="INT"), 16.32, 50000, sf.scalar(2,type="INT"), 1
]))
# The remaining unfilled quantity can be viewed in the pending user orders
res = engine.get_open_orders()
print(res)
# orderId timestamp symbol price totalQty openQty direction isMatching
# ------- ----------------------- --------- ----- -------- ------- --------- ----------
# 1 2022.04.15T09:55:15.000 000001.SZ 16.32 50000 17900 2 1
The corresponding trade detail is as follows:
# The executed trades are listed in the output table
print(order_detail_output)
# orderId symbol direction sendTime orderPrice orderQty tradeTime tradePrice tradeQty orderStatus sysReceiveTime
# ------- --------- --------- ----------------------- ---------- -------- ----------------------- ------------------ -------- ----------- -----------------------------
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:15.000 0 0 4 2025.07.23T10:30:12.859071804
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:15.000 16.329999999999998 10100 0 2025.07.23T10:30:12.859071804
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:15.000 16.32 22000 0 2025.07.23T10:30:12.859071804
The order book in the next market snapshot, shown in Table 2, the latest trade price is 16.34, and the interval trade volume is 2550.
# Insert Market Table 2
engine.insert_market(t2)
The remaining order is matched against the interval’s latest trade price, resulting in an executed volume of 2550. The remaining 17900 - 2550 = 15350 shares are then matched against the first level of the buy side. After this step, the order is fully executed, the pending orders table is empty, and the resulting trade details are as follows:
# The pending user orders are now empty, indicating all user orders have been fully executed
res = engine.get_open_orders()
print(res)
# orderId timestamp symbol price totalQty openQty direction isMatching
# ------- ----------------------- --------- ----- -------- ------- --------- ----------
# The trade output table shows the executed trade details
print(order_detail_output)
# orderId symbol direction sendTime orderPrice orderQty tradeTime tradePrice tradeQty orderStatus sysReceiveTime
# ------- --------- --------- ----------------------- ---------- -------- ----------------------- ------------------ -------- ----------- -----------------------------
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:15.000 0 0 4 2025.07.23T10:30:12.859071804
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:15.000 16.329999999999998 10100 0 2025.07.23T10:30:12.859071804
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:15.000 16.32 22000 0 2025.07.23T10:30:12.859071804
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:18.000 16.32 2550 0 2025.07.23T10:30:12.859071804
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:18.000 16.32 15350 1 2025.07.23T10:30:12.859071804
# Scenario complete, stop the engine
engine.drop()
Matching Orders Using Mode 2
In Matching Mode 2, limit orders are immediately matched against the top N levels of the order book. Any unfilled orders enter the pending order process, where they are first matched against trade prices within the interval, and then sequentially matched against the latest order book at the current time. In this scenario, the parameter matchingMode is set to 2, with other specific configurations as follows:
# Configure the simulation matching engine
config = MatchingEngineSimulatorConfig()
config.latency = 0 # User order latency set to 0
config.orderbook_matching_ratio = 1 # Execution ratio when matching against the order book
config.depth = 10 # Matching depth of the order book, range 5 - 50
config.output_interval = 1 # Execution ratio with the order book
config.matching_mode = 2 # Matching mode for snapshot data, can be set to 1 or 2
config.matching_ratio = 0.1 # Interval execution ratio in snapshot mode; by default, equal to orderBookMatchingRatio
# Define the trade detail output table
order_detail_output = sf.table(types={
'orderId': "LONG",
'symbol': "STRING",
'direction': "INT",
'sendTime': "TIMESTAMP",
'orderPrice': "DOUBLE",
'orderQty': "LONG",
'tradeTime': "TIMESTAMP",
'tradePrice': "DOUBLE",
'tradeQty': "LONG",
'orderStatus': "INT",
'sysReceiveTime': "NANOTIMESTAMP",
})
# Define the market quote table
quote_schema = sf.table( types={
'symbol': "STRING",
'symbolSource': "STRING",
'timestamp': "TIMESTAMP",
'lastPrice': "DOUBLE",
'upLimitPrice': "DOUBLE",
'downLimitPrice': "DOUBLE",
'totalBidQty': "LONG",
'totalOfferQty': "LONG",
'bidPrice': "DOUBLE[]",
'bidQty': "LONG[]",
'offerPrice': "DOUBLE[]",
'offerQty': "LONG[]",
'tradePrice': "DOUBLE[]",
'tradeQty': "LONG[]"
}
)
# Define the user order table
user_order_schema = {
'symbol': "STRING",
'timestamp': "TIMESTAMP",
'orderType': "INT",
'price': "DOUBLE",
'orderQty': "LONG",
'direction': "INT",
'orderId': "LONG",
}
# Create the engine
dataType = 1 # Snapshot mode
engine = MatchingEngineSimulator.create(
"engine_snapshot2", "XSHE",dataType , order_detail_output, quote_schema, user_order_schema
).config(config).submit()
At time t, the order book is shown in Table 1. A sell order is submitted with a price of 16.32 and a quantity of 50000 shares. The order is immediately matched against counterparty orders at price levels 16.33 and 16.32, with executed volumes of 10100 and 22000, respectively. The remaining unfilled quantity is 50000 - 10100 - 22000 = 17900, positioned at the first level on the sell side.
# Insert Market Table 1
engine.insert_market(t1)
# User submits a sell order with a price of 16.32 and quantity of 50,000 shares
engine.insert_order(sf.any_vector([
"000001.SZ", sf.scalar("2022.04.15T09:55:15.000", type="TIMESTAMP"), sf.scalar(5,type="INT"), 16.32, 50000, sf.scalar(2,type="INT"), 1
]))
# The remaining unfilled quantity can be viewed in the pending user orders
res = engine.get_open_orders()
print(res)
# orderId timestamp symbol price totalQty openQty direction isMatching
# ------- ----------------------- --------- ----- -------- ------- --------- ----------
# 1 2022.04.15T09:55:15.000 000001.SZ 16.32 50000 17900 2 1
The trade detail is as follows, which is identical to the results under Mode 1:
# The executed trades are listed in the output table
print(order_detail_output)
# orderId symbol direction sendTime orderPrice orderQty tradeTime tradePrice tradeQty orderStatus sysReceiveTime
# ------- --------- --------- ----------------------- ---------- -------- ----------------------- ------------------ -------- ----------- -----------------------------
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:15.000 0 0 4 2025.07.23T15:19:37.194979087
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:15.000 16.329999999999998 10100 0 2025.07.23T15:19:37.194979087
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:15.000 16.32 22000 0 2025.07.23T15:19:37.194979087
The user order occupies the best position on the sell side. The order book in the next market snapshot, shown in Table 2, the latest interval trade price list is [16.34, 16.33, 16.34], with corresponding volumes [300, 1000, 24200]. The pending order is matched sequentially against the interval trade price list. After this step, the user order is fully executed. The resulting trade details are as follows, differing from those under Matching Mode 1:
# Insert Market Table 2
engine.insert_market(t2)
# All user orders have been fully executed; the pending orders table is now empty
res = engine.get_open_orders()
print(res)
# orderId timestamp symbol price totalQty openQty direction isMatching
# ------- --------- ------ ----- -------- ------- --------- ----------
# The executed trades can be viewed in the output table
print(order_detail_output)
# orderId symbol direction sendTime orderPrice orderQty tradeTime tradePrice tradeQty orderStatus sysReceiveTime
# ------- --------- --------- ----------------------- ---------- -------- ----------------------- ------------------ -------- ----------- -----------------------------
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:15.000 0 0 4 2025.07.23T15:19:37.194979087
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:15.000 16.329999999999998 10100 0 2025.07.23T15:19:37.194979087
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:15.000 16.32 22000 0 2025.07.23T15:19:37.194979087
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:18.000 16.32 300 0 2025.07.23T15:19:37.194979087
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:18.000 16.32 1000 0 2025.07.23T15:19:37.194979087
# 1 000001.SZ 2 2022.04.15T09:55:15.000 16.32 50000 2022.04.15T09:55:18.000 16.32 16600 1 2025.07.23T15:19:37.194979087
# End of scenario; stop the engine
engine.drop()
Conclusion#
The matching engine simulator in PySwordfish can simulate the matching of user orders based on snapshot data or tick-level market data. The engine supports configuration of order execution ratios and latency. When multiple user orders in the same direction are matched simultaneously, the matching follows price-time priority. This engine enables realistic emulation of actual trading during high-frequency strategy backtesting, providing crucial reference for the optimization and evaluation of high-frequency trading strategies.