What Is Self Trade Prevention (STP)?

2023-08-29 07:03

What is Self Trade Prevention (STP)?

Self Trade Prevention (STP) is a mechanism employed by trading platforms to prevent users from inadvertently trading against themselves. This situation, known as self-trading, occurs when a user's own orders match each other on the order book. STP is particularly important in maintaining a fair and transparent trading environment, as it helps to prevent manipulation of market prices and ensures that trading activity accurately reflects genuine market interest.

At Binance, Self Trade Prevention (STP) prevents orders of users, or the user's “tradeGroupId”, to match against their own.

What defines a self-trade?

A self-trade can occur in either scenario:

  • The order traded against the same account.
  • The order traded against an account with the same “tradeGroupId”.

What happens when STP is triggered?

There are four possible modes for what the system will do if an order could create a self-trade.

  • NONE: This mode exempts the order from self-trade prevention. Accounts or “tradeGroupIds” will not be compared, no orders will be revoked, and the trade will occur.
  • EXPIRE_TAKER: This mode prevents a trade by immediately revoking the taker order's remaining quantity.
  • EXPIRE_MAKER: This mode prevents a trade by immediately revoking the potential maker order's remaining quantity.
  • EXPIRE_BOTH: This mode prevents a trade by immediately revoking both the taker and the potential maker orders' remaining quantities.

STP will occur depending on the STP mode of the taker order. Thus, the STP mode of an order that goes on the book is no longer relevant and will be ignored for all future order processing.

How do I set STP mode for an order?

STP can only be set using field “selfTradePreventionMode” through the API endpoints below:

  • POST /fapi/v1/order
  • POST /fapi/v1/batchOrders

What is a “tradeGroupId”?

Different accounts with the same “tradeGroupId” are considered part of the same "trade group". Orders submitted by members of a trade group are eligible for STP according to the taker order's STP mode.

A user can confirm if their accounts are under the same “tradeGroupId”' from the API from GET fapi/v2/account (REST API).

If the value is -1, then the “tradeGroupId“ has not been set for that account, the STP may only take place between orders of the same account.

Which symbols support STP?

All symbols in GET fapi/v1/exchangeInfo support STP.

Which order types support STP?

The following order types support STP when Time in force (timeInForce) is set to GTC/IOC/GTD:

  • Limit order
  • Market order
  • Take profit order
  • Stop market order
  • Take profit market order
  • Trailing stop market order

Please note that STP won't take effect for Time in force (timeInForce) in FOK or GTX.

Do modify orders support STP?

No. Modify orders do not support STP.

How to tell if an order is revoked due to STP?

The order status will be shown as “EXPIRED_IN_MATCH”. In the user data stream event “ORDER_TRADE_UPDATE”, the field X will show “EXPIRED_IN_MATCH” if an order is revoked due to STP.

