Building a Binance Futures client in Python - part III
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 started the actual implementation of the BinanceFuturesClient
class, focusing on its public methods and how to interface with Binance's FAPI (USDⓈ-Margined Perpetual Futures).
In Part III, I cover the signed methods of the BinanceFuturesClient
class for Binance's FAPI (USDⓈ-Margined Perpetual Futures).
To maintain 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 at 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 private 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 of the private FAPI endpoints available for Binance USDⓈ-Margined Futures:
- /fapi/v3/account - Get current account information.
- /fapi/v1/leverage - Change the initial leverage of a specific symbol.
- /fapi/v1/marginType - Change symbol level margin type.
- /fapi/v1/openOrders - Retrieve all open orders for a given symbol.
- /fapi/v1/order - Order placement, canceling, and modification.
- /fapi/v3/positionRisk - Get current position information. Only symbol that has a position or open orders will be returned.
- /fapi/v1/userTrades - Get trades for a specific account and 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.
Binance futures base client: private functions
Signed requests
Certain Binance API endpoints require authentication. This is done using a process called signing. Signing ensures that the request really comes from you (the holder of the API key and secret) and that the data hasn’t been tampered with.
The process works as follows:
- We add a
timestamp
parameter to the request (in milliseconds). This prevents replay attacks. Binance rejects old requests. - We take all the request parameters, sort them, and turn them into a query string (e.g.,
symbol=BTCUSDT×tamp=1691661059000
). - We use our API secret to create an HMAC-SHA256 signature of that query string. This produces a unique hash only possible with the secret.
- We append the signature to the request as another parameter (
signature=...
). - We include our API key in the request headers (
X-MBX-APIKEY
) so Binance knows which account we’re authenticating.
If the signature doesn’t match, Binance will reject the request. If the timestamp is too old (more than a few seconds), it will also reject it.
These helper functions encapsulate HTTP requests to Binance. They simplify making REST calls and ensure the response is properly handled:
def sign(self, params):
# 1) Build the exact query string Binance will see.
# IMPORTANT: The signature must be over the SAME string you send on the wire.
query_string = urlencode(params) # e.g. "symbol=ETHUSDT×tamp=1753966"
# 2) HMAC-SHA256 of the query string using your API secret.
# This proves the request came from someone who knows the secret (you).
signature = hmac.new(
self.api_secret.encode('utf-8'), # secret bytes
query_string.encode('utf-8'), # message bytes (exact query string)
hashlib.sha256, # digest algorithm
).hexdigest() # hex string Binance expects
# 3) Append signature as a regular parameter.
params['signature'] = signature
return params
def get_headers(self):
# Binance uses this header to associate the request with your API key.
# The secret is NEVER sent in headers; only used locally to compute the signature.
return {'X-MBX-APIKEY': self.api_key}
def send_signed_request(
self,
http_method,
endpoint,
params=None
):
if params is None:
params = {}
# 1) Anti-replay: timestamp (ms). Binance rejects old requests.
# Many users also include recvWindow (e.g., 5000 ms) to allow small clock drift.
params['timestamp'] = int(time.time() * 1000)
# 2) Sign over the EXACT query string you will send.
params = self.sign(params)
# 3) Build full URL.
url = self.base_url + endpoint
# 4) Send. For Binance Futures REST, signed POSTs/DELETEs also send params on the query string.
if http_method == 'GET':
response = requests.get(url, headers=self.get_headers(), params=params)
elif http_method == 'POST':
response = requests.post(url, headers=self.get_headers(), params=params)
elif http_method == 'DELETE':
response = requests.delete(url, headers=self.get_headers(), params=params)
else:
raise ValueError('Unsupported HTTP method')
# 5) Return parsed JSON (error handling intentionally omitted in this base client).
return response.json()
Account-Related Functions
Account Info
def get_account_info(self,
endpoint='/fapi/v3/account'):
return self.send_signed_request('GET', endpoint)
The get_account_info
function gets the current account information. Users in single-asset/multi-assets mode will see a different value. Here is an example of usage and an extract of the response (multi-assets mode):
client.get_account_info()
{'totalInitialMargin': '0.38597227',
'totalMaintMargin': '0.01929861',
'totalWalletBalance': '6211.08842366',
'totalUnrealizedProfit': '0.90983767',
'totalMarginBalance': '6211.99826133',
'totalPositionInitialMargin': '0.38597227',
'totalOpenOrderInitialMargin': '0.00000000',
'totalCrossWalletBalance': '6219.99599029',
'totalCrossUnPnl': '0.90983767',
'availableBalance': '6220.42574292',
'maxWithdrawAmount': '6211.06912505',
'assets': [{'asset': 'USDT',
'walletBalance': '6211.08842366',
'unrealizedProfit': '0.90983767',
'marginBalance': '6211.99826133',
'maintMargin': '0.01929861',
'initialMargin': '0.38597227',
'positionInitialMargin': '0.38597227',
'openOrderInitialMargin': '0.00000000',
'crossWalletBalance': '6219.99599029',
'crossUnPnl': '0.90983767',
'availableBalance': '6220.42574292',
'maxWithdrawAmount': '6211.06912505',
'updateTime': 1752307592683},
'positions': [{'symbol': 'ETHUSDT',
'positionSide': 'BOTH',
'positionAmt': '0.001',
'unrealizedProfit': '0.90983767',
'isolatedMargin': '0',
'notional': '3.85972277',
'isolatedWallet': '0',
'initialMargin': '0.38597227',
'maintMargin': '0.01929861',
'updateTime': 1752307437817}]}
Now I detail the meaning of some of these variables, starting with the ETHUSDT open position:
- 'positionAmt': position held.
- 'unrealizedProfit': current positions' gain or loss from the moment they were opened.
- 'notional': Current spot price.
- 'initialMargin': Initial Margin. This is the minimum amount of capital required to open a futures position. We can see that the leverage is 10 by dividing 'notional' by 'initialMargin'.
- 'maintMargin': Maintenance margin. This is the minimum balance the trader must maintain in the account once the trade is open.
Now I cover the portfolio variables:
- 'totalInitialMargin': Total initial margin required.
- 'totalMaintMargin': Total maintenance margin for USDT quoted assets.
- 'totalWalletBalance': The total balance of your futures wallet in USDT, excluding any unrealized profit or loss from open positions. This is essentially the “cash” you have on deposit.
- 'totalUnrealizedProfit': The combined unrealized PnL across all open positions. If your current positions move in your favor, this value is positive; if they move against you, it is negative.
- 'totalMarginBalance': The total margin balance of your futures account, expressed in USDT. This value represents your wallet balance plus both realized and unrealized PnL. In practice, changes in this number reflect your profit and loss over time.
- 'totalCrossWalletBalance' / 'totalCrossUnPnl': Cross-portfolio wallet balance and unrealized PnL (in multi-assets mode, these are in USD and can include multiple collateral assets).
- 'availableBalance': The part of your equity that is free to deploy for new orders/positions. It is computed by Binance's risk engine, taking into account the margin already in use, order holds, fees/funding accrual, and (in multi-assets mode) cross-asset offsets. There isn't a single public formula.
- 'maxWithdrawAmount': Maximum transferable amount without breaching maintenance margin (often close to wallet balance minus maintenance margin).
Account Balance
def get_account_balance(self, endpoint='/fapi/v2/balance'):
return self.send_signed_request('GET', endpoint)
The get_account_balance
function retrieves your futures wallet balance for each asset. This is different from get_account_info
, which includes margin and position details.
client.get_account_balance()
[{'accountAlias': 'fWfWuXTiAuoCAu',
'asset': 'FDUSD',
'balance': '0.00000000',
'crossWalletBalance': '0.00000000',
'crossUnPnl': '0.00000000',
'availableBalance': '0.00000000',
'maxWithdrawAmount': '0.00000000',
'marginAvailable': True,
'updateTime': 0},
{'accountAlias': 'fWfWuXTiAuoCAu',
'asset': 'BFUSD',
'balance': '0.00000000',
'crossWalletBalance': '0.00000000',
'crossUnPnl': '0.00000000',
'availableBalance': '0.00000000',
'maxWithdrawAmount': '0.00000000',
'marginAvailable': True,
'updateTime': 0},
{'accountAlias': 'fWfWuXTiAuoCAu',
'asset': 'BNB',
'balance': '0.00000000',
'crossWalletBalance': '0.00000000',
'crossUnPnl': '0.00000000',
'availableBalance': '0.00000000',
'maxWithdrawAmount': '0.00000000',
'marginAvailable': True,
'updateTime': 0},
{'accountAlias': 'fWfWuXTiAuoCAu',
'asset': 'ETH',
'balance': '0.00000000',
'crossWalletBalance': '0.00000000',
'crossUnPnl': '0.00000000',
'availableBalance': '0.00000000',
'maxWithdrawAmount': '0.00000000',
'marginAvailable': True,
'updateTime': 0},
{'accountAlias': 'fWfWuXTiAuoCAu',
'asset': 'BTC',
'balance': '0.00000000',
'crossWalletBalance': '0.00000000',
'crossUnPnl': '0.00000000',
'availableBalance': '0.00000000',
'maxWithdrawAmount': '0.00000000',
'marginAvailable': True,
'updateTime': 0},
{'accountAlias': 'fWfWuXTiAuoCAu',
'asset': 'USDT',
'balance': '6211.18267760',
'crossWalletBalance': '6220.09024423',
'crossUnPnl': '1.67581490',
'availableBalance': '6220.90927913',
'maxWithdrawAmount': '6211.15954910',
'marginAvailable': True,
'updateTime': 1752307592683},
{'accountAlias': 'fWfWuXTiAuoCAu',
'asset': 'USDC',
'balance': '0.00000000',
'crossWalletBalance': '0.00000000',
'crossUnPnl': '0.00000000',
'availableBalance': '0.00000000',
'maxWithdrawAmount': '0.00000000',
'marginAvailable': True,
'updateTime': 0}]
Leverage and Margin
def set_leverage(self, symbol, leverage=1, endpoint='/fapi/v1/leverage'):
params = {
'symbol': symbol,
'leverage': leverage,
}
return self.send_signed_request('POST', endpoint, params=params)
Leverage lets you control a larger position size with a smaller amount of capital. For example, at 10× leverage, you can open a 10,000 USDT position with just 1,000 USDT of margin. This magnifies both gains and losses. The set_leverage
function changes the maximum leverage for a specific symbol. Please note that Binance has leverage caps, which vary depending on the symbol and position size.
def set_margin(self, symbol, marginType='ISOLATED', endpoint='/fapi/v1/marginType'):
params = {
'symbol': symbol,
'marginType': marginType,
}
return self.send_signed_request('POST', endpoint, params=params)
There are two margin types in Binance Futures:
- Cross Margin: All available margin in your account can be used to prevent liquidation of any position. Losses in one position can drain your balance and affect other positions.
- Isolated Margin: The margin assigned to a position is limited to the amount allocated for that trade. Liquidation only affects that position.
The set_margin
function changes the margin type for a given symbol.
Submitting, Canceling Orders
I will now implement the necessary functions to submit and cancel orders, as well as other related functions. At the end of this section, I will exemplify how this function works.
Canceling Orders
def cancel_order(self,
params,
endpoint='/fapi/v1/order',
):
if not params:
print('params was empty')
return None
return self.send_signed_request('DELETE', endpoint, params)
Executing an Order
def execute_order(self,
params,
endpoint='/fapi/v1/order'):
if not params:
print('params was empty')
return None
order = self.send_signed_request(
http_method='POST',
endpoint=endpoint,
params=params
)
return order
There are two main types of orders: limit and market orders.
A limit order is an order to buy or sell at a specific price or better. A buy limit order will only execute at the limit price or lower; a sell limit order will only execute at the limit price or higher. This allows you to control the execution price but does not guarantee that the order will be filled if the market never reaches your price.
In contrast, a market order executes immediately at the best available price. This guarantees execution but not the exact price.
Here is an example of how to submit a limit order:
limit_order = client.execute_order({
'symbol': 'ETHUSDT',
'side': 'BUY',
'type': 'LIMIT',
'timeInForce': 'GTC', # Good until canceled
'quantity': 1,
'price': 1700
})
Both quantity and price have to be valid. As you can see in our Part II, the ETHUSDT contract minimum quantity is 0.001. Additionally, the minimum amount required for trading is 20 USDT. With the price of ETHUSDT at around 3700 at the time of writing this paragraph, 0.001x3700 is equivalent to just 3.7 USDT. Consequently, at this price level, the minimum valid amount is 0.006.
ETHUSDT tick size (minimum price movement) is 0.01 (see blog's part II). This means that setting a price of 1700.01 for the limit order would be valid, but a price of 1700.001 would not.
Getting Open Orders
def get_open_orders(self, symbol):
return self.send_signed_request('GET', '/fapi/v1/openOrders', {'symbol': symbol})
The get_open_orders
function returns all currently open orders for a given symbol. This is useful for confirming pending orders or managing them programmatically.
Order Status
def get_order_status(self, symbol, orderID, endpoint='/fapi/v1/order'):
order_status = self.send_signed_request('GET', endpoint,
{
'symbol': symbol,
'orderId': orderID,
})
return order_status
Trading Example
For this trading example, I'll use the limit order described above. Once the order is submitted, its details can be displayed for review.
display(limit_order)
{'orderId': 4938425133,
'symbol': 'ETHUSDT',
'status': 'NEW',
'clientOrderId': '9ainLT2PyWL3mfWmhtJjwQ',
'price': '1700.00',
'avgPrice': '0.00',
'origQty': '1.000',
'executedQty': '0.000',
'cumQty': '0.000',
'cumQuote': '0.00000',
'timeInForce': 'GTC',
'type': 'LIMIT',
'reduceOnly': False,
'closePosition': False,
'side': 'BUY',
'positionSide': 'BOTH',
'stopPrice': '0.00',
'workingType': 'CONTRACT_PRICE',
'priceProtect': False,
'origType': 'LIMIT',
'priceMatch': 'NONE',
'selfTradePreventionMode': 'EXPIRE_MAKER',
'goodTillDate': 0,
'updateTime': 1753966897526}
I can now check the order status:
client.get_order_status('ETHUSDT', 4938425133)
{'orderId': 4938425133,
'symbol': 'ETHUSDT',
'status': 'NEW',
'clientOrderId': '9ainLT2PyWL3mfWmhtJjwQ',
'price': '1700.00',
'avgPrice': '0.00',
'origQty': '1.000',
'executedQty': '0.000',
'cumQuote': '0.00000',
'timeInForce': 'GTC',
'type': 'LIMIT',
'reduceOnly': False,
'closePosition': False,
'side': 'BUY',
'positionSide': 'BOTH',
'stopPrice': '0.00',
'workingType': 'CONTRACT_PRICE',
'priceProtect': False,
'origType': 'LIMIT',
'priceMatch': 'NONE',
'selfTradePreventionMode': 'EXPIRE_MAKER',
'goodTillDate': 0,
'time': 1753966897526,
'updateTime': 1753966897526}
I can check for open orders:
client.get_open_orders('ETHUSDT')
{[{'orderId': 4938425133,
'symbol': 'ETHUSDT',
'status': 'NEW',
'clientOrderId': '9ainLT2PyWL3mfWmhtJjwQ',
'price': '1700',
'avgPrice': '0',
'origQty': '1',
'executedQty': '0',
'cumQuote': '0.00000',
'timeInForce': 'GTC',
'type': 'LIMIT',
'reduceOnly': False,
'closePosition': False,
'side': 'BUY',
'positionSide': 'BOTH',
'stopPrice': '0',
'workingType': 'CONTRACT_PRICE',
'priceProtect': False,
'origType': 'LIMIT',
'priceMatch': 'NONE',
'selfTradePreventionMode': 'EXPIRE_MAKER',
'goodTillDate': 0,
'time': 1753966897526,
'updateTime': 1753966897526}]
I can cancel my orders (if they are still open):
client.cancel_order(params={'symbol':'ETHUSDT', 'orderId': 4938425133})
{'orderId': 4938425133,
'symbol': 'ETHUSDT',
'status': 'CANCELED',
'clientOrderId': '9ainLT2PyWL3mfWmhtJjwQ',
'price': '1700.00',
'avgPrice': '0.00',
'origQty': '1.000',
'executedQty': '0.000',
'cumQty': '0.000',
'cumQuote': '0.00000',
'timeInForce': 'GTC',
'type': 'LIMIT',
'reduceOnly': False,
'closePosition': False,
'side': 'BUY',
'positionSide': 'BOTH',
'stopPrice': '0.00',
'workingType': 'CONTRACT_PRICE',
'priceProtect': False,
'origType': 'LIMIT',
'priceMatch': 'NONE',
'selfTradePreventionMode': 'EXPIRE_MAKER',
'goodTillDate': 0,
'updateTime': 1753967622077}
Check Positions and Trades
def get_position(self, endpoint='/fapi/v3/positionRisk', symbol='BTCUSDT'):
positions = self.send_signed_request('GET', endpoint)
for pos in positions:
if pos['symbol'] == symbol:
return pos
return {}
The get_position
function gets information about both positions held and open. Note that /fapi/v2/positionRisk
only returns held positions.
client.get_position(symbol='ETHUSDT')
{'symbol': 'ETHUSDT',
'positionSide': 'BOTH',
'positionAmt': '0.001',
'entryPrice': '2949.885092733',
'breakEvenPrice': '-1054916.028356',
'markPrice': '4620.23931877',
'unRealizedProfit': '1.67035422',
'liquidationPrice': '0',
'isolatedMargin': '0',
'notional': '4.62023931',
'marginAsset': 'USDT',
'isolatedWallet': '0',
'initialMargin': '0.46202393',
'maintMargin': '0.02310119',
'positionInitialMargin': '0.46202393',
'openOrderInitialMargin': '0',
'adl': 1,
'bidNotional': '0',
'askNotional': '0',
'updateTime': 1752307437817}
def get_my_trades(
self,
symbol,
order_id = None,
start_time: datetime.datetime | None = None,
end_time: datetime.datetime | None = None,
endpoint = '/fapi/v1/userTrades',
):
# I typed the time arguments just to make clear.
def to_ms(dt: datetime.datetime) -> int:
if dt.tzinfo is None:
# assume UTC if naive
dt = dt.replace(tzinfo=datetime.UTC)
return timestamp_2_int(dt, unit='ms')
now_utc = datetime.datetime.now(datetime.UTC)
if start_time and (now_utc - start_time) > datetime.timedelta(days=180):
raise ValueError('start_time cannot be older than 6 months (~180 days).')
if start_time and end_time:
if end_time < start_time:
raise ValueError('end_time must be >= start_time.')
if (end_time - start_time) > datetime.timedelta(days=7):
raise ValueError('The span between start_time and end_time must be <= 7 days.')
params: dict[str, object] = {'symbol': symbol}
if order_id is not None:
params['orderId'] = int(order_id)
if start_time is not None:
params['startTime'] = to_ms(start_time)
if end_time is not None:
params['endTime'] = to_ms(end_time)
trades = self.send_signed_request('GET', endpoint, params=params)
return trades
The get_my_trades
function retrieves information about past trades.
client.get_my_trades('SOLUSDT',
order_id=232214776,
start_time=datetime.datetime(2025, 7, 4, tzinfo=datetime.timezone.utc)
[{'symbol': 'SOLUSDT',
'id': 12869968,
'orderId': 232214776,
'side': 'SELL',
'price': '149.9100',
'qty': '1',
'realizedPnl': '-2.67417546',
'quoteQty': '149.9100',
'commission': '0.05996400',
'commissionAsset': 'USDT',
'time': 1751619607421,
'positionSide': 'BOTH',
'maker': False,
'buyer': False}]
Conclusion
In Part I, I covered the various Binance APIs, the distinction between Mainnet and Testnet, and how to obtain the API keys.
In Part II, I began implementing the Binance class, starting with public functions.
In this part, I covered 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!"