Back to Blog

Building a Binance Futures Client in Python – Part II

Posted by

Binance futures client

Introduction

Part I of this blog laid the groundwork for building a Binance Futures client.

In Part II, I begin the actual implementation of the BinanceFuturesClient class, focusing on its public methods and how to interface with Binance's FAPI (USDⓈ-Margined Perpetual Futures).

To maintain code readability and focus on structure, this implementation of the BinanceFuturesClient class deliberately omits robust error handling. However, it's important to understand that error handling is not optional in production code. The Binance API can occasionally return malformed responses, rate-limit errors, or temporary connection failures. In live trading, failing to anticipate and recover gracefully from these issues can lead to missed trades or, worse, unintended positions. A production-grade client should include retries, timeouts, and meaningful logging at a minimum.

We'll cover only the endpoints I find most useful in practice. While Binance offers a vast set of functionalities, my goal is to keep the client lean and tailored to real-world usage. You can explore the full API documentation here: https://developers.binance.com/docs/derivatives.

One important disclaimer: I approach this as a researcher, not a professional software developer. The code is functional and well-tested for my purposes, but it is not production-hardened. Use it as a learning resource or a starting point for your own projects.

Additionally, it is worth noting that trading futures is inherently a high-risk activity. You can lose more than your initial margin. This material is for educational purposes only.

FAPI endpoints

An endpoint is a specific URL pattern provided by an API that allows you to perform certain operations, such as retrieving market data or submitting trade orders. The following are some key public FAPI endpoints available for Binance USDⓈ-Margined Futures:

As introduced in Part I, Binance also offers DAPI (coin-margined futures) and PAPI (portfolio margin trading) endpoints. Please note that the examples and functions presented here are specifically tailored for FAPI. Endpoint structures and behaviors may vary across these APIs.

Imports

For this project, I will use the following imports:

import datetime
import pandas as pd
import numpy as np
import time
import json
import hmac
import hashlib
import requests
from urllib.parse import urlencode

Binance Futures Client: Public Functions

I will start by writing the public methods for the BinanceFuturesClient class.

class BinanceFuturesClient:

    def __init__(self, api_key, api_secret, mainnet=True):
        if mainnet:
          self.base_url = 'https://fapi.binance.com'
        else:
            self.base_url = 'https://testnet.binancefuture.com'
        self.api_key = api_key
        self.api_secret = api_secret
        self.symbol_info = self.get_symbol_info()
        self.cols = ['open_time','open','high','low','close','volume','close_time',
                     'quote_volume','number_trades', 'taker_buy_base_asset_volume',
                     'taker_buy_quote_asset_volume','ignore']

The function get_symbol_info will be described below. Its purpose is to collect all tradable symbols along with important information, such as tick size (the minimum allowed price movement).

The variable cols will be useful when getting the klines. Klines are candlestick data that, for a specified interval (one minute, one hour, or a day), contain open, high, low, and close prices (OHLC) as well as volumes.

Submitting Requests to Binance

These helper functions encapsulate HTTP requests to Binance. They simplify making REST calls and ensure the response is properly handled:

    def send_public_request(self, endpoint, params=None):
        # TODO: Add try/except for request errors and retries 
        if params is None:
            params = {}
        url = self.base_url + endpoint
        response = requests.get(url, params=params, timeout=5)
        response.raise_for_status()  # need to raise HTTPError
        return response.json()

Requesting Data from Binance

Best Bid and Ask

    def get_best_price(self,
                       endpoint='/fapi/v1/ticker/bookTicker',
                       params={'symbol':'BTCUSDT'}):
        return self.send_public_request(endpoint=endpoint,params=params)

The get_best_price function retrieves the best bid and best ask, along with their corresponding quantities, from the order book. Here is an example of usage and response:

client.get_best_price(params={'symbol':'ETHUSDT'})
{'symbol': 'ETHUSDT',
 'bidPrice': '3640.43',
 'bidQty': '0.270',
 'askPrice': '3699.99',
 'askQty': '0.068',
 'time': 1753692520860,
 'lastUpdateId': 57996135530}            

