Building a Binance Futures Client in Python – Part II
Posted by

Related reading
Beyond the Buzzwords: Building a Real Blockchain from First Principles
In this post, I build a simple blockchain in Python, focusing on mining, cryptography and the creation of the first coin.

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:
- /fapi/v1/ticker/bookTicker - best bid and best ask, and quantity on the order book
- /fapi/v1/depth - order book
- /fapi/v1/exchangeInfo - current exchange trading rules and symbol information
- /fapi/v1/fundingRate - funding rate history
- /fapi/v1/klines - kline/candlestick bars for a symbol.
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.80010 – 0.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!"