The Self-Trade Prevention (STP) feature on Binance.US is designed to prevent trades from being executed when they are unintentionally placed and traded by the same user (ie. instances when an API user is both maker and taker on the trade).
Self-Trade Prevention will be enforced for both regular trading accounts (via the app or website), and API traders. For that reason, parts of this article will apply to both user types. However, since API traders will have a wider degree of control over the feature, this article primarily applies to our API users.
What is Self-Trade Prevention?
Self-trade prevention (STP) is a measure to prevent users from trading against their own account or other accounts that share the same tradeGroupId
(such as parent and sub-accounts which belong to the same entity).
STP prevents users and trade groups from acting as both a maker and a taker within the same trade. When a self-trade is detected by the system, at least one of the orders involved will expire. For regular users (of the Binance.US website and app), the maker order will be the one to expire.
However, API users have the option to set an expiry mode and decide which order(s) will expire in the case of STP. If API users do not set an expiry mode, the maker-side will remain the default to expire.
What defines a Self-Trade?
A self-trade is a scenario in which either of the following occurs:
- An order attempts to be traded against the same account (the account is both the maker and the taker of the trade).
- An order attempts to be traded against an account with a shared
tradeGroupId
(the maker and the taker accounts belong to the sametradeGroupId
).
What happens when STP is triggered?
STP causes at least one of the orders involved in a trade to expire, thus preventing the trade from taking place.
For the majority of Binance.US users, i.e. those using the Binance.US app or website, the maker side of the trade will expire in the event of STP. However, API users will have the ability to set their chosen expiry mode (if they do not choose an expiry mode, the maker side will expire by default).
There are four expiry modes available for API traders when an STP is triggered. Importantly, STP is controlled by and will follow the expiry mode of the taker order.
These modes determine which order(s) are canceled by STP:
NONE
- Exempts the order from self-trade prevention and allows the trade to occur. In this case, the account andtradeGroupId
will not be compared, and there will be no order expiry.EXPIRE_TAKER
- Prevents a trade by immediately causing the taker orders remaining quantity to expire.EXPIRE_MAKER
- Prevents a trade by immediately causing the potential maker orders remaining quantity to expire. This is the default expiry mode if no other selection is given.EXPIRE_BOTH
- Prevents a trade by immediately causing both the taker and the potential maker orders’ remaining quantities to expire.
What is a Trade Group ID?
A tradeGroupId
is used to classify different but related accounts as belonging to the same trading group or organization, including parent and sub-accounts. Two accounts that share the same tradeGroupId
are automatically classified as belonging to the same trade group.
Orders matched between members of the same trade group are eligible for STP depending on the taker order's STP mode.
To check whether accounts are under the same tradeGroupId
, users can check the results of their query using either of the following endpoints:
-
GET /api/v3/account
, or -
GET /api/v3/preventedMatches
If the value for tradeGroupId
is -1, then the tradeGroupId
has not been set for that account. This means that the STP will only take place between orders of the same account, but tradeGroupId
will not be compared or used to prevent trades.
What is a Prevented Match?
A prevented match is a record of one or more expired orders that took place during an attempted trade due to STP. The trade did not occur as one or more expiries caused the orders not to match.
Prevented match records can be queried through the endpoint GET /api/v3/preventedMatches
.
Here is a sample output for reference:
[
{
"symbol": "BTCDUSDT",
//Symbol of the orders.
"preventedMatchId": 8,
//Identifies prevented match of expired order(s) for symbol.
"takerOrderId": 12,
//Order Id of the Taker Order.
"makerOrderId": 10,
//Order Id of the Maker Order.
"tradeGroupId": 1,
//Identifies Trade Group Id (If account is not in a trade group, this will be -1).
"selfTradePreventionMode": "EXPIRE_BOTH",
//STP mode that expired the order(s).
"price": "50.0000000",
//Price at which the match occurred.
"takerPreventedQuantity": "1.00000000",
//Taker's remaining quantity. Only appears if the STP mode is EXPIRE_TAKER or EXPIRE_BOTH.
"makerPreventedQuantity": "10.90000000",
//Maker's remaining quantity. Only appears if the STP mode is EXPIRE_MAKER or EXPIRE_BOTH.
"transactTime": 1663190634060
//Time the order(s) expired due to STP.
}
]
What is Prevented Quantity?
STP causes the quantity from open order to expire. The STP modesEXPIRE_TAKER
, EXPIRE_MAKER
, andEXPIRE_BOTH
expire all remaining quantity on the affected orders, resulting in the entire order being expired.
Prevented quantity is the amount of quantity that is expired due to STP events for a particular order. User stream execution reports for orders involved in STP may have these fields:
{
"A":"3.000000", // Prevented Quantity
"B":"3.000000" // Last Prevented Quantity}
B
is present for execution typeTRADE_PREVENTION
, and is the quantity expired due to that individual STP event.
A
is the cumulative quantity expired due to STP over the lifetime of the order.
ForEXPIRE_TAKER
,EXPIRE_MAKER
, andEXPIRE_BOTH
modes this will always be the same value asB
.
While an order is open, the following inequality holds true:
executed quantity + prevented quantity < original order quantity
When an order has the statusEXPIRED_IN_MATCH
orFILLED
, the following equation will hold true:
executed quantity + prevented quantity = original order quantity
How do I know which symbol uses STP?
Symbols may be configured to allow different sets of STP modes and take different default STP modes.
defaultSelfTradePreventionMode
- Orders will use this STP mode if the user does not provide one on order placement.
allowedSelfTradePreventionModes
- Defines the allowed set of STP modes for order placement on that symbol.
For example, if a symbol has the following configuration:
"defaultSelfTradePreventionMode":"NONE",
"allowedSelfTradePreventionModes": [
"NONE",
"EXPIRE_TAKER",
"EXPIRE_BOTH"
]
Then that means if a user sends an order with no selfTradePreventionMode
provided, then the order sent will have the value of NONE
.
If a user wants to explicitly mention the mode, they can pass the enumerated type NONE
, EXPIRE_TAKER
, or EXPIRE_BOTH
.
If a user tries to specify EXPIRE_MAKER
for orders on this symbol, they will receive an error:
{
"code":-1013,
"msg": "This symbol does not allow the specified self-trade prevention mode."
}
How do I know if an order expired due to STP?
If an order has expired due to STP, the order will have the status EXPIRED_IN_MATCH
STP Examples for API Traders:
For the following cases, assume that all orders for these examples are made from the same account.
Scenario A:
A user sends a new order with selfTradePreventionMode: NONE
that will match with a previous order of theirs, already on the book.
Maker Order: symbol=BTCUSDT side=BUY type=LIMIT quantity=1 price=1
selfTradePreventionMode=NONE
Taker Order: symbol=BTCUSDT side=SELL type=LIMIT quantity=1 price=1
selfTradePreventionMode=NONE
Result: No STP is triggered, and the orders will match.
Order status of the Maker Order
{ "symbol": "BTCUSDT", "orderId": 2, "orderListId": -1, "clientOrderId": "FaDk4LPRxastaICEFE9YTf", "price": "1.000000", "origQty": "1.000000", "executedQty": "1.000000", "cummulativeQuoteQty": "1.000000", "status": "FILLED", "timeInForce": "GTC", "type": "LIMIT", "side": "BUY", "stopPrice": "0.000000", "icebergQty": "0.000000", "time": 1670217090310, "updateTime": 1670217090330, "isWorking": true, "workingTime": 1670217090310, "origQuoteOrderQty": "0.000000", "selfTradePreventionMode": "NONE"
}
Order status of the Taker Order
{ "symbol": "BTCUSDT", "orderId": 3, "orderListId": -1, "clientOrderId": "Ay48Vtpghnsvy6w8RPQEde", "transactTime": 1670207731263, "price": "1.000000", "origQty": "1.000000", "executedQty": "1.000000", "cummulativeQuoteQty": "1.000000", "status": "FILLED", "timeInForce": "GTC", "type": "LIMIT", "side": "SELL", "workingTime": 1670207731263, "fills": [
{
"price": "1.000000",
"qty": "1.000000",
"commission": "0.000000",
"commissionAsset": "USDT",
"tradeId": 1
}
],
"selfTradePreventionMode": "NONE"
}
Scenario B:
A user sends an order with EXPIRE_MAKER
that would match with their orders that are already on the book.
Maker Order 1: symbol=BTCUSDT side=BUY type=LIMIT quantity=1.2 price=1.2
selfTradePreventionMode=NONE
Maker Order 2: symbol=BTCUSDT side=BUY type=LIMIT quantity=1.3 price=1.1
selfTradePreventionMode=NONE
Maker Order 3: symbol=BTCUSDT side=BUY type=LIMIT quantity=8.1 price=1
selfTradePreventionMode=NONE
Taker Order 1: symbol=BTCUSDT side=SELL type=LIMIT quantity=3 price=1
selfTradePreventionMode=EXPIRE_MAKER
Result: The orders that were on the book will expire due to the STP trigger, and the taker order will go on the book.
Maker Order 1
{ "symbol": "BTCUSDT", "orderId": 2, "orderListId": -1, "clientOrderId": "wpNzhSclc16pV8g5THIOR3", "price": "1.200000", "origQty": "1.200000",
"executedQty": "0.000000",
"cummulativeQuoteQty": "0.000000",
"status": "EXPIRED_IN_MATCH",
"timeInForce": "GTC",
"type": "LIMIT",
"side": "BUY",
"stopPrice": "0.000000",
"icebergQty": "0.000000",
"time": 1670217957437,
"updateTime": 1670217957498,
"isWorking": true,
"workingTime": 1670217957437,
"origQuoteOrderQty": "0.000000",
"selfTradePreventionMode": "NONE",
"preventedMatchId": 0,
"preventedQuantity": "1.200000"
}
Maker Order 2
{ "symbol": "BTCUSDT", "orderId": 3, "orderListId": -1, "clientOrderId": "ZT9emqia99V7x8B6FW0pFF", "price": "1.100000", "origQty": "1.300000", "executedQty": "0.000000", "cummulativeQuoteQty": "0.000000", "status": "EXPIRED_IN_MATCH", "timeInForce": "GTC", "type": "LIMIT", "side": "BUY", "stopPrice": "0.000000", "icebergQty": "0.000000", "time": 1670217957458, "updateTime": 1670217957498, "isWorking": true, "workingTime": 1670217957458, "origQuoteOrderQty": "0.000000", "selfTradePreventionMode": "NONE", "preventedMatchId": 1, "preventedQuantity": "1.300000"
}
Maker Order 3
{
"symbol": "BTCUSDT",
"orderId": 4,
"OrderListId": -1,
"clientOrderId": "8QZ3taGcU4gND59TxHAcR0", "price": "1.000000", "origQty": "8.100000", "executedQty": "0.000000", "cummulativeQuoteQty": "0.000000", "status": "EXPIRED_IN_MATCH", "timeInForce": "GTC",
"type": "LIMIT",
"side": "BUY",
"stopPrice": "0.000000",
"icebergQty": "0.000000",
"time": 1670217957478,
"updateTime": 1670217957498,
"isWorking": true,
"workingTime": 1670217957478,
"origQuoteOrderQty": "0.000000",
"selfTradePreventionMode": "NONE",
"preventedMatchId": 2,
"preventedQuantity": "8.100000"
}
Output of the Taker Order
{ "symbol": "BTCUSDT", "orderId": 5, "orderListId": -1, "clientOrderId": "WRzbhp257NhZsIJW4y2Nri", "transactTime": 1670217957498, "price": "1.000000", "origQty": "3.000000", "executedQty": "0.000000", "cummulativeQuoteQty": "0.000000", "status": "NEW", "timeInForce": "GTC", "type": "LIMIT", "side": "SELL", "workingTime": 1670217957498, "fills": [], "preventedMatches": [
{
"preventedMatchId": 0,
"makerOrderId": 2,
"price": "1.200000",
"makerPreventedQuantity": "1.200000"
},
{
"preventedMatchId": 1,
"makerOrderId": 3,
"price": "1.100000",
"makerPreventedQuantity":
"1.300000"
},
{
"preventedMatchId": 2,
"makerOrderId": 4,
"price": "1.000000",
"makerPreventedQuantity": "8.100000"
}
],
"selfTradePreventionMode": "EXPIRE_MAKER"
}
Scenario C:
A user sends an order with EXPIRE_TAKER
that would match with their orders already on the book.
Maker Order 1: symbol=BTCUSDT side=BUY type=LIMIT quantity=1.2 price=1.2
selfTradePreventionMode=NONE
Maker Order 2: symbol=BTCUSDT side=BUY type=LIMIT quantity=1.3 price=1.1
selfTradePreventionMode=NONE
Maker Order 3: symbol=BTCUSDT side=BUY type=LIMIT quantity=8.1 price=1
selfTradePreventionMode=NONE
Taker Order 1: symbol=BTCUSDT side=SELL type=LIMIT quantity=3 price=1
selfTradePreventionMode=EXPIRE_TAKER
Result: The orders already on the book will remain, while the taker order will expire.
Maker Order 1
{ "symbol": "BTCUSDT", "orderId": 2, "orderListId": -1, "clientOrderId": "NpwW2t0L4AGQnCDeNjHIga", "price": "1.200000", "origQty": "1.200000", "executedQty": "0.000000", "cummulativeQuoteQty": "0.000000", "status": "NEW", "timeInForce": "GTC", "type": "LIMIT", "side": "BUY", "stopPrice": "0.000000", "icebergQty": "0.000000", "time": 1670219811986, "updateTime": 1670219811986, "isWorking": true, "workingTime": 1670219811986, "origQuoteOrderQty": "0.000000", "selfTradePreventionMode": "NONE"
}
Maker Order 2
{ "symbol": "BTCUSDT", "orderId": 3, "orderListId": -1, "clientOrderId": "TSAmJqGWk4YTB2yA9p04UO", "price": "1.100000", "origQty": "1.300000", "executedQty": "0.000000", "cummulativeQuoteQty": "0.000000", "status": "NEW", "timeInForce": "GTC", "type": "LIMIT", "side": "BUY", "stopPrice": "0.000000", "icebergQty": "0.000000", "time": 1670219812007, "updateTime": 1670219812007, "isWorking": true, "workingTime": 1670219812007, "origQuoteOrderQty": "0.000000", "selfTradePreventionMode": "NONE"
}
Maker Order 3
{ "symbol": "BTCUSDT", "orderId": 4, "orderListId": -1, "clientOrderId": "L6FmpCJJP6q4hCNv4MuZDG", "price": "1.000000", "origQty": "8.100000", "executedQty": "0.000000", "cummulativeQuoteQty": "0.000000", "status": "NEW", "timeInForce": "GTC", "type": "LIMIT", "side": "BUY", "stopPrice": "0.000000", "icebergQty": "0.000000", "time": 1670219812026, "updateTime": 1670219812026, "isWorking": true, "workingTime": 1670219812026, "origQuoteOrderQty": "0.000000", "selfTradePreventionMode": "NONE"
}
Output of the Taker order
{ "symbol": "BTCUSDT", "orderId": 5, "orderListId": -1, "clientOrderId": "kocvDAi4GNN2y1l1Ojg1Ri", "price": "1.000000", "origQty": "3.000000", "executedQty": "0.000000", "cummulativeQuoteQty": "0.000000", "status": "EXPIRED_IN_MATCH", "timeInForce": "GTC", "type": "LIMIT", "side": "SELL", "stopPrice": "0.000000", "icebergQty": "0.000000", "time": 1670219812046, "updateTime": 1670219812046, "isWorking": true, "workingTime": 1670219812046, "origQuoteOrderQty": "0.000000", "selfTradePreventionMode": "EXPIRE_TAKER", "preventedMatchId": 0, "preventedQuantity": "3.000000"
}
Scenario D:
A user has an order on the book, and then sends an order with EXPIRE_BOTH
that would match with the existing order.
Maker Order: symbol=BTCUSDT side=BUY type=LIMIT quantity=1 price=1
selfTradePreventionMode=NONE
Taker Order: symbol=BTCUSDT side=SELL type=LIMIT quantity=3 price=1
selfTradePreventionMode=EXPIRE_BOTH
Result: Both orders will expire.
Maker Order
{ "symbol": "ABCDEF", "orderId": 2, "orderListId": -1, "clientOrderId": "2JPC8xjpLq6Q0665uYWAcs", "price": "1.000000", "origQty": "1.000000", "executedQty": "0.000000", "cummulativeQuoteQty": "0.000000", "status": "EXPIRED_IN_MATCH", "timeInForce": "GTC", "type": "LIMIT", "side": "BUY", "stopPrice": "0.000000",
"icebergQty": "0.000000",
"time": 1673842412831,
"updateTime": 1673842413170,
"isWorking": true,
"workingTime": 1673842412831,
"origQuoteOrderQty": "0.000000",
"selfTradePreventionMode": "NONE",
"preventedMatchId": 0,
"preventedQuantity": "1.000000"
}
Taker Order
{ "symbol": "ABCDEF", "orderId": 5, "orderListId": -1, "clientOrderId": "qMaz8yrOXk2iUIz74cFkiZ", "transactTime": 1673842413170, "price": "1.000000", "origQty": "3.000000", "executedQty": "0.000000", "cummulativeQuoteQty": "0.000000", "status": "EXPIRED_IN_MATCH", "timeInForce": "GTC", "type": "LIMIT", "side": "SELL", "workingTime": 1673842413170, "fills": [], "preventedMatches": [
{
"preventedMatchId": 0,
"makerOrderId": 2,
"price": "1.000000",
"takerPreventedQuantity": "3.000000",
"makerPreventedQuantity": "1.000000"
}
],
"selfTradePreventionMode": "EXPIRE_BOTH",
"tradeGroupId": 1,
"preventedQuantity": "3.000000"
}
Scenario E:
A user has an order on the book with EXPIRE_MAKER
, and then sends a new order with EXPIRE_TAKER
which would match with the existing order.
Maker Order: symbol=BTCUSDT side=BUY type=LIMIT quantity=1 price=1
selfTradePreventionMode=EXPIRE_MAKER
Taker Order: symbol=BTCUSDT side=SELL type=LIMIT quantity=1 price=1
selfTradePreventionMode=EXPIRE_TAKER
Result: The taker order's STP mode will be used, so the take order will expire. { "symbol": "ABCDEF", "orderId": 0, "orderListId": -1, "clientOrderId": "jFUap8iFwwgqIpOfAL60GS", "price": "1.000000", "origQty": "1.000000", "executedQty": "0.000000", "cummulativeQuoteQty": "0.000000", "status": "NEW", "timeInForce": "GTC", "type": "LIMIT", "side": "BUY", "stopPrice": "0.000000", "icebergQty": "0.000000", "time": 1670220769261, "updateTime": 1670220769261, "isWorking": true, "workingTime": 1670220769261, "origQuoteOrderQty": "0.000000", "selfTradePreventionMode": "EXPIRE_MAKER"
}
Taker Order
{ "symbol": "ABCDEF", "orderId": 1, "orderListId": -1, "clientOrderId": "zxrvnNNm1RXC3rkPLUPrc1", "transactTime": 1670220800315, "price": "1.000000", "origQty": "1.000000", "executedQty": "0.000000", "cummulativeQuoteQty": "0.000000", "status": "EXPIRED_IN_MATCH", "timeInForce": "GTC", "type": "LIMIT", "side": "SELL", "workingTime": 1670220800315, "fills": [], "preventedMatches": [
{
"preventedMatchId": 0,
"makerOrderId": 0,
"price": "1.000000",
"takerPreventedQuantity": "1.000000"
}
],
"selfTradePreventionMode": "EXPIRE_TAKER",
"preventedQuantity": "1.000000"
}
Scenario F:
A user sends a market order with EXPIRE_MAKER
which would match with an existing order.
Maker Order: symbol=ABCDEF side=BUY type=LIMIT quantity=1 price=1
selfTradePreventionMode=NONE
Taker Order: symbol=ABCDEF side=SELL type=MARKET quantity=1
selfTradePreventionMode=EXPIRE_MAKER
Result: The existing order expires with the status EXPIRED_IN_MATCH
, due to the STP trigger. The new order also expires but with status EXPIRED
, due to low liquidity on the order book.
Maker Order
{ "symbol": "ABCDEF", "orderId": 2, "orderListId": -1, "clientOrderId": "7sgrQQInL69XDMQpiqMaG2", "price": "1.000000", "origQty": "1.000000", "executedQty": "0.000000", "cummulativeQuoteQty": "0.000000", "status": "EXPIRED_IN_MATCH", "timeInForce": "GTC", "type": "LIMIT", "side": "BUY", "stopPrice": "0.000000", "icebergQty": "0.000000", "time": 1670222557456, "updateTime": 1670222557478, "isWorking": true, "workingTime": 1670222557456, "origQuoteOrderQty": "0.000000", "selfTradePreventionMode": "NONE", "preventedMatchId": 0, "preventedQuantity": "1.000000"
}
Taker Order
{
"symbol": "ABCDEF",
"orderId": 3,
"orderListId": -1,
"clientOrderId": "zqhsgGDEcdhxy2oza2Ljxd",
"transactTime": 1670222557478,
"price": "0.000000",
"origQty": "1.000000",
"executedQty": "0.000000",
"cummulativeQuoteQty": "0.000000",
"status": "EXPIRED",
"timeInForce": "GTC",
"type": "MARKET",
"side": "SELL",
"workingTime": 1670222557478,
"fills": [],
"preventedMatches": [
{
"preventedMatchId": 0,
"makerOrderId": 2,
"price": "1.000000",
"makerPreventedQuantity": "1.000000"
}
],
"selfTradePreventionMode": "EXPIRE_MAKER"
}