With the Python-Binance API, my limit order is only partially filled

6k views Asked by At

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.

2

There are 2 answers

2
tyson.wu On

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.

0
Life is complex On

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