Lesson 4 — Order placement (sandbox)
Place BACK and LAY orders safely in the Betfair sandbox, handle partial/unmatched fills, cancellations, idempotency and retries. This lesson focuses on safe order logic and interpreting API responses.
Prerequisites
- Lessons 1–3 completed (env, auth, market lookup)
- Working auth.request helper and marketId + selectionId
- Understanding of stake sizing and basic risk limits
Overview
The goal is a robust place_order function that uses idempotency tokens, performs pre-checks (market status, available size), respects minimum stakes, retries safely on transient errors, and records the full order response for reconciliation.
1) Place order — core example (orders.py)
Save this as orders.py. Update the endpoint path and request body to match your Betfair API (this example is a generalized pattern).
import uuid
import time
from auth import request # from Lesson 2
from decimal import Decimal
MIN_STAKE = Decimal('0.5') # example minimum stake
MAX_RETRIES = 3
def generate_idempotency():
return str(uuid.uuid4())
def validate_stake(stake):
stake = Decimal(stake)
if stake < MIN_STAKE:
raise ValueError(f"Stake {stake} is below minimum {MIN_STAKE}")
return stake
def place_order(market_id, selection_id, side, stake, price, idempotency_token=None):
"""
side: 'BACK' or 'LAY'
stake: Decimal or numeric
price: desired price (odds)
"""
if idempotency_token is None:
idempotency_token = generate_idempotency()
stake = validate_stake(stake)
body = {
"marketId": market_id,
"instructions": [
{
"selectionId": selection_id,
"side": side,
"orderType": "LIMIT",
"limitOrder": {
"size": float(stake),
"price": float(price),
"persistenceType": "LAPSE"
}
}
],
"customerRef": idempotency_token
}
# Place order with retries on transient failures
for attempt in range(1, MAX_RETRIES + 1):
try:
resp = request('POST', '/placeOrders', json_body=body)
# resp structure varies; parse for status and matched/unmatched details
return resp
except Exception as e:
# Retry on network/server errors; do not retry if error indicates invalid input
if attempt < MAX_RETRIES:
time.sleep(0.5 * (2 ** (attempt - 1)))
continue
raise
Idempotency: Use a customerRef (idempotency token) so retries of request at the application level don't create duplicate bets. Record the token and server response for reconciliation.
2) Parse order response & handle partial fills
After placing an order, inspect the response for instructionReports to find status: SUCCESS, FAILURE, or ACCEPTED with details about matched/remaining amounts.
def interpret_place_response(resp):
reports = resp.get('instructionReports') or resp.get('instructionReports', [])
results = []
for r in reports:
status = r.get('status')
error = r.get('errorCode')
instruction = r.get('instruction') or {}
descr = {
'status': status,
'error': error,
'placedDate': r.get('placedDate'),
'sizeMatched': r.get('sizeMatched') or r.get('matched') or 0,
'sizeRemaining': r.get('sizeRemaining') or r.get('remaining') or 0,
'instruction': instruction
}
results.append(descr)
return results
Partial fills: If sizeRemaining > 0, decide whether to cancel the rest, reprice, or green-up with hedging. Record partial fills for P/L calculations.
3) Cancel and replace / cancelling orders
Example cancellation flow. Always check orderId/marketId and use idempotency for cancel instructions where supported.
def cancel_order(market_id, bet_id):
body = {
"marketId": market_id,
"instructions": [
{
"betId": bet_id
}
]
}
resp = request('POST', '/cancelOrders', json_body=body)
return resp
Note: cancelOrders responses include reports with status and sizeCancelled. Handle failures and log server error codes.
4) Safety, throttling & market checks
- Verify market book before placing orders — ensure market is OPEN and prices available.
- Respect API rate limits: use a token bucket / leaky-bucket to throttle order calls.
- Use timeouts on network calls and fail-safe logic to not leave unmatched orders unattended.
- Log everything: request body, response, idempotency token, and timestamps for reconciliation.
5) Reconciliation & persistent order store
Keep a local DB or file with each order record: customerRef, marketId, selectionId, side, size, price, placedAt, response, matchedSize, remainingSize, status. Use this store for downstream P/L and manual auditing.
# simple append to JSON log
import json
from pathlib import Path
LOG = Path('./orders_log.json')
def log_order(record):
arr = []
if LOG.exists():
try:
arr = json.loads(LOG.read_text())
except Exception:
arr = []
arr.append(record)
LOG.write_text(json.dumps(arr, indent=2))
What you'll build next
Lesson 5 will add webhooks and a server receiver for TradingView alerts and show how to safely map alerts to order placements (with HMAC verification).
