'With the Python-Binance API, my limit order is only partially filled
I'm using Python 3.9 and the Python - Binance API, version python-binance==1.0.15. In their test environment, I'm placing buy orders like so
order=self._get_auth_client(account).order_limit_buy(symbol=formatted_name,
quantity=amount,
price=fiat_price)
This returns the following JSON
{'symbol': 'ETHUSDT', 'orderId': 2603582, 'orderListId': -1, 'clientOrderId': 'Ru4Vv2jmxHIfGI21vIMtjD', 'transactTime': 1650828003836, 'price': '2915.16000000', 'origQty': '0.34303000', 'executedQty': '0.00000000', 'cummulativeQuoteQty': '0.00000000', 'status': 'NEW', 'timeInForce': 'GTC', 'type': 'LIMIT', 'side': 'BUY', 'fills': []}
Using the "orderId" field, I check the status of the order, and then get back the result
{'symbol': 'ETHUSDT', 'orderId': 2603582, 'orderListId': -1, 'clientOrderId': 'Ru4Vv2jmxHIfGI21vIMtjD', 'price': '2915.16000000', 'origQty': '0.34303000', 'executedQty': '0.08067000', 'cummulativeQuoteQty': '235.16595720', 'status': 'PARTIALLY_FILLED', 'timeInForce': 'GTC', 'type': 'LIMIT', 'side': 'BUY', 'stopPrice': '0.00000000', 'icebergQty': '0.00000000', 'time': 1650828003836, 'updateTime': 1650828050722, 'isWorking': True, 'origQuoteOrderQty': '0.00000000'}
the status indicates a partial fill. I was wondering if there was a way to specify my buy order such that it either fills completely or not at all. I don't see anything specified in their docs though but they are a little sparse.
Solution 1:[1]
A partial fill order seems to be a common problem that has been discussed on Reddit.
The following is from the API documentation related to an order_limit_buy
, which you are executing.
order_limit_buy(timeInForce='GTC', **params)[source]
Send in a new limit buy order
Any order with an icebergQty MUST have timeInForce set to GTC.
Parameters:
- symbol (str) – required
- quantity (decimal) – required
- price (str) – required
- timeInForce (str) – default Good till cancelled
- newClientOrderId (str) – A unique id for the order. Automatically generated if not sent.
- stopPrice (decimal) – Used with stop orders
- icebergQty (decimal) – Used with iceberg orders
- newOrderRespType (str) – Set the response JSON. ACK, RESULT, or FULL; default: RESULT.
- recvWindow (int) – the number of milliseconds the request is valid for
Returns:. API response
See order endpoint for full response options
Raises:
- BinanceRequestException
- BinanceAPIException
- BinanceOrderException
- BinanceOrderMinAmountException
- BinanceOrderMinPriceException
- BinanceOrderMinTotalException
- BinanceOrderUnknownSymbolException
- BinanceOrderInactiveSymbolException
Below is the source code for the order_limit_buy function
def order_limit_buy(self, timeInForce=BaseClient.TIME_IN_FORCE_GTC, **params):
"""Send in a new limit buy order
Any order with an icebergQty MUST have timeInForce set to GTC.
:param symbol: required
:type symbol: str
:param quantity: required
:type quantity: decimal
:param price: required
:type price: str
:param timeInForce: default Good till cancelled
:type timeInForce: str
:param newClientOrderId: A unique id for the order. Automatically generated if not sent.
:type newClientOrderId: str
:param stopPrice: Used with stop orders
:type stopPrice: decimal
:param icebergQty: Used with iceberg orders
:type icebergQty: decimal
:param newOrderRespType: Set the response JSON. ACK, RESULT, or FULL; default: RESULT.
:type newOrderRespType: str
:param recvWindow: the number of milliseconds the request is valid for
:type recvWindow: int
:returns: API response
See order endpoint for full response options
:raises: BinanceRequestException, BinanceAPIException, BinanceOrderException, BinanceOrderMinAmountException, BinanceOrderMinPriceException, BinanceOrderMinTotalException, BinanceOrderUnknownSymbolException, BinanceOrderInactiveSymbolException
"""
params.update({
'side': self.SIDE_BUY,
})
return self.order_limit(timeInForce=timeInForce, **params)
Neither the API parameters or the Python order_limit_buy
function make it clear how to prevent the partial fill order issue.
Here is your buy order:
order=self._get_auth_client(account).order_limit_buy(symbol=formatted_name,
quantity=amount,
price=fiat_price)
Your order has the 3 required parameters as stated in the API documentation:
- symbol (str) – required
- quantity (decimal) – required
- price (str) – required
I found the article What Is a Stop-Limit Order? on the Binance Academy website. The article had this statement:
If you're worried about your orders only partially filling, consider using fill or kill.
Based on this statement I started looking through the API documentation and the source code for how to set either a FILL
or KILL
order.
I noted that the Python order_limit_buy
function has this parameter:
:param timeInForce: default Good till cancelled
:type timeInForce: str
The default value is Good till cancelled
or GTC
.
Looking at the API source code I found that the timeInForce
parameter has 3 possible values:
TIME_IN_FORCE_GTC = 'GTC' # Good till cancelled
TIME_IN_FORCE_IOC = 'IOC' # Immediate or cancel
TIME_IN_FORCE_FOK = 'FOK' # Fill or kill
Note the value TIME_IN_FORCE_FOK
or FOK
.
The following is from the Binance API documentation on GitHub:
Time in force (timeInForce):
This sets how long an order will be active before expiration.
Status | Description |
---|---|
GTC | Good Till Canceled An order will be on the book unless the order is canceled. |
IOC | Immediate Or Cancel An order will try to fill the order as much as it can before the order expires. |
FOK | Fill or Kill An order will expire if the full order cannot be filled upon execution |
Your buy request should look like this when using the timeInForce
parameter with the value FOK
:
order=self._get_auth_client(account).order_limit_buy(symbol=formatted_name,
quantity=amount,
price=fiat_price,
timeInForce='FOK')
I created a Binance TestNet Account and developed the code below as a test. I set my target price at 2687.00 to buy ETHUSDT. I used a loop to place my limited buy and to check to see if it was filled.
from binance.client import Client
api_key = 'my key'
api_secret = 'my secret'
client = Client(api_key, api_secret, testnet=True)
order_status = True
while True:
limit_order = client.order_limit_buy(symbol="ETHUSDT", quantity=0.01, price='2687.00', timeInForce='FOK')
ticker = client.get_ticker(symbol="ETHUSDT")
print(f"Current Price: {ticker.get('askPrice')}")
print(limit_order)
_status = limit_order.get('status')
if _status == 'FILLED':
order_status = False
print(order_status)
break
elif _status == 'EXPIRED':
order_status = True
The output from the code above is below:
NOTE: this is a snippet of the output, because the loop will run until the buy order triggers.
Current Price: 2687.33000000
{'symbol': 'ETHUSDT', 'orderId': 962373, 'orderListId': -1, 'clientOrderId': 'nW7bI2tkTwQEvrb8sSqgM6', 'transactTime': 1651927994444, 'price': '2687.00000000', 'origQty': '0.01000000', 'executedQty': '0.00000000', 'cummulativeQuoteQty': '0.00000000', 'status': 'EXPIRED', 'timeInForce': 'FOK', 'type': 'LIMIT', 'side': 'BUY', 'fills': []}
Current Price: 2687.33000000
{'symbol': 'ETHUSDT', 'orderId': 962378, 'orderListId': -1, 'clientOrderId': '7MPoHZDykxsK3Oqo7uOB78', 'transactTime': 1651927995310, 'price': '2687.00000000', 'origQty': '0.01000000', 'executedQty': '0.00000000', 'cummulativeQuoteQty': '0.00000000', 'status': 'EXPIRED', 'timeInForce': 'FOK', 'type': 'LIMIT', 'side': 'BUY', 'fills': []}
Current Price: 2687.33000000
{'symbol': 'ETHUSDT', 'orderId': 962387, 'orderListId': -1, 'clientOrderId': '3mRS9GK6pfGpqaqU9SK1vK', 'transactTime': 1651927996177, 'price': '2687.00000000', 'origQty': '0.01000000', 'executedQty': '0.00000000', 'cummulativeQuoteQty': '0.00000000', 'status': 'EXPIRED', 'timeInForce': 'FOK', 'type': 'LIMIT', 'side': 'BUY', 'fills': []}
Current Price: 2687.33000000
{'symbol': 'ETHUSDT', 'orderId': 962395, 'orderListId': -1, 'clientOrderId': '8yTAtrsjNH2PELtg93SdH3', 'transactTime': 1651927997041, 'price': '2687.00000000', 'origQty': '0.01000000', 'executedQty': '0.00000000', 'cummulativeQuoteQty': '0.00000000', 'status': 'EXPIRED', 'timeInForce': 'FOK', 'type': 'LIMIT', 'side': 'BUY', 'fills': []}
Current Price: 2687.06000000
{'symbol': 'ETHUSDT', 'orderId': 962403, 'orderListId': -1, 'clientOrderId': '5q8GPEg5bYgzoW7PUnR2VN', 'transactTime': 1651927997903, 'price': '2687.00000000', 'origQty': '0.01000000', 'executedQty': '0.00000000', 'cummulativeQuoteQty': '0.00000000', 'status': 'EXPIRED', 'timeInForce': 'FOK', 'type': 'LIMIT', 'side': 'BUY', 'fills': []}
Current Price: 2686.87000000
{'symbol': 'ETHUSDT', 'orderId': 962420, 'orderListId': -1, 'clientOrderId': 'ENxCN1JAW4OcxLiAkSmdIH', 'transactTime': 1651927999639, 'price': '2687.00000000', 'origQty': '0.01000000', 'executedQty': '0.01000000', 'cummulativeQuoteQty': '26.86870000', 'status': 'FILLED', 'timeInForce': 'FOK', 'type': 'LIMIT', 'side': 'BUY', 'fills': [{'price': '2686.87000000', 'qty': '0.01000000', 'commission': '0.00000000', 'commissionAsset': 'ETH', 'tradeId': 225595}]}
False # loop closed
Solution 2:[2]
I don't think it is possible. This is due to the nature of an exchange order-matching system. When you send an order to buy 0.34303ETH @2915.16, the exchange looks for people who wants to sell ETH @2915.16, aka. the counter-party. However the amount they want to sell can rarely be exactly 0.34303ETH. It can be greater or lesser than this quantity. That's why you can get partially filled when the market moves around the price level specified vastly.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | tyson.wu |