Funding Rate

    def get_funding_rate(self,
                         endpoint='/fapi/v1/fundingRate',
                         params={'symbol':'BTCUSDT'}):
        '''
            • Funding happens every 8 hours (typically at 00:00 UTC, 
              08:00 UTC, and 16:00 UTC).
            • At those times, funding payments are exchanged directly 
              between long and short traders.
            • Binance doesn’t charge a fee — instead:
                ◦ If you're long, you pay the short if the funding rate
                  is positive.
                ◦ If you're short, you receive from the long if the funding
                  rate is positive.
        '''

        response = self.send_public_request(endpoint, params=params)
        funding = {}
        for res in response:
            ts = int_2_timestamp(int(res['fundingTime']))
            funding[ts] = res['fundingRate']
        return funding

The get_funding_rate function gets the historical funding rate. As an example, the code below gets the funding rates for XLMUSDT starting from the previous 2 days until the time the function was run:

star_time = datetime.datetime.now(datetime.UTC) + datetime.timedelta(days=-2)
st_int = timestamp_2_int(star_time, unit = "ms")
client.get_funding_rate(params={'symbol':'XLMUSDT','startTime':st_int})
{Timestamp('2025-07-27 16:00:00+0000', tz='UTC'): '-0.00750000',
 Timestamp('2025-07-28 00:00:00+0000', tz='UTC'): '-0.00750000',
 Timestamp('2025-07-28 08:00:00.001000+0000', tz='UTC'): '-0.00750000',
 Timestamp('2025-07-28 16:00:00+0000', tz='UTC'): '-0.00750000',
 Timestamp('2025-07-29 00:00:00+0000', tz='UTC'): '-0.00750000',
 Timestamp('2025-07-29 08:00:00+0000', tz='UTC'): '-0.00724509'}

As you can see from the above output, funding rates are charged/paid every 8 hours at 0, 8, and 16 UTC. In this XLMUSDT (stellar) example, the funding rate is negative, meaning that the shorts pay the longs. Note that Binance does not keep any part of the funding rate. The moneys go directly from the parties paying it to the participants receiving it.

The get_funding_rate function needs a helper function int_2_timestamp, which will be defined in the Util functions section, that converts an integer into a timestamp.

Additionally, in this example, we invoked the get_funding_rate function, requesting the funding rates starting from 2 days ago. For this, we need the function timestamp_2_int, also defined in the Util functions section, that converts a datetime into an integer.

Candlestick Data

    def get_klines(self, 
                   endpoint='/fapi/v1/klines',
                   params={'symbol': 'BTCUSDT',
                           'interval': '1h',
                           'limit': 500}):
        price_data = self.send_public_request(
            endpoint=endpoint,
            params=params,
        )
        price_data = pd.DataFrame(price_data, columns=self.cols)
        price_data['open'] = pd.to_numeric(price_data['open'])
        price_data['close'] = pd.to_numeric(price_data['close'])
        return price_data

The get_klines function fetches historical candlestick (kline) data for a given symbol. Each candlestick represents open, high, low, and close prices (OHLC) along with volume and other metadata for a given interval. Klines are indexed by their open time and are useful for both technical analysis and backtesting.

Order book

    def get_order_book(self,
                       endpoint='/fapi/v1/depth',
                       params={'symbol': 'BTCUSDT', 'limit': 10}):

        order_book = self.send_public_request(
            endpoint,
            params=params
        )
        return order_book

The get_order_book function gets the order book. Here is an example of usage and response:

client.get_order_book(params={'symbol':'ADAUSDT','limit':5})
{'lastUpdateId': 57996436676,
 'E': 1753693317457,
 'T': 1753693317451,
 'bids': [['0.80000', '118210274'],
  ['0.79610', '424'],
  ['0.79600', '42'],
  ['0.79590', '2301'],
  ['0.79580', '2874']],
 'asks': [['0.80010', '3401'],
  ['0.80020', '3756'],
  ['0.80040', '4078'],
  ['0.80060', '40'],
  ['0.80090', '3772']]}            
                 Order Book (simplified)

          Asks (Sellers)                Bids (Buyers)
    -----------------------------------------------
    Price     Quantity            Price     Quantity
    0.80090   3772                0.80000   118210274  ← highest bid
    0.80060     40                0.79610        424
    0.80040   4078                0.79600         42
    0.80020   3756                0.79590       2301
    0.80010   3401 ← lowest ask   0.79580       2874
    -----------------------------------------------

                  Spread = 0.800100.80000 = 0.00010

The order book shows two sides of the market: bids and asks.

Bids represent buy orders. Each bid is a price level and the quantity buyers are willing to purchase at that price. For example, a bid of 0.79610 with a quantity of 424 means that buyers are willing to purchase 424 contracts at a price of 0.79610.

