Lesson 4 — Order placement (sandbox)

Skip to content

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.

Estimated time: 60 minutes • Skill level: Beginner → Intermediate

Use this as a starting point. Don’t commit secrets.

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).

Back to course hub

← Previous
Author: Stephane Patteux • Part of the Build your own bot series

Don’t miss our blog post!

We don’t spam! Read our privacy policy for more info.

Leave a Reply

Your email address will not be published. Required fields are marked *