Forsyt Orderbook Integration Guide
This guide covers how to integrate with the Forsyt Universal Orderbook from your TypeScript backend.
Table of Contents
- Quick Start
- Server Modes
- Monitor Dashboard
- WebSocket Event Subscription
- Core Concepts: BACK vs LAY
- Decimal Odds
- Market Types (including Asian Handicaps)
- Synthetic Matching
- Order Types: LIMIT and MARKET
- TypeScript gRPC Integration
- gRPC Event Streaming
- Best Practices
- Resetting the Orderbook
- Known Issues
Quick Start
Starting the Server
# Production mode (monitoring only)
cargo run --bin server
# Test mode (full test UI + stress testing)
cargo run --bin server -- --test
The server exposes:
- gRPC API on port
50051(configurable with--grpc-port) - HTTP/WebSocket on port
3000(configurable with--http-port)
Minimal TypeScript Example
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
const packageDefinition = protoLoader.loadSync('proto/orderbook.proto');
const proto = grpc.loadPackageDefinition(packageDefinition) as any;
const client = new proto.orderbook.v1.OrderBookService(
'localhost:50051',
grpc.credentials.createInsecure()
);
// 1. Create a market
client.CreateMarket({
market_id: 'match_123',
outcomes: ['Home', 'Away']
}, callback);
// 2. Open the market
client.OpenMarket({ market_id: 'match_123' }, callback);
// 3. Submit a BACK order at 2.50 decimal odds
client.SubmitOrder({
market_id: 'match_123',
outcome_id: 'Home',
side: 'BACK',
price: 2.50,
price_type: 1, // DECIMAL_ODDS
quantity: 100,
order_type: 'LIMIT'
}, (err: Error | null, response: { order_id: number }) => {
console.log('Order ID:', response.order_id);
});
Server Modes
The orderbook server supports two operational modes:
Production Mode (Default)
cargo run --bin server
Enabled Features:
- ✅ gRPC API for order submission, cancellation, market management
- ✅ Live Monitor Dashboard at
http://localhost:3000/ - ✅ WebSocket event streaming at
ws://localhost:3000/ws - ✅ Read-only orderbook API endpoints
- ❌ Test UI (disabled)
- ❌ Order submission via HTTP (disabled - use gRPC)
- ❌ Stress testing (disabled)
Test Mode
cargo run --bin server -- --test
Enabled Features (everything in Production plus):
- ✅ Full Test UI at
http://localhost:3000/test - ✅ HTTP endpoints for order submission (
POST /api/orders) - ✅ Market management via HTTP (
POST /api/markets) - ✅ Scenario runner for predefined test cases
- ✅ Stress testing controls (
/api/stress/*) - ✅ Reset functionality (
POST /api/reset)
Command Line Options
| Option | Default | Description |
|---|---|---|
--test | false | Enable test mode with full Test UI |
--grpc-port | 50051 | Port for gRPC API |
--http-port | 3000 | Port for HTTP/WebSocket server |
# Custom ports example
cargo run --bin server -- --test --grpc-port 50052 --http-port 8080
Monitor Dashboard
The Monitor Dashboard provides real-time visibility into orderbook activity.
Accessing the Dashboard
- URL:
http://localhost:3000/(available in both Production and Test modes) - Test UI Link: When in test mode, a banner appears linking to
/test
Dashboard Features
Main Dashboard Tab
| Component | Description |
|---|---|
| Total Orders | Running count of all orders submitted |
| Matches | Number of trades executed |
| Volume | Total matched quantity |
| Resting Orders | Orders waiting on the book |
| Active Markets | Markets currently open for trading |
| Live Order Flow | Real-time table of incoming orders with status |
| Match Feed | Real-time trade executions with synthetic match details |
| Throughput Chart | Orders per second graph |
| Market Depth | Aggregated bid/ask visualization |
| Performance Metrics | Orders/sec, Matches/sec, Fill Rate |
Event Log Tab
| Feature | Description |
|---|---|
| All Events | Complete chronological log |
| Filters | Filter by Orders, Matches, Info, Errors |
| Export JSON | Download events as JSON |
| Export CSV | Download events as CSV |
| Copy All | Copy events to clipboard |
Dashboard WebSocket Connection
The dashboard connects automatically to ws://localhost:3000/ws and displays:
- Connection status (Connected/Disconnected)
- Server uptime
- Events per second rate
WebSocket Event Subscription
The Forsyt frontend can subscribe to real-time orderbook events via WebSocket to keep the UI updated.
Connecting to WebSocket
// TypeScript/JavaScript WebSocket client
const ws = new WebSocket('ws://localhost:3000/ws');
ws.onopen = () => {
console.log('Connected to orderbook');
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
handleOrderbookEvent(data);
};
ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
ws.onclose = () => {
console.log('Disconnected from orderbook');
// Implement reconnection logic
};
Event Types
The WebSocket sends JSON events with a type field. All events use snake_case naming.
1. Order Submission Event (order_submit)
Fired when a new order is submitted to the orderbook.
{
"type": "order_submit",
"order_id": 42,
"market_id": "match_123",
"outcome_id": "Home",
"side": "back",
"price": 400000,
"size": 10000,
"user_id": "user_abc",
"expiry_ms": 1703808000000
}
| Field | Type | Description |
|---|---|---|
order_id | number | Unique order ID assigned by engine |
market_id | string | Market identifier |
outcome_id | string | Outcome being bet on |
side | string | "back" or "lay" |
price | number | Price in basis points (400000 = 40% = 2.50 odds) |
size | number | Order size in smallest currency unit |
user_id | string | User who placed the order |
expiry_ms | number? | Optional. Expiry timestamp in milliseconds since epoch |
2. Match Event (match)
Fired when two orders are matched (trade executed).
{
"type": "match",
"sequence": 15,
"market_id": "match_123",
"outcome_id": "Home",
"maker_order_id": 38,
"taker_order_id": 42,
"maker_side": "Long",
"taker_side": "Short",
"price": 400000,
"quantity": 5000,
"timestamp": 1703807940000000000,
"is_synthetic": false,
"is_cross_match": false
}
| Field | Type | Description |
|---|---|---|
sequence | number | Global sequence number for ordering |
market_id | string | Market identifier |
outcome_id | string | Outcome that was matched |
maker_order_id | number | The resting (maker) order ID |
taker_order_id | number | The incoming (taker) order ID |
maker_side | string | "Long" (Back) or "Short" (Lay) |
taker_side | string | "Long" (Back) or "Short" (Lay) |
price | number | Execution price in basis points |
quantity | number | Matched quantity |
timestamp | number | Unix timestamp in nanoseconds |
is_synthetic | boolean | True if multi-outcome synthetic match (e.g., Yes/No market) |
is_cross_match | boolean | True if cross-line match (e.g., Team1 -0.5 ↔ Team2 +0.5) |
3. Order Filled Event (order_filled)
Fired when an order is completely filled (remaining quantity = 0).
{
"type": "order_filled",
"order_id": 42,
"market_id": "match_123",
"outcome_id": "Home"
}
| Field | Type | Description |
|---|---|---|
order_id | number | The order that was fully filled |
market_id | string | Market identifier |
outcome_id | string | Outcome identifier |
4. Order Expired Event (order_expired)
Fired when an order expires due to its TTL (time-to-live) being reached.
{
"type": "order_expired",
"order_id": 42,
"market_id": "match_123",
"outcome_id": "Home"
}
| Field | Type | Description |
|---|---|---|
order_id | number | The order that expired |
market_id | string | Market identifier |
outcome_id | string | Outcome identifier |
5. Order Cancelled Event (order_cancelled)
Fired when an order is cancelled (by user request or system).
{
"type": "order_cancelled",
"order_id": 42,
"market_id": "match_123",
"outcome_id": "Home"
}
| Field | Type | Description |
|---|---|---|
order_id | number | The order that was cancelled |
market_id | string | Market identifier |
outcome_id | string | Outcome identifier |
6. Market Created Event (market_created)
Fired when a new market is created.
{
"type": "market_created",
"market_id": "match_123",
"outcomes": ["Home", "Draw", "Away"],
"synthetic_enabled": true
}
| Field | Type | Description |
|---|---|---|
market_id | string | Market identifier |
outcomes | string[] | List of outcome names |
synthetic_enabled | boolean | Whether synthetic matching is enabled |
Complete Integration Example
interface OrderSubmitEvent {
type: 'order_submit';
order_id: number;
market_id: string;
outcome_id: string;
side: 'back' | 'lay';
price: number;
size: number;
user_id: string;
expiry_ms?: number;
}
interface MatchEvent {
type: 'match';
sequence: number;
market_id: string;
outcome_id: string;
maker_order_id: number;
taker_order_id: number;
maker_side: 'Long' | 'Short';
taker_side: 'Long' | 'Short';
price: number;
quantity: number;
timestamp: number;
is_synthetic: boolean;
is_cross_match: boolean;
}
interface OrderFilledEvent {
type: 'order_filled';
order_id: number;
market_id: string;
outcome_id: string;
}
interface OrderExpiredEvent {
type: 'order_expired';
order_id: number;
market_id: string;
outcome_id: string;
}
interface OrderCancelledEvent {
type: 'order_cancelled';
order_id: number;
market_id: string;
outcome_id: string;
}
interface MarketCreatedEvent {
type: 'market_created';
market_id: string;
outcomes: string[];
synthetic_enabled: boolean;
}
type OrderbookEvent =
| OrderSubmitEvent
| MatchEvent
| OrderFilledEvent
| OrderExpiredEvent
| OrderCancelledEvent
| MarketCreatedEvent;
class OrderbookWebSocket {
private ws: WebSocket | null = null;
private reconnectInterval = 3000;
constructor(
private url: string = 'ws://localhost:3000/ws',
private onEvent: (event: OrderbookEvent) => void
) {}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('✅ Connected to orderbook WebSocket');
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data) as OrderbookEvent;
this.onEvent(data);
} catch (e) {
console.error('Failed to parse event:', e);
}
};
this.ws.onclose = () => {
console.log('❌ Disconnected, reconnecting...');
setTimeout(() => this.connect(), this.reconnectInterval);
};
this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}
disconnect() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}
// Usage in Forsyt frontend
const orderbookWs = new OrderbookWebSocket('ws://localhost:3000/ws', (event) => {
switch (event.type) {
case 'order_submit':
// Update order list in UI, track for expiry
updateOrderList(event);
if (event.expiry_ms) {
trackOrderExpiry(event.order_id, event.expiry_ms);
}
break;
case 'match':
// Show trade notification, update positions
showTradeNotification(event);
updateUserPositions(event);
break;
case 'order_filled':
// Remove from active orders, update UI
removeFromActiveOrders(event.order_id);
break;
case 'order_expired':
// Show expiry notification, update UI
showExpiryNotification(event.order_id);
removeFromActiveOrders(event.order_id);
break;
case 'order_cancelled':
// Remove from active orders
removeFromActiveOrders(event.order_id);
break;
case 'market_created':
// Add market to available markets list
addMarket(event.market_id, event.outcomes);
break;
}
});
orderbookWs.connect();
Converting Prices for Display
// Basis points to decimal odds
function bpToDecimalOdds(bp: number): number {
return 1_000_000 / bp;
}
// Example: 400000 BP → 2.50 decimal odds
const displayOdds = bpToDecimalOdds(400000); // 2.50
// Decimal odds to basis points
function decimalOddsToBp(odds: number): number {
return Math.round(1_000_000 / odds);
}
Core Concepts: BACK vs LAY
In a betting exchange, every bet has two sides:
| Side | Description | What You Are Saying |
|---|---|---|
| BACK | Betting FOR an outcome | "I think Team A will win" |
| LAY | Betting AGAINST an outcome | "I think Team A will NOT win" |
How Matching Works
A BACK order matches with a LAY order at the same or better odds:
Punter A: BACK Team A @ 2.50 (bets £100 to win £150 profit)
Punter B: LAY Team A @ 2.50 (accepts £150 liability to win £100)
→ Match! Both filled.
Liability Calculation
| Side | Stake | Odds | Win Profit | Lose Amount |
|---|---|---|---|---|
| BACK | £100 | 2.50 | £150 | £100 |
| LAY | £100 | 2.50 | £100 | £150 (liability) |
LAY Liability Formula: Stake × (Odds - 1)
At 2.50 odds: £100 × 1.50 = £150 liability
Decimal Odds
The orderbook uses decimal odds (European format) as the standard representation.
Understanding Decimal Odds
| Decimal Odds | Implied Probability | Meaning |
|---|---|---|
| 1.50 | 66.7% | Strong favourite |
| 2.00 | 50.0% | Even chance |
| 2.50 | 40.0% | Slight underdog |
| 3.00 | 33.3% | Underdog |
| 5.00 | 20.0% | Long shot |
| 10.00 | 10.0% | Very unlikely |
Conversion Formulas
// Decimal odds to probability
const probability = 1 / decimalOdds;
// Example: 2.50 → 1/2.50 = 0.40 (40%)
// Probability to decimal odds
const decimalOdds = 1 / probability;
// Example: 0.40 → 1/0.40 = 2.50
Internal BasisPoints Representation
The engine stores prices internally as BasisPoints where 1.0 probability = 1,000,000 BP:
// Decimal odds to BasisPoints
const basisPoints = Math.round((1 / decimalOdds) * 1_000_000);
// Examples:
// 2.50 → 400,000 BP (40%)
// 2.00 → 500,000 BP (50%)
// 1.50 → 666,667 BP (66.67%)
Note: When using price_type: 1 (DECIMAL_ODDS), this conversion is done automatically by the engine.
Market Types
Binary Market (Two Outcomes)
The simplest market with two mutually exclusive outcomes:
// Create a binary market
client.CreateMarket({
market_id: 'match_winner',
outcomes: ['Team A', 'Team B']
}, callback);
1X2 Market (Three Outcomes)
Standard football/soccer match result market:
// Create 1X2 market with synthetic matching enabled
client.CreateMarket({
market_id: 'match_result',
outcomes: ['Home', 'Draw', 'Away']
}, callback);
Multi-Runner Market (Horse Racing, Elections)
Markets with many mutually exclusive outcomes:
// Create a horse race market
const runners = ['Horse 1', 'Horse 2', 'Horse 3', 'Horse 4', 'Horse 5'];
client.CreateMarket({
market_id: 'race_winner',
outcomes: runners
}, callback);
Asian Handicap Markets
Asian Handicaps give one team a goal advantage/disadvantage to level the playing field.
Line Types
| Line Type | Examples | Settlement Outcomes |
|---|---|---|
| Half-Goal | -0.5, -1.5, +0.5, +1.5 | WIN or LOSE only |
| Whole-Goal | 0, -1, -2, +1, +2 | WIN, LOSE, or PUSH |
| Quarter-Goal | -0.25, -0.75, +1.25 | WIN, LOSE, HALF-WIN, or HALF-LOSE |
How Quarter-Goal Lines Work
Quarter-goal lines (e.g., -0.25) are automatically split into two half-stakes on adjacent lines:
BACK Team A -0.25 @ 1.90 for £100 becomes:
├── £50 on Team A -0.0 @ 1.90
└── £50 on Team A -0.5 @ 1.90
This is handled automatically by the engine.
Creating Asian Handicap Markets
// Create Asian Handicap market with standard lines
client.CreateMarket({
market_id: 'match_ah',
outcomes: [
'Home_0.0', // Whole goal (Draw No Bet)
'Home_-0.5', // Half goal
'Home_-1.0', // Whole goal
'Home_-1.5', // Half goal
'Home_-0.25', // Quarter (splits to 0.0 and -0.5)
'Home_-0.75', // Quarter (splits to -0.5 and -1.0)
]
}, callback);
Outcome Naming Convention
Use the format: Team_Handicap where handicap is the numeric value:
| Outcome ID | Meaning |
|---|---|
Home_-0.5 | Home team with -0.5 handicap |
Away_+0.5 | Away team with +0.5 handicap |
Home_-1.25 | Home team with -1.25 handicap (quarter line) |
Placing Asian Handicap Bets
// BACK Home -0.5 at 1.90 odds
client.SubmitOrder({
market_id: 'match_ah',
outcome_id: 'Home_-0.5',
side: 'BACK',
price: 1.90,
price_type: 1,
quantity: 10000, // £100
order_type: 'LIMIT',
user_id: 'user_123'
}, callback);
// BACK a quarter line (automatically splits)
client.SubmitOrder({
market_id: 'match_ah',
outcome_id: 'Home_-0.25', // Splits into -0.0 and -0.5
side: 'BACK',
price: 1.90,
price_type: 1,
quantity: 10000, // £50 on each leg
order_type: 'LIMIT',
user_id: 'user_123'
}, callback);
Settlement Outcomes
| Score | Home -0.5 | Home -1.0 | Home -0.25 |
|---|---|---|---|
| Home 2-0 | WIN | WIN | WIN |
| Home 1-0 | WIN | PUSH | WIN (full) |
| Draw 0-0 | LOSE | LOSE | HALF-LOSE |
| Away 1-0 | LOSE | LOSE | LOSE |
Market Lifecycle
Markets progress through these states:
Created → Open → Suspended → Closed → Settled
// Open market (required before accepting bets)
client.OpenMarket({ market_id: 'match_123' }, callback);
// Suspend during dangerous play or half-time
client.SuspendMarket({ market_id: 'match_123' }, callback);
// Resume trading
client.OpenMarket({ market_id: 'match_123' }, callback);
// Close for settlement
client.CloseMarket({ market_id: 'match_123' }, callback);
Orders can only be submitted when the market is in the Open state.
Synthetic Matching
Synthetic matching creates implied liquidity by recognising that outcomes in a market are related.
How It Works
In a 1X2 market (Home/Draw/Away), these bets are mathematically equivalent:
- BACK Home = LAY Draw + LAY Away combined
- LAY Home = BACK Draw + BACK Away combined
Example
Market: Liverpool vs Arsenal (1X2)
Existing orders:
- LAY Draw @ 3.50 (£100)
- LAY Away @ 4.00 (£100)
Incoming order:
- BACK Home @ 2.00 (£100)
Without synthetic matching: No match (no direct BACK/LAY pair)
With synthetic matching: Engine creates implied LAY Home from the two LAY orders
When Synthetic Matching Is Used
- 1X2 markets (football/soccer match result)
- Multi-runner markets (horse racing, elections)
For binary markets (2 outcomes), synthetic matching is simpler:
- BACK Outcome A @ 2.00 = LAY Outcome B @ 2.00
Order Types: LIMIT and MARKET
The orderbook supports two order types:
LIMIT Orders
A LIMIT order specifies both quantity and price. It will:
- Match immediately against existing orders at the same or better price
- Place any remaining quantity on the book at the specified price
// BACK at 2.50 or better odds
client.SubmitOrder({
market_id: 'match_123',
outcome_id: 'Home',
side: 'BACK',
price: 2.50, // Will match at 2.50 or higher
price_type: 1, // DECIMAL_ODDS
quantity: 100,
order_type: 'LIMIT',
user_id: 'user_abc123' // For self-trade prevention
}, callback);
MARKET Orders
A MARKET order matches at the best available price until filled or liquidity exhausted:
// BACK at whatever price is available
client.SubmitOrder({
market_id: 'match_123',
outcome_id: 'Home',
side: 'BACK',
quantity: 100,
order_type: 'MARKET',
user_id: 'user_abc123'
}, callback);
Warning: MARKET orders may execute at unfavourable prices in thin markets.
Order Parameters Reference
| Field | Type | Required | Description |
|---|---|---|---|
market_id | string | Yes | Unique market identifier |
outcome_id | string | Yes | Which outcome to bet on |
side | string | Yes | "BACK" or "LAY" |
price | number | LIMIT only | Decimal odds (e.g., 2.50) |
price_type | number | LIMIT only | 1 for DECIMAL_ODDS |
quantity | number | Yes | Stake in smallest currency unit |
order_type | string | Yes | "LIMIT" or "MARKET" |
user_id | string | Optional | For self-trade prevention |
client_order_id | string | Optional | Your idempotency key |
Self-Trade Prevention
When user_id is provided, the engine prevents matching against the same user's orders:
// User's BACK order
client.SubmitOrder({
market_id: 'match_123',
outcome_id: 'Home',
side: 'BACK',
price: 2.50,
price_type: 1,
quantity: 100,
order_type: 'LIMIT',
user_id: 'user_abc123' // Same user
}, callback);
// User's LAY order - will NOT match their own BACK
client.SubmitOrder({
market_id: 'match_123',
outcome_id: 'Home',
side: 'LAY',
price: 2.50,
price_type: 1,
quantity: 100,
order_type: 'LIMIT',
user_id: 'user_abc123' // Same user - prevented from self-trade
}, callback);
TypeScript gRPC Integration
Setup with @grpc/grpc-js
npm install @grpc/grpc-js @grpc/proto-loader
Complete Client Implementation
import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { promisify } from 'util';
// Load proto
const PROTO_PATH = './proto/orderbook.proto';
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const proto = grpc.loadPackageDefinition(packageDefinition) as any;
class OrderbookClient {
private client: any;
constructor(address: string = 'localhost:50051') {
this.client = new proto.orderbook.v1.OrderBookService(
address,
grpc.credentials.createInsecure()
);
}
async createMarket(marketId: string, outcomes: string[]): Promise<void> {
return new Promise((resolve, reject) => {
this.client.CreateMarket({ market_id: marketId, outcomes },
(err: Error | null) => err ? reject(err) : resolve());
});
}
async openMarket(marketId: string): Promise<void> {
return new Promise((resolve, reject) => {
this.client.OpenMarket({ market_id: marketId },
(err: Error | null) => err ? reject(err) : resolve());
});
}
async submitBackOrder(
marketId: string,
outcomeId: string,
odds: number,
stake: number,
userId?: string
): Promise<{ order_id: number; status: string }> {
return new Promise((resolve, reject) => {
this.client.SubmitOrder({
market_id: marketId,
outcome_id: outcomeId,
side: 'BACK',
price: odds,
price_type: 1, // DECIMAL_ODDS
quantity: stake,
order_type: 'LIMIT',
user_id: userId
}, (err: Error | null, response: any) => {
if (err) reject(err);
else resolve(response);
});
});
}
async submitLayOrder(
marketId: string,
outcomeId: string,
odds: number,
stake: number,
userId?: string
): Promise<{ order_id: number; status: string }> {
return new Promise((resolve, reject) => {
this.client.SubmitOrder({
market_id: marketId,
outcome_id: outcomeId,
side: 'LAY',
price: odds,
price_type: 1,
quantity: stake,
order_type: 'LIMIT',
user_id: userId
}, (err: Error | null, response: any) => {
if (err) reject(err);
else resolve(response);
});
});
}
async cancelOrder(marketId: string, orderId: number): Promise<void> {
return new Promise((resolve, reject) => {
this.client.CancelOrder({ market_id: marketId, order_id: orderId },
(err: Error | null) => err ? reject(err) : resolve());
});
}
async getOrderBook(marketId: string, outcomeId: string): Promise<{
bids: Array<{ price: number; quantity: number; order_count: number }>;
asks: Array<{ price: number; quantity: number; order_count: number }>;
}> {
return new Promise((resolve, reject) => {
this.client.GetOrderBook({ market_id: marketId, outcome_id: outcomeId },
(err: Error | null, response: any) => {
if (err) reject(err);
else resolve(response);
});
});
}
}
// Usage
async function main() {
const client = new OrderbookClient();
// Create and open market
await client.createMarket('pl_match_1', ['Liverpool', 'Draw', 'Man City']);
await client.openMarket('pl_match_1');
// Place a BACK bet on Liverpool at 2.50 odds, £100 stake
const result = await client.submitBackOrder(
'pl_match_1',
'Liverpool',
2.50,
10000, // £100.00 in pence
'user_123'
);
console.log('Order placed:', result.order_id);
// Check the order book
const book = await client.getOrderBook('pl_match_1', 'Liverpool');
console.log('Best BACK odds available:', book.asks[0]?.price);
console.log('Best LAY odds available:', book.bids[0]?.price);
}
gRPC Event Streaming
In addition to WebSocket events (see WebSocket Event Subscription), you can subscribe to real-time market events using gRPC server streaming:
// Subscribe to market events
const stream = client.SubscribeMarket({ market_id: 'pl_match_1' });
stream.on('data', (update: any) => {
if (update.match_event) {
const match = update.match_event;
console.log(`Trade: ${match.quantity} @ ${match.price} BP`);
console.log(` Maker Order: ${match.maker_order_id}`);
console.log(` Taker Order: ${match.taker_order_id}`);
}
if (update.order_update) {
const order = update.order_update;
console.log(`Order ${order.order_id}: ${order.status}`);
}
if (update.book_update) {
const book = update.book_update;
console.log(`Book updated: ${book.outcome_id}`);
}
});
stream.on('error', (err: Error) => {
console.error('Stream error:', err);
});
stream.on('end', () => {
console.log('Stream ended');
});
Event Types
| Event | Description |
|---|---|
match_event | A trade occurred between two orders |
order_update | Order status changed (filled, partially filled, cancelled) |
book_update | Order book levels changed |
Recovery & Journaling
The engine uses event sourcing for durability. All operations are journaled before acknowledgment.
What Gets Journaled
- Market creation and lifecycle changes
- All order submissions
- All cancellations
- Match events
Recovery Process
On restart, the engine:
- Reads the journal from
data/journal.log - Replays all events in order
- Reconstructs exact state before the shutdown
No data is lost even on unexpected shutdown.
Best Practices
1. Always Use Decimal Odds
Set price_type: 1 for all orders to use human-readable decimal odds:
{
price: 2.50,
price_type: 1 // DECIMAL_ODDS
}
2. Include User ID for Self-Trade Prevention
Always pass a user_id to prevent users from accidentally matching against themselves:
{
user_id: 'unique_user_identifier'
}
3. Use Client Order IDs for Idempotency
Pass a unique client_order_id to safely retry failed requests:
{
client_order_id: 'uuid-v4-string'
}
4. Handle Market States
Always check market state before submitting orders:
// Orders will be rejected if market is not OPEN
try {
await client.submitBackOrder(...);
} catch (err: any) {
if (err.code === grpc.status.FAILED_PRECONDITION) {
console.log('Market is not open for trading');
}
}
5. Monitor the Order Book
Before placing large orders, check available liquidity:
const book = await client.getOrderBook(marketId, outcomeId);
const totalBackLiquidity = book.asks.reduce((sum, level) =>
sum + level.quantity, 0);
console.log('Available to BACK:', totalBackLiquidity);
6. Use LIMIT Orders for Better Control
Prefer LIMIT orders to avoid slippage:
// Good: LIMIT order with explicit price
{ order_type: 'LIMIT', price: 2.50 }
// Risky: MARKET order may fill at any price
{ order_type: 'MARKET' }
Troubleshooting
Common Errors
| Error Code | Meaning | Solution |
|---|---|---|
FAILED_PRECONDITION | Market not open | Call OpenMarket first |
NOT_FOUND | Market or outcome doesn't exist | Check IDs |
INVALID_ARGUMENT | Bad price, quantity, or side | Validate inputs |
Checking Connectivity
// Simple health check
client.GetOrderBook({
market_id: 'test',
outcome_id: 'test'
}, (err: Error | null) => {
if (err && err.message.includes('NOT_FOUND')) {
console.log('Connected successfully (market just does not exist)');
} else if (err) {
console.log('Connection failed:', err.message);
}
});
Resetting the Orderbook
To start with a completely fresh orderbook (clear all markets, orders, and journal):
Option 1: API Reset (Recommended for Development)
The engine provides a Reset command that clears everything in memory and truncates the journal:
// Reset everything via the test UI server (if running)
const response = await fetch('http://localhost:3000/reset', {
method: 'POST'
});
Or using the engine message directly (if you have access to the engine channel):
// This is internal - use the API endpoint above for external access
engineTx.send({
type: 'Reset',
reply_tx: replyChannel
});
Option 2: Delete Journal File (Clean Restart)
For a complete fresh start, stop the server and delete the journal file:
# Stop the orderbook server first, then:
# On Linux/Mac
rm -rf data/journal.log
# On Windows
Remove-Item -Recurse -Force data\journal.log
# Restart the server
cargo run --bin server
The data/ directory contains:
journal.log- All events (orders, markets, matches)
Deleting this file means the engine starts with zero state on next boot.
Option 3: Use a Fresh Data Directory
Start the server with a different data directory:
# Linux/Mac
DATA_DIR=/tmp/fresh_orderbook cargo run --bin server
# Or simply use a timestamped directory for testing
mkdir -p data_test_$(date +%s)
What Gets Cleared
| Component | After Reset |
|---|---|
| All markets | Removed |
| All resting orders | Removed |
| Match history | Removed |
| Journal file | Truncated to 0 bytes |
| Sequence numbers | Reset to 0 |
Known Issues
This section documents known limitations and issues that will be addressed in future updates.
Monitor Dashboard: In-Memory Data Only
Issue: The Monitor Dashboard (/monitor.html) only displays events that occur while it is open. Historical data from before the dashboard was opened is not available.
Details:
- The monitor stores all event data in browser memory only
- Refreshing the page or opening a new tab clears all accumulated data
- Events that occurred before connecting to the WebSocket are not visible
- Large volumes of events may consume significant browser memory over time
Workaround:
- Open the monitor before starting stress tests or important operations
- Use the Export JSON/CSV feature to save data before refreshing
- For production monitoring, integrate with external logging/monitoring systems
Status: This limitation will be addressed in a future update with:
- Server-side event history buffering
- Persistent storage for event logs
- Historical event replay on dashboard connection
See Also
- API_REFERENCE.md - Complete gRPC API documentation
- OrderBook_Universal_Architecture.md - Architecture deep dive