Asks represent sell orders. Each ask is a price level and the quantity sellers are offering. For example, an ask of 0.80010 with a quantity of 3401 means sellers are offering 3401 contracts at the price of 0.80010.

The bid-ask spread is the difference between the highest bid price (the best price a buyer is willing to pay) and the lowest ask price (the best price a seller is willing to accept). In this example, the highest bid is 0.80000 and the lowest ask is 0.80010, resulting in a spread of 0.00010. The spread is a direct measure of market liquidity: tighter spreads usually indicate deeper, more liquid markets, while wider spreads suggest lower liquidity.

One useful way to judge whether the spread is “tight” or “wide” is to measure it in terms of ticks. The tick size is the minimum price movement allowed for a symbol. Continuing to follow our example, ADAUSDT tick size is 0.0001, and the spread is 0.0001, which means the spread is exactly 1 tick. If the spread is exactly one tick wide, it means the market is very liquid — buyers and sellers are meeting at the smallest possible increment. A spread of several ticks, on the other hand, suggests lower liquidity or less active trading in that market.

In formula form, the spread in ticks can be written as: (best ask – best bid) / tick size. For ADAUSDT: (0.8001 – 0.8000) / 0.0001 = 1 tick.

Symbol info

    def get_symbol_info(self,
                        endpoint='/fapi/v1/exchangeInfo'):
            data = self.send_public_request(endpoint)
            symbol_info = {}
            for item in data['symbols']:
                filters = {f['filterType']: f for f in item['filters']}
                symbol_info[item['symbol']] = {
                    'baseAsset': item['baseAsset'],
                    'quoteAsset': item['quoteAsset'],
                    'minQty': float(filters['LOT_SIZE']['minQty']),
                    'maxQty': float(filters['LOT_SIZE']['maxQty']),
                    'stepSize': float(filters['LOT_SIZE']['stepSize']),
                    'tickSize': float(filters['PRICE_FILTER']['tickSize']),
                    'minPrice': float(filters['PRICE_FILTER']['minPrice']),
                    'minNotional': float(filters['MIN_NOTIONAL']['notional']),
                }
            return symbol_info

The get_symbol_info function retrieves metadata for all tradable symbols on the FAPI exchange. This includes important trading constraints, such as minimum order quantities, tick sizes, and notional value limits. This data is essential when validating orders before submission.

client.get_symbol_info()
{'BTCUSDT': {'baseAsset': 'BTC',
  'quoteAsset': 'USDT',
  'minQty': 0.001,
  'maxQty': 1000.0,
  'stepSize': 0.001,
  'tickSize': 0.1,
  'minPrice': 261.1,
  'minNotional': 100.0},
 'ETHUSDT': {'baseAsset': 'ETH',
  'quoteAsset': 'USDT',
  'minQty': 0.001,
  'maxQty': 10000.0,
  'stepSize': 0.001,
  'tickSize': 0.01,
  'minPrice': 18.67,
  'minNotional': 20.0},
  etc.
}

Util functions

def int_2_timestamp(ts):
    digits = (np.log10(ts) + 1).astype(int)
    dt = None
    if digits == 10:
        dt = pd.to_datetime(ts, unit="s", utc=True)
    elif digits == 13:
        dt = pd.to_datetime(ts, unit="ms", utc=True)
    elif digits == 16:
        dt = pd.to_datetime(ts, unit="us", utc=True)
    return dt

def timestamp_2_int(dt, unit = "ms"):
     if unit == "ms":
        return int(dt.timestamp() * 1_000)
    elif unit == "us":
        return int(dt.timestamp() * 1_000_000)
    elif unit == "s":
        return int(dt.timestamp())
    else:
        raise ValueError(f"Unsupported unit: {unit}")

The int_2_timestamp function converts an integer to a timestamp. The timestamp_2_int function performs the reverse operation, converting a timestamp to an integer based on the specified unit. The timestamp should be timezone-aware or UTC.

The units should be 's' (seconds), 'ms' (milliseconds), or 'us' (microseconds).

Conclusion

In Part I, I covered the various Binance APIs, the distinction between Mainnet and Testnet, and how to obtain the API keys.

In this article, I implemented the public functions of the Binance class.

In Part III, I will cover the private (signed) endpoints of the Binance Futures API, including how to authenticate requests, check account status, set leverage and margin, place and cancel orders, and review trades and positions.

Want deeper insights into risk and trading strategies? Subscribe to Trading Shepherd today and stay ahead of market volatility!"