{ "e":"ORDER_TRADE_UPDATE", // Event Type "E":1568879465651, // Event Time "T":1568879465650, // Transaction Time "o":{ "s":"BTCUSDT", // Symbol "c":"TEST", // Client Order Id // special client order id: // starts with "autoclose-": liquidation order // "adl_autoclose": ADL auto close order // "settlement_autoclose-": settlement order for delisting or delivery "S":"SELL", // Side "o":"TRAILING_STOP_MARKET", // Order Type "f":"GTC", // Time in Force "q":"0.001", // Original Quantity "p":"0", // Original Price "ap":"0", // Average Price "sp":"7103.04", // Stop Price. Please ignore with TRAILING_STOP_MARKET order "x":"EXPIRED", // Execution Type "X":"EXPIRED_IN_MATCH", // Order Status "i":8886774, // Order Id "l":"0", // Order Last Filled Quantity "z":"0", // Order Filled Accumulated Quantity "L":"0", // Last Filled Price "N":"USDT", // Commission Asset, will not push if no commission "n":"0", // Commission, will not push if no commission "T":1568879465650, // Order Trade Time "t":0, // Trade Id "b":"0", // Bids Notional "a":"9.91", // Ask Notional "m":false, // Is this trade the maker side? "R":false, // Is this reduce only "wt":"CONTRACT_PRICE", // Stop Price Working Type "ot":"TRAILING_STOP_MARKET", // Original Order Type "ps":"LONG", // Position Side "cp":false, // If Close-All, pushed with conditional order "AP":"7476.89", // Activation Price, only pushed with TRAILING_STOP_MARKET order "cr":"5.0", // Callback Rate, only pushed with TRAILING_STOP_MARKET order "pP": false, // ignore "si": 0, // ignore "ss": 0, // ignore "rp":"0" // Realized Profit of the trade "V": "NONE". // selfTradePreventionMode "pm":"QUEUE" // price match type "gtd":1768879465650 // good till date }}

STP examples

For all these cases, assume that all orders for these examples are made on the same account.

Scenario A: User sends a new order with “selfTradePreventionMode:NONE” that will match with another order of theirs that is already on the book.

Maker Order: symbol=BTCUSDT side=BUY type=LIMIT quantity=1 price=20000 selfTradePreventionMode=NONE Taker Order: symbol=BTCUSDT side=SELL type=LIMIT quantity=1 price=20000 selfTradePreventionMode=NONE

Result: No STP is triggered and the orders will match.

Order status of the maker order:

{ "orderId": 292864713, "symbol": "BTCUSDT", "status": "FILLED", "clientOrderId": "43N239GaUaqshfG7825184", "price": "20000", "avgPrice": "20000", "origQty": "1", "executedQty": "1", "cumQty": "1", "cumQuote": "20000", "timeInForce": "GTC", "type": "LIMIT", "reduceOnly": false, "closePosition": false, "side": "BUY", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "NONE", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "updateTime": 1692849639460}

Order status of the taker order:

{ "orderId": 292864714, "symbol": "BTCUSDT", "status": "FILLED", "clientOrderId": "43N239GaUaqshfG7825184", "price": "20000", "avgPrice": "20000", "origQty": "1", "executedQty": "1", "cumQty": "1", "cumQuote": "20000", "timeInForce": "GTC", "type": "LIMIT", "reduceOnly": false, "closePosition": false, "side": "SELL", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "NONE", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "updateTime": 1692849639460}

Scenario B: 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 price=20002 selfTradePreventionMode=NONE Maker Order 2: symbol=BTCUSDT side=BUY type=LIMIT quantity=1 price=20001 selfTradePreventionMode=NONE Taker Order 1: symbol=BTCUSDT side=SELL type=LIMIT quantity=1 price=20000 selfTradePreventionMode=EXPIRE_MAKER

Result: The orders that were on the book will be revoked due to STP, and the taker order will go on the book.

Maker order 1:

{ "orderId": 292864710, "symbol": "BTCUSDT", "status": "FILLED", "clientOrderId": "testMaker1", "price": "20002", "avgPrice": "20002", "origQty": "1", "executedQty": "1", "cumQuote": "20002", "timeInForce": "GTC", "type": "LIMIT", "reduceOnly": false, "closePosition": false, "side": "BUY", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "NONE", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "time": 1692849639460, "updateTime": 1692849639460}

Maker order 2:

{ "orderId": 292864711, "symbol": "BTCUSDT", "status": "EXPIRED_IN_MATCH", "clientOrderId": "testMaker2", "price": "20001", "avgPrice": "0.0000", "origQty": "1", "executedQty": "0", "cumQuote": "0", "timeInForce": "GTC", "type": "LIMIT", "reduceOnly": false, "closePosition": false, "side": "BUY", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "NONE", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "time": 1692849639460, "updateTime": 1692849639460}

Output of the taker order:

{ "orderId": 292864712, "symbol": "BTCUSDT", "status": "PARTIALLY_FILLED", "clientOrderId": "testTaker1", "price": "20000", "avgPrice": "20002", "origQty": "2", "executedQty": "1", "cumQuote": "20002", "timeInForce": "GTC", "type": "LIMIT", "reduceOnly": false, "closePosition": false, "side": "SELL", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "EXPIRE_MAKER", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "time": 1692849639460, "updateTime": 1692849639460}

Scenario C: User sends an order with “EXPIRE_TAKER” that would match with their orders that are already on the book.

Maker Order 1: symbol=BTCUSDT side=BUY type=LIMIT quantity=1 price=20002 selfTradePreventionMode=NONE Maker Order 2: symbol=BTCUSDT side=BUY type=LIMIT quantity=1 price=20001 selfTradePreventionMode=NONE Taker Order 1: symbol=BTCUSDT side=SELL type=LIMIT quantity=2 price=20000 selfTradePreventionMode=EXPIRE_TAKER

Result: The orders already on the book will remain, while the taker order will be revoked.

Maker order 1:

{ "orderId": 292864710, "symbol": "BTCUSDT", "status": "NEW", "clientOrderId": "testMaker1", "price": "20002", "avgPrice": "0.0000", "origQty": "1", "executedQty": "0", "cumQuote": "0", "timeInForce": "GTC", "type": "LIMIT", "reduceOnly": false, "closePosition": false, "side": "BUY", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "NONE", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "time": 1692849639460, "updateTime": 1692849639460}

Maker order 2:

{ "orderId": 292864711, "symbol": "BTCUSDT", "status": "NEW", "clientOrderId": "testMaker2", "price": "20001", "avgPrice": "0.0000", "origQty": "1", "executedQty": "0", "cumQuote": "0", "timeInForce": "GTC", "type": "LIMIT", "reduceOnly": false, "closePosition": false, "side": "BUY", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "NONE", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "time": 1692849639460, "updateTime": 1692849639460}

Output of the taker order:

{ "orderId": 292864712, "symbol": "BTCUSDT", "status": "EXPIRED_IN_MATCH", "clientOrderId": "testTaker1", "price": "20000", "avgPrice": "0.0000", "origQty": "3", "executedQty": "0", "cumQuote": "0", "timeInForce": "GTC", "type": "LIMIT", "reduceOnly": false, "closePosition": false, "side": "SELL", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "EXPIRE_TAKER", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "time": 1692849639460, "updateTime": 1692849639460}

Scenario D: 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=20002 selfTradePreventionMode=NONETaker Order: symbol=BTCUSDT side=SELL type=LIMIT quantity=3 price=20000 selfTradePreventionMode=EXPIRE_BOTH

Result: Both orders will be revoked.

Maker order:

{ "orderId": 292864710, "symbol": "BTCUSDT", "status": "EXPIRED_IN_MATCH", "clientOrderId": "testMaker1", "price": "20002", "avgPrice": "0.0000", "origQty": "1", "executedQty": "0", "cumQuote": "0", "timeInForce": "GTC", "type": "LIMIT", "reduceOnly": false, "closePosition": false, "side": "BUY", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "NONE", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "time": 1692849639460, "updateTime": 1692849639460}

Taker order:

{ "orderId": 292864712, "symbol": "BTCUSDT", "status": "EXPIRED_IN_MATCH", "clientOrderId": "testTaker1", "price": "20000", "avgPrice": "0.0000", "origQty": "3", "executedQty": "0", "cumQuote": "0", "timeInForce": "GTC", "type": "LIMIT", "reduceOnly": false, "closePosition": false, "side": "SELL", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "EXPIRE_BOTH", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "time": 1692849639460, "updateTime": 1692849639460}

Scenario E: User has an order on the book with “EXPIRE_MAKER”, 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=20002 selfTradePreventionMode=EXPIRE_MAKER Taker Order: symbol=BTCUSDT side=SELL type=LIMIT quantity=1 price=20000 selfTradePreventionMode=EXPIRE_TAKER

Result: The taker order's STP mode will be triggered, so the taker order will be revoked.

Maker order:

{ "orderId": 292864710, "symbol": "BTCUSDT", "status": "NEW", "clientOrderId": "testMaker1", "price": "20002", "avgPrice": "0.0000", "origQty": "1", "executedQty": "0", "cumQuote": "0", "timeInForce": "GTC", "type": "LIMIT", "reduceOnly": false, "closePosition": false, "side": "BUY", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "EXPIRE_MAKER", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "time": 1692849639460, "updateTime": 1692849639460}

Taker order:

{ "orderId": 292864712, "symbol": "BTCUSDT", "status": "EXPIRED_IN_MATCH", "clientOrderId": "testTaker1", "price": "20000", "avgPrice": "0.0000", "origQty": "1", "executedQty": "0", "cumQuote": "0", "timeInForce": "GTC", "type": "LIMIT", "reduceOnly": false, "closePosition": false, "side": "SELL", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "EXPIRE_TAKER", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "time": 1692849639460, "updateTime": 1692849639460}

Scenario F: User sends a market order with “EXPIRE_MAKER” which would match with an existing order.

Maker Order: symbol=BTCUSDT side=BUY type=LIMIT quantity=1 price=20002 selfTradePreventionMode=NONE Taker Order: symbol=BTCUSDT side=SELL type=MARKET quantity=3 selfTradePreventionMode=EXPIRE_MAKER

Result: The existing order will be revoked due to STP, with the status showing as “EXPIRED_IN_MATCH”. The new order will also be revoked, but due to low liquidity on the order book, and display the status as “EXPIRED”.

Maker order:

{ "orderId": 292864710, "symbol": "BTCUSDT", "status": "EXPIRED_IN_MATCH", "clientOrderId": "testMaker1", "price": "20002", "avgPrice": "0.0000", "origQty": "1", "executedQty": "0", "cumQuote": "0", "timeInForce": "GTC", "type": "LIMIT", "reduceOnly": false, "closePosition": false, "side": "BUY", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "EXPIRE_MAKER", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "time": 1692849639460, "updateTime": 1692849639460}

Taker order:

{ "orderId": 292864712, "symbol": "BTCUSDT", "status": "EXPIRED", "clientOrderId": "testTaker1", "price": "20000", "avgPrice": "0.0000", "origQty": "3", "executedQty": "0", "cumQuote": "0", "timeInForce": "GTC", "type": "MARKET", "reduceOnly": false, "closePosition": false, "side": "SELL", "positionSide": "BOTH", "stopPrice": "0", "workingType": "CONTRACT_PRICE", "priceMatch": "NONE", "selfTradePreventionMode": "EXPIRED", "goodTillDate": 0, "priceProtect": false, "origType": "LIMIT", "time": 1692849639460, "updateTime": 1692849639460}