Filter: Complete System Design
Version: 2.0 | January 2026 | Technical Design Document
What Filter Actually Does
Filter is an intelligent order routing layer that sits between users and execution venues. Every order that enters the system can be filled across one or more destinations:
- Internal Orderbook — Matched peer-to-peer with another user
- B-Book — We take the opposite side using our own capital
- External Exchange — Passed to Betfair
A single order might split across all three — e.g., £200 filled internally, £500 in B-Book, £300 on Betfair. The intelligence is in determining the optimal split for each order, managing B-Book risk, and maximizing margin while keeping liability bounded.
Part 1: The B-Book — How It Actually Works
Understanding Liability
When we B-Book an order, we become the counterparty. This creates liability — money we might owe if the user wins.
BACK Order (User bets on outcome happening):
- User stakes £100 at odds 2.50
- If they win: we pay £150 (stake × (odds - 1) = £100 × 1.50)
- If they lose: we keep £100
- Our liability on this bet: £150
LAY Order (User bets against outcome happening):
- User stakes £100 at odds 2.50
- If they win: we pay £100
- If they lose: we keep £150 (their liability to us)
- Our liability on this bet: £100
Critical insight: BACK and LAY orders on the same selection create opposite liabilities. They can offset each other.
The Net Position Concept
For any selection, we track our net position:
| Our Position | Meaning | We Profit If |
|---|---|---|
| Net LONG | More BACK orders than LAY | Selection loses |
| Net SHORT | More LAY orders than BACK | Selection wins |
| Balanced | BACK ≈ LAY | Either outcome (we keep spread) |
Example: Mumbai Indians to Win
| Order # | User Action | Stake | Odds | Our Liability | Running Net |
|---|---|---|---|---|---|
| 1 | BACK Mumbai | £1000 | 2.00 | -£1000 (if Mumbai wins) | -£1000 |
| 2 | LAY Mumbai | £500 | 2.10 | -£550 (if Mumbai loses) | Net: -£1000 BACK, -£550 LAY |
| 3 | BACK Mumbai | £500 | 1.95 | -£475 (if Mumbai wins) | Net: -£1475 BACK, -£550 LAY |
| 4 | LAY Mumbai | £800 | 2.05 | -£840 (if Mumbai loses) | Net: -£1475 BACK, -£1390 LAY |
After these 4 orders:
- If Mumbai wins: We lose £1475, gain £1390 = -£85
- If Mumbai loses: We gain £1500, lose £1390 = +£110
We are slightly net LONG on Mumbai (we want them to lose).
The £100K Pool and How Limits Work
The pool is not a bucket of money we spend. It's a liability limit — the maximum we're willing to owe at any moment.
Net Liability Calculation
BACK and LAY orders on the same selection offset each other. Our actual exposure is the net position, not the gross.
Per-Selection Net Exposure:
| If User... | We Take... | We Pay If Selection... |
|---|---|---|
| BACKs | LAY side | Wins |
| LAYs | BACK side | Loses |
Example on Mumbai Win:
| Order | User Side | Stake | Odds | Our Liability | Trigger |
|---|---|---|---|---|---|
| 1 | BACK | £1000 | 2.00 | £1000 | Mumbai wins |
| 2 | LAY | £600 | 2.00 | £600 | Mumbai loses |
| 3 | BACK | £400 | 2.50 | £600 | Mumbai wins |
If Mumbai wins:
- We pay BACK bettors: £1000 + £600 = £1600
- We collect from LAY bettors: £600 (their liability to us)
- Net: -£1000
If Mumbai loses:
- We collect from BACK bettors: £1000 + £400 = £1400
- We pay LAY bettors: £600
- Net: +£800
Net exposure on this selection = £1000 (our worst case)
Total Pool Exposure
Total exposure is the sum of net exposures across all selections:
Per selection: net_exposure = |back_liability - lay_liability|
Total worst case = Σ(net_exposure) across all open selections
Example across multiple selections:
| Selection | BACK Liability | LAY Liability | Net Exposure | Worst Outcome |
|---|---|---|---|---|
| Mumbai Win | £1600 | £600 | £1000 | Mumbai wins |
| CSK Win | £500 | £800 | £300 | CSK loses |
| RCB Win | £400 | £400 | £0 | Balanced! |
Total worst case = £1000 + £300 + £0 = £1300
Note: RCB is perfectly balanced—we profit regardless of outcome (we keep the spread).
The Balancing Problem
If we only accept BACK orders, we accumulate one-sided exposure. One bad result (all selections win) could cause massive losses.
The goal: Keep BACK and LAY liability roughly balanced on each selection so they offset.
Why balance matters:
| State | BACK Liability | LAY Liability | Net Exposure | Risk |
|---|---|---|---|---|
| Balanced | £50,000 | £45,000 | £5,000 | Low — positions offset |
| One-sided BACK | £80,000 | £20,000 | £60,000 | High — we're naked short |
| One-sided LAY | £15,000 | £70,000 | £55,000 | High — we're naked long |
The solution: Dynamic side acceptance
When one side is heavy, we actively seek the other side to offset:
| Pool State | BACK Liability | LAY Liability | Action |
|---|---|---|---|
| Balanced | £40,000 | £35,000 | Accept both sides freely |
| BACK-heavy | £70,000 | £30,000 | Restrict new BACK, encourage LAY |
| LAY-heavy | £25,000 | £65,000 | Restrict new LAY, encourage BACK |
| Critical | £85,000 | £20,000 | Stop BACK entirely, only accept LAY |
Each LAY order we accept reduces our net exposure (because it offsets existing BACK liability).
The Balancing Algorithm
Core principle: Orders that reduce our net exposure are always welcome. Orders that increase exposure are subject to limits.
When an order arrives, we calculate its impact on net exposure:
current_net = abs(back_liability - lay_liability)
new_net = abs(new_back - new_lay) # After accepting order
exposure_change = new_net - current_net
| exposure_change | Meaning | Action |
|---|---|---|
| Negative | Order reduces risk | Accept immediately — this helps us! |
| Zero | Perfect balance | Accept immediately |
| Positive | Order increases risk | Apply capacity limits |
When exposure increases, we check:
- Would total net exposure exceed £100K? → Limit acceptance
- Would this selection exceed per-selection limit? → Limit acceptance
- Would this market exceed per-market limit? → Limit acceptance
- Would this user exceed per-user limit? → Limit acceptance
Worked Example: The Power of Offsetting Orders
Starting state on Mumbai Win:
- BACK liability: £3,000
- LAY liability: £1,000
- Net exposure: £2,000 (we're short Mumbai — lose if Mumbai wins)
Order 1: User wants to BACK Mumbai for £1,000 at 2.00
This adds £1,000 to BACK liability:
- New BACK: £4,000
- New LAY: £1,000
- New net exposure: £3,000 (increased by £1,000!)
This order makes our position MORE risky. We apply limits — only accept if we have capacity.
Order 2: User wants to LAY Mumbai for £1,500 at 2.00
This adds £1,500 to LAY liability:
- New BACK: £3,000
- New LAY: £2,500
- New net exposure: £500 (reduced by £1,500!)
This order REDUCES our risk by £1,500. Accept immediately, no limits needed.
We actively WANT this order — it brings our book closer to balance.
What Happens at Pool Capacity
Scenario: Pool near £100K limit
| Selection | BACK | LAY | Net Exposure |
|---|---|---|---|
| Mumbai Win | £40,000 | £10,000 | £30,000 |
| CSK Win | £35,000 | £15,000 | £20,000 |
| RCB Win | £25,000 | £5,000 | £20,000 |
| Draw | £10,000 | £20,000 | £10,000 |
| Total | £80,000 |
Pool at 80% — room for £20,000 more net exposure.
Order arrives: BACK Mumbai £30,000
- Would add £30,000 to Mumbai BACK liability
- New Mumbai net: £60,000 (increased by £30,000)
- But we only have £20,000 capacity
- Accept £20,000, route £10,000 to Betfair
Order arrives: LAY Mumbai £25,000
- Would add £25,000 to Mumbai LAY liability
- New Mumbai net: £5,000 (REDUCED by £25,000!)
- This FREES UP capacity for other selections
- Accept full £25,000 — this is what we want!
Per-Selection and Per-Market Limits
The pool limit is the global cap, but we also need granular limits to prevent concentration risk.
| Limit Type | Value | Why |
|---|---|---|
| Per-Market | £20,000 | One match shouldn't dominate |
| Per-Selection | £5,000 | One outcome shouldn't dominate |
| Per-User | £1,000 | One user can't gamble us dry |
| Correlation Group | £30,000 | Related outcomes share a budget |
Correlation Groups Explained:
"Mumbai to Win" and "Mumbai to Win by 2+ Goals" are correlated. If Mumbai wins big, both hit. We track these together.
| Selection A | Selection B | Correlation | Shared Limit |
|---|---|---|---|
| Mumbai Win | Mumbai Win 2+ | High | £30,000 combined |
| Mumbai Win | Over 2.5 Goals | Medium | £40,000 combined |
| Mumbai Win | Chelsea Win | None | Independent limits |
What Happens at 100% Capacity
When the pool is full, B-Book closes entirely for the heavy side:
| Condition | BACK Orders | LAY Orders |
|---|---|---|
| BACK liability = £100K | Route to Betfair | Accept (helps balance) |
| LAY liability = £100K | Accept (helps balance) | Route to Betfair |
| Both near £100K | Route all to Betfair | Route all to Betfair |
The pool never exceeds £100K. This is a hard constraint, not a soft target.
Part 2: The Filter Engine — Decision Making
The Core Decision
For every order, the Filter Engine must answer: How should this order be split across venues?
An order can be filled across one or more destinations. The split depends on:
- Available internal liquidity
- User classification (sharp or not)
- B-Book capacity and admin-configured caps
- Betfair as the fallback
Routing Flow
┌─────────────────────────────────────────────────────────────┐
│ Order Arrives │
└─────────────────────────────┬───────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 1: Internal Orderbook (ALL users) │
│ Fill up to available internal liquidity │
│ Builds ecosystem liquidity, earns commission │
└─────────────────────────────┬───────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 2: Check Sharp List │
│ ├─ Sharp? → Skip B-Book, route remainder to Betfair │
│ └─ Not Sharp? → Continue to B-Book │
└─────────────────────────────┬───────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 3: B-Book (Recreational users only) │
│ Fill = MIN(remaining, capacity, admin caps) │
└─────────────────────────────┬───────────────────────────────┘
▼
┌─────────────────────────────────────────────────────────────┐
│ Step 4: Betfair (remainder) │
└─────────────────────────────────────────────────────────────┘
Key insight: Internal orderbook comes FIRST for everyone because:
- Builds platform liquidity (critical for growth)
- Diversifies risk (not everything in B-Book)
- Earns commission with zero risk
- Sharp users can trade with recreational users (we just facilitate)
Step 1: Internal Orderbook Fill
For every order, regardless of user type, we first check internal liquidity.
User A wants to BACK Mumbai at 2.50 for £1,000
| Orderbook State | Action |
|---|---|
| LAY order exists at 2.45 for £300 | Fill £300 internally, continue with £700 |
| LAY order exists at 2.60 for £500 | No match (price doesn't cross) |
| No LAY orders | Continue with full £1,000 |
Why internal first:
- Zero cost (no Betfair fees)
- Commission from both sides
- Instant execution
- Builds orderbook liquidity
Step 2: Sharp User Check
After internal fill, check if user is on the sharp list.
MVP Approach: Hardcoded Sharp List
sharp_list = {
"user_123": "Known arb bettor",
"user_456": "Professional syndicate member",
"user_789": "Flagged by ops - 80% win rate over 200 bets"
}
def is_sharp(user_id):
return user_id in sharp_list
| User Status | Remaining Amount Goes To |
|---|---|
| In sharp list | Betfair only (skip B-Book) |
| Not in sharp list | B-Book (subject to caps), then Betfair |
Managing the Sharp List
| Action | Who | When |
|---|---|---|
| Add user to list | Operations team | User identified as sharp |
| Remove from list | Operations team | False positive confirmed |
| Review list | Weekly | Ops reviews recent high-performers |
Step 3: B-Book Fill (Recreational Only)
For users NOT on the sharp list, calculate B-Book fill subject to capacity AND admin caps.
Admin-Configurable Caps
| Parameter | Description | Default | Rationale |
|---|---|---|---|
max_bbook_pct | Max % of any single order in B-Book | 70% | Force diversification |
max_bbook_per_selection | Max B-Book exposure per selection | £5,000 | Concentration limit |
max_bbook_per_market | Max B-Book exposure per market | £20,000 | Market-level limit |
max_bbook_per_user | Max B-Book exposure per user | £500 | User-level limit |
global_pool_limit | Total B-Book exposure | £100,000 | Hard cap |
All caps are configurable via admin interface. Operations can adjust based on:
- Current pool health
- Market volatility
- Risk appetite
B-Book Fill Calculation
remaining = order_size - internal_fill
# Calculate all applicable limits
limit_from_pct = order_size × max_bbook_pct
limit_from_selection = max_bbook_per_selection - current_selection_exposure
limit_from_market = max_bbook_per_market - current_market_exposure
limit_from_user = max_bbook_per_user - current_user_exposure
limit_from_pool = global_pool_limit - current_pool_exposure
# B-Book fill is minimum of all limits
bbook_fill = MIN(
remaining,
limit_from_pct,
limit_from_selection,
limit_from_market,
limit_from_user,
limit_from_pool
)
Step 4: Betfair (Remainder)
Whatever isn't filled internally or in B-Book goes to Betfair.
betfair_fill = order_size - internal_fill - bbook_fill
Complete Example
Order: £1,000 BACK Mumbai at 2.50 from recreational user
Current state:
- Internal orderbook has £200 LAY liquidity at 2.48
max_bbook_pct= 70%- Selection exposure = £3,000 (limit £5,000)
- User exposure = £300 (limit £500)
Calculation:
| Step | Calculation | Result |
|---|---|---|
| 1. Internal fill | Available liquidity | £200 |
| 2. Remaining | £1,000 - £200 | £800 |
| 3. Check sharp list | User not on list | Continue to B-Book |
| 4. Limit from pct | 70% × £1,000 | £700 |
| 5. Limit from selection | £5,000 - £3,000 | £2,000 |
| 6. Limit from user | £500 - £300 | £200 |
| 7. B-Book fill | MIN(£800, £700, £2,000, £200) | £200 (user limit is binding) |
| 8. Betfair fill | £800 - £200 | £600 |
Final Routing Plan:
- Internal: £200
- B-Book: £200
- Betfair: £600
Routing Output
The Filter Engine produces a Routing Plan:
| Field | Description |
|---|---|
| internal_fill | Amount matched on orderbook |
| bbook_fill | Amount taken into B-Book |
| betfair_fill | Amount to route to Betfair |
| limiting_factor | Which cap was binding (if any) |
Example Routing Plans:
| Scenario | Internal | B-Book | Betfair | Limiting Factor |
|---|---|---|---|---|
| Full internal match | £1,000 | £0 | £0 | Internal liquidity |
| Recreational, caps ok | £200 | £560 | £240 | max_bbook_pct (70%) |
| Recreational, user limit | £200 | £200 | £600 | max_bbook_per_user |
| Sharp user | £200 | £0 | £800 | Sharp list |
| No internal liquidity | £0 | £700 | £300 | max_bbook_pct |
Handling Edge Cases
Edge Case 1: Betfair is Down
If Betfair is unavailable, we cannot route externally. Options:
| Strategy | Pros | Cons |
|---|---|---|
| Reject order | Safe, no unbounded risk | Bad UX, lost revenue |
| Queue for later | Order eventually fills | Price may move, user waiting |
| Accept in B-Book anyway | Order fills immediately | Risk limits may be exceeded |
Our approach:
- If B-Book has capacity AND user is not sharp → Accept in B-Book
- Otherwise → Reject with "exchange unavailable" error
- Never exceed pool limits, even during outages
Edge Case 2: Price Moves During Routing
User requests BACK at 2.50. By the time we route to Betfair, price is 2.45.
Slippage rule: Accept if actual price is within 2% of requested price.
For BACK orders: Accept if actual_price >= requested_price × 0.98
For LAY orders: Accept if actual_price <= requested_price × 1.02
| Requested | Actual | Slippage | Within 2%? | Action |
|---|---|---|---|---|
| 2.50 | 2.55 | +2% (better) | N/A | Fill at better price |
| 2.50 | 2.50 | 0% | Yes | Fill as requested |
| 2.50 | 2.47 | -1.2% | Yes | Accept (within tolerance) |
| 2.50 | 2.45 | -2% | Boundary | Accept (exactly at limit) |
| 2.50 | 2.40 | -4% | No | Reject and requote |
Boundary is inclusive: 2% slippage is accepted, >2% is rejected.
Edge Case 3: Partial Fill Across Venues
Order: £1,000 BACK Mumbai at 2.50
- £200 fills on internal orderbook at 2.50
- £500 fills in B-Book at 2.50
- £300 sent to Betfair, only £250 fills at 2.48
Result:
- User gets £950 filled (£200 + £500 + £250)
- £50 remains unfilled
- Return unfilled portion to user with fill report
Edge Case 4: Race Condition on B-Book Capacity
Two orders arrive simultaneously. Each would fit individually, but not both.
| Order | Size | Available Before | Would Fit? |
|---|---|---|---|
| Order A | £3,000 | £5,000 | Yes |
| Order B | £4,000 | £5,000 | Yes |
| Both | £7,000 | £5,000 | No |
Our approach: Serialize B-Book decisions. First order to acquire the lock wins. Second order sees reduced capacity.
Part 3: Risk Engine — Real-Time Protection
What the Risk Engine Monitors
The Risk Engine runs continuously, not just on order arrival. It watches:
| Metric | Update Frequency | Purpose |
|---|---|---|
| Global pool liability | Every order | Hard limit enforcement |
| Side imbalance | Every order | Trigger balancing actions |
| Per-market liability | Every order | Concentration prevention |
| Per-selection liability | Every order | Granular risk control |
| Unrealized P&L | Every price update | Early warning |
| User classification scores | Hourly | Catch emerging sharps |
Automatic Actions
The Risk Engine can take actions without human intervention:
Action 1: Throttle B-Book Intake
| Trigger | Action |
|---|---|
| Pool > 60% | Log warning |
| Pool > 75% | Reduce acceptance rate by 50% |
| Pool > 90% | Accept only balancing orders |
| Pool = 100% | B-Book closed for heavy side |
Action 2: Side Restriction
| Imbalance Ratio | BACK Acceptance | LAY Acceptance |
|---|---|---|
| 1.0 - 1.5 | 100% | 100% |
| 1.5 - 2.0 | 75% for heavy side | 100% for light side |
| 2.0 - 3.0 | 50% for heavy side | 100% for light side |
| > 3.0 | 0% for heavy side | 100% for light side |
Action 3: Auto-Hedge
When exposure becomes dangerous, the system places hedging orders on Betfair:
Trigger: Single selection liability > 80% of limit
Example:
- Mumbai Win liability: £4,200 (limit £5,000)
- Trigger: £4,200 > £4,000 (80%)
- Action: Place £1,000 BACK on Mumbai at Betfair
- Result: If Mumbai wins, Betfair payout offsets our loss
Hedge sizing: Target post-hedge liability = 60% of limit
Action 4: Emergency Shutdown
| Condition | Action |
|---|---|
| Daily loss > £20,000 | Suspend B-Book, alert ops |
| Betfair connection lost > 5 min | Suspend external routing |
| Classification service down | Route all to Betfair |
| Database unreachable | Reject all orders |
The Settlement Process
When an event concludes:
| Step | Action |
|---|---|
| 1 | Receive result (Mumbai won/lost) |
| 2 | Calculate B-Book P&L for this event |
| 3 | Credit/debit user accounts |
| 4 | Update pool liability (remove settled bets) |
| 5 | Release capacity for new bets |
| 6 | Log outcome for ML training |
Post-settlement capacity release:
Before: Pool liability £80,000, Mumbai market £15,000 Result: Mumbai wins, we lose £8,000 After: Pool liability £65,000 (reset for Mumbai market)
The pool is now available for new exposure.
Part 4: System Guarantees
What We Guarantee
| Guarantee | Mechanism |
|---|---|
| Pool never exceeds £100K | Hard limit checked before every B-Book decision |
| Known sharps never B-Booked | Sharp list check on every order |
| All orders eventually resolve | Fallback to Betfair if B-Book unavailable |
| Audit trail for every order | Event sourcing, immutable log |
| Recovery after crash | Journal replay reconstructs state |
What We Don't Guarantee
| Non-Guarantee | Reason |
|---|---|
| Best price for user | We optimize for our margin, not user price |
| Immediate fill | May queue if Betfair slow |
| B-Book access for all users | Listed sharps are excluded by design |
| Catching all sharps | Manual list only; unknown sharps may slip through (pool limits cap damage) |
Summary
Filter is an intelligent order routing system that:
- Maximizes internal matching to eliminate exchange costs
- Selectively B-Books flow from users not on the sharp list
- Dynamically balances the book to prevent one-sided exposure
- Enforces hard limits that can never be exceeded
- Auto-hedges when positions become dangerous
The pool limits and per-user caps provide a safety net while operating with manual sharp identification.
Future enhancement (v2): ML-based user classification to automatically detect sharps. See Appendix A.
Appendix A: ML-Based User Classification (Future v2)
This section describes the planned ML system for automatic sharp detection. Not included in v1.
Why ML Will Beat the Manual List
Current approach (v1):
- Manual list of known sharps
- Ops team monitors and adds users
- Unknown sharps slip through until detected
ML approach (v2):
- Considers 50+ signals simultaneously
- Finds non-obvious correlations
- Catches sharps before they accumulate wins
- Adapts as sharps change behavior
Classification Signals
| Signal | Sharp Indicator | Recreational Indicator |
|---|---|---|
| Win rate (last 500 bets) | > 53% | < 48% |
| Closing Line Value | Positive (beats closing odds) | Negative |
| Stake pattern | Precise amounts (£47.23) | Round numbers (£50) |
| Timing | Bets close to event start | Bets placed early |
| Market selection | Niche markets, low liquidity | Popular markets |
Proposed Model
Model: Gradient Boosted Trees (XGBoost)
Why not deep learning?
- Tabular data, not images/text
- Interpretability required (why was user flagged?)
- Faster inference (<5ms)
Training approach:
- Dataset: 6 months of historical bets with outcomes
- Labels: Did user have positive ROI over next 3 months?
- Positive class: Sharp (ROI > 5%)
- Negative class: Recreational (ROI < -5%)
Output: Score from 0.0 (recreational) to 1.0 (sharp)
Classification Tiers (Proposed)
| Tier | Criteria | B-Book Eligible? |
|---|---|---|
| Sharp | Score > 0.7 | No |
| Suspicious | Score 0.5 - 0.7 | Limited |
| Regular | Score 0.3 - 0.5 | Yes |
| Recreational | Score < 0.3 | Yes, preferred |
Implementation Prerequisites
Before implementing ML classification:
- Data collection — Need 6+ months of bet history with outcomes
- Feature pipeline — Real-time feature calculation infrastructure
- Model serving — Low-latency inference (<5ms per order)
- Monitoring — Track model drift and classification distribution
Until these prerequisites are met, the manual sharp list is sufficient given the pool limits provide a safety net.
Appendix B: ML-Based Routing Optimizer (Future v3)
This section describes the planned ML system for optimal order routing splits. Not included in v1 or v2.
The Problem
In v1, routing splits are determined by simple rules and admin-configured caps:
- Internal first (up to available liquidity)
- B-Book next (up to caps)
- Betfair last (remainder)
This is suboptimal because:
- Fixed caps don't adapt to market conditions
- No learning from historical profitability
- Ignores correlations between orders
The ML Opportunity
An ML model could learn the optimal split for each order based on:
Inputs:
| Feature | Description |
|---|---|
| User classification score | How likely is this user to be sharp? |
| Current pool state | Exposure levels, balance ratios |
| Selection characteristics | Sport, league, time to start, liquidity |
| Market conditions | Betfair liquidity depth, price volatility |
| Historical profitability | Past P&L on similar orders |
| Time of day | Betting patterns vary by time |
Output:
{
"recommended_internal_pct": 0.20,
"recommended_bbook_pct": 0.50,
"recommended_betfair_pct": 0.30,
"confidence": 0.85
}
Objective Function
The model optimizes for expected profit subject to risk constraints:
Maximize: E[Profit]
Subject to:
- Pool exposure ≤ limit
- Per-selection exposure ≤ limit
- Variance of returns ≤ risk tolerance
Where profit includes:
- B-Book edge (expected win rate on recreational flow)
- Commission from internal matching
- Minus Betfair fees
Training Approach
Reinforcement Learning or Contextual Bandits:
- Each order is a decision point
- Action = routing split percentages
- Reward = realized P&L after settlement
Challenges:
- Delayed rewards (bets settle hours/days later)
- Counterfactual problem (we don't know what would have happened with different split)
- Non-stationary environment (market conditions change)
Implementation Prerequisites
Before implementing routing optimization:
- v1 and v2 running — Need baseline data on current routing performance
- Outcome tracking — Per-order P&L attribution by venue
- Simulation environment — Backtest routing strategies
- Guardrails — Model suggestions bounded by hard limits
Appendix C: Open Questions
Questions requiring decisions before or during implementation.
Routing Logic
| # | Question | Options | Impact |
|---|---|---|---|
| 1 | What should max_bbook_pct default to? | 50%, 70%, 80%? | Higher = more profit potential, more risk |
| 2 | Should we have a min_internal_pct to force orderbook liquidity? | 0%, 10%, 20%? | Higher = better liquidity, less B-Book revenue |
| 3 | If internal liquidity exists at worse price, do we still fill? | Yes (liquidity priority) / No (price priority) | Affects orderbook incentives |
| 4 | Should B-Book caps be per-order or per-time-window? | Per order / Per hour / Per day | Affects how fast pool fills |
Sharp User Management
| # | Question | Options | Impact |
|---|---|---|---|
| 5 | How do we seed the initial sharp list? | Empty / Known names / Conservative guess | Empty = risk, too many = lost revenue |
| 6 | Should sharp users be blocked from internal matching? | Yes / No | No = they can still trade with other users |
| 7 | Can a user be removed from the sharp list? | Never / After N losing months / Manual review | Affects false positive recovery |
| 8 | Should we have a "suspicious" tier with reduced limits? | Yes / No | More granular control vs complexity |
B-Book Operations
| # | Question | Options | Impact |
|---|---|---|---|
| 9 | When pool hits 90%, should we proactively hedge? | Yes (auto) / Yes (manual) / No | Auto = safer, manual = more control |
| 10 | What imbalance ratio triggers side restrictions? | 2:1, 3:1, 4:1? | Lower = more conservative |
| 11 | Should we offer better odds to attract the "light" side? | Yes / No | Could help balance but adds complexity |
| 12 | How do we handle correlated selections (e.g., Team A win + Team B lose)? | Track correlation / Ignore for MVP | Ignoring = hidden concentration risk |
Internal Orderbook
| # | Question | Options | Impact |
|---|---|---|---|
| 13 | Do we show our B-Book liquidity on the orderbook? | Yes / No | Yes = more apparent liquidity, but exposes our position |
| 14 | Should internal orders have priority over B-Book at same price? | Yes / No | Yes = fairer to users, No = more B-Book revenue |
| 15 | What's the minimum order size? | £1, £5, £10? | Lower = more orders, higher = less noise |
Betfair Integration
| # | Question | Options | Impact |
|---|---|---|---|
| 16 | What slippage tolerance for Betfair fills? | 1%, 2%, 5%? | Lower = more rejections, higher = worse fills |
| 17 | If Betfair is slow, do we queue or reject? | Queue (with timeout) / Reject | Queue = better UX, reject = faster feedback |
| 18 | Should we use Betfair streaming or polling? | Streaming (v2) / Polling (v1) | Streaming = faster but complex |
Settlement & Risk
| # | Question | Options | Impact |
|---|---|---|---|
| 19 | How do we handle disputed/voided bets? | Full refund / Proportional / Manual review | Affects P&L and user trust |
| 20 | What triggers an emergency pool shutdown? | 95% capacity / Betfair down + 80% / Manual only | Auto = safer, manual = more control |
| 21 | Should we pause B-Book during high-volatility events? | Yes / No | Yes = conservative, No = more opportunity |
Data & Monitoring
| # | Question | Options | Impact |
|---|---|---|---|
| 22 | What metrics trigger an alert? | Pool %, imbalance, P&L drawdown? | Defines operational visibility |
| 23 | How long do we retain order history? | 1 year / 3 years / Forever | Compliance vs storage cost |
| 24 | Should we log every routing decision for audit? | Yes / Sampled / No | Yes = full audit trail, storage cost |
These questions should be resolved with stakeholders before finalizing implementation.