Lesson 4 — Order placement & fills (testnet)
Place limit and market orders on exchange testnets, handle partial fills and cancellations, account for slippage and fees, and persist order state for reconciliation.
Prerequisites
- Completion of Crypto Lessons 1–3 (env, auth, market data)
- Testnet API key/secret and working signed_request or CCXT instance
- Understanding of order types and basic risk limits
Overview
This lesson shows two common approaches:
- Using CCXT (convenient, unified interface) — great for prototyping
- Direct signed REST calls / WebSocket (required for exchanges or advanced features)
1) Recommended .env values
# .env
EXCHANGE_API_URL=https://testnet-api.example.com
EXCHANGE_API_KEY=your_testnet_api_key
EXCHANGE_API_SECRET=your_testnet_api_secret
EXCHANGE_WS_URL=wss://testnet-ws.example.com
DEFAULT_MARKET=BTC/USDT
MIN_TRADE_SIZE=0.0001
2) Place orders using CCXT (recommended for testnets)
CCXT simplifies order placement across many exchanges. Example below demonstrates a limit and a market order on a testnet-enabled exchange.
import os
import ccxt
from dotenv import load_dotenv
load_dotenv()
exchange = ccxt.binance({
'apiKey': os.getenv('EXCHANGE_API_KEY'),
'secret': os.getenv('EXCHANGE_API_SECRET'),
'enableRateLimit': True,
})
# enable sandbox/testnet if supported
if hasattr(exchange, 'set_sandbox_mode'):
exchange.set_sandbox_mode(True)
symbol = os.getenv('DEFAULT_MARKET','BTC/USDT')
# Load markets first
exchange.load_markets()
# 1) Place a limit buy order
order = exchange.create_limit_buy_order(symbol, 0.001, 20000.0) # size, price
print('Placed limit order:', order)
# 2) Place a market buy (testnet)
order2 = exchange.create_market_buy_order(symbol, 0.001)
print('Placed market order:', order2)
# 3) Check order status
status = exchange.fetch_order(order['id'], symbol)
print('Order status:', status['status'], 'filled:', status.get('filled'))
Note: CCXT fields vary across exchanges. Always inspect the returned order structure and map fields (id, status, filled, remaining, price).
3) Direct signed REST order example
If you’re using a direct provider that requires custom signing, adapt the signed_request helper from Lesson 2 and post to the orders endpoint. Example skeleton:
from auth_hmac import signed_request, resilient_request
def place_limit_order(symbol, side, size, price):
body = {
"symbol": symbol,
"side": side, # BUY or SELL
"type": "LIMIT",
"price": price,
"quantity": size,
"timeInForce": "GTC"
}
# path for your exchange
return resilient_request('POST', '/api/v1/orders', json_body=body)
# Example usage
resp = place_limit_order('BTC/USDT', 'BUY', 0.001, 20000.0)
print('Order response:', resp)
Important: Exact body keys differ per exchange. For example Binance uses side, type, quantity, while others may use different names.
4) Interpret order responses & handle partial fills
Order responses usually contain fields: orderId, status (NEW, PARTIALLY_FILLED, FILLED, CANCELED), filledQty, remainingQty. Use those to update your order store.
def interpret_order(resp):
# Normalize common fields
return {
'order_id': resp.get('id') or resp.get('orderId') or resp.get('clientOrderId'),
'status': resp.get('status'),
'filled': float(resp.get('filledQty') or resp.get('filled') or 0),
'remaining': float(resp.get('remainingQty') or resp.get('remaining') or 0),
'price': float(resp.get('price') or resp.get('avgPrice') or 0),
'raw': resp
}
Partial fills: If ‘remaining’ > 0 you can cancel the rest, reprice, or wait. Record fills immediately for P/L and reconciliation.
5) Cancel/replace and idempotency
Some exchanges support direct modify/replace; otherwise implement cancel followed by new order. Use client-side idempotency keys (clientOrderId) when supported.
# CCXT cancel example
cancel_resp = exchange.cancel_order(order['id'], symbol)
print('Cancel response:', cancel_resp)
# Cancel + replace pattern
def cancel_and_replace(order_id, symbol, new_size, new_price):
exchange.cancel_order(order_id, symbol)
return exchange.create_limit_buy_order(symbol, new_size, new_price)
Warning: Race conditions: an order may fill between your cancel and replace. Use idempotency and reconcile with the exchange’s order history.
6) Fees, slippage & execution models
Always account for fees and expected slippage:
- Subtract taker/maker fees when computing expected P/L
- Estimate slippage based on current orderbook available size at target price
- Use limit orders to control price, market orders to prioritize immediacy
# simple slippage estimate using orderbook snapshot
def estimate_slippage(orderbook, side, desired_size):
# orderbook: {'bids':[[price,size],...],'asks':[[price,size],...]]}
remaining = desired_size
avg_price = 0.0
if side == 'BUY':
asks = orderbook.get('asks',[])
for price, size in asks:
take = min(remaining, size)
avg_price += float(price) * take
remaining -= take
if remaining <= 0: break
else:
bids = orderbook.get('bids',[])
for price, size in bids:
take = min(remaining, size)
avg_price += float(price) * take
remaining -= take
if remaining <= 0: break
if remaining > 0:
return None # insufficient depth
return avg_price / desired_size
Tip: Use small test sizes on testnet to measure real slippage behavior before trusting model estimates.
7) Persistent order store & reconciliation
Persist each order record with timestamps, idempotency keys, responses and fill updates. Reconcile by fetching exchange order history and matching by clientOrderId/orderId.
import json
from pathlib import Path
ORDERS_LOG = Path('./orders_crypto.json')
def log_order(record):
arr = []
if ORDERS_LOG.exists():
try:
arr = json.loads(ORDERS_LOG.read_text())
except Exception:
arr = []
arr.append(record)
ORDERS_LOG.write_text(json.dumps(arr, indent=2))
Reconcile job: run a periodic job: fetch recent trades/orders from exchange, match local records by clientOrderId, update filled/remaining and detect orphan remote orders.
8) Safety & test tips
- Start with the smallest test size on live; use testnet for full flow validation.
- Never hardcode secrets; keep in .env or a secrets manager.
- Implement circuit breakers (max failed orders, abnormal fees/spreads) and logging/alerts.
What you’ll build next
Lesson 5 will wire webhook alerts (TradingView Pine) to your crypto webhook receiver and show how to validate and forward signals to the order placement layer.
