Example Tutorials
Deep-dive explanations for each example. Understand not just what the code does, but why it works that way.
Basics
Login, place orders, cancel orders
Cross-Matching
Multi-account order execution
Market Data
Redis & Kafka streaming
Account
Portfolio, orders, transactions
Interactive Notebook Tutorial
Self-guided Jupyter Notebook with all examples in one place. Run cells interactively and learn by doing.
(.ipynb)Basics
What It Does
Establishes a FIX session with the paper trading server and demonstrates the event-driven connection lifecycle.
Why This Approach?
Event-driven vs Polling: Instead of continuously checking is_logged_on() in a loop (which wastes CPU), we subscribe to events. The fix:logon event fires instantly when connection succeeds.
Key Concepts
- Event Handlers: Define functions like
on_logon(),on_logout()before connecting - Non-blocking Connect:
client.connect()returns immediately, usewait_until_logged_on()to block - REST API Access: After FIX login, REST endpoints become available for account queries
- Cleanup: Uses
os._exit(0)to avoid QuickFIX cleanup segfault (known issue)
Core Pattern
# 1. Define event handlers FIRST
def on_logon(session_id, **kw):
print(f"✅ Connected: {session_id}")
# 2. Subscribe before connecting
client.on("fix:logon", on_logon)
# 3. Connect (non-blocking)
client.connect()
# 4. Wait for result
if client.wait_until_logged_on(timeout=10):
# Success - REST APIs now available
cash = client.get_cash_balance()
else:
print(f"Failed: {client.last_logon_error()}")
What It Does
Places a limit order at an unlikely price, waits for acceptance, then cancels it before execution.
Why This Approach?
Async Order Lifecycle: cancel_order() doesn't return success/failure directly. Instead, listen to fix:order:canceled event. This matches real exchange behavior where cancel requests are processed asynchronously.
Key Concepts
- Order Tracker Pattern: Use a class with
ThreadEventto synchronize async events - Event Filtering: Compare
cl_ord_idin handler to filter relevant events - Cancellation Flow: Order → Accepted → (wait) → Cancel Request → Canceled Event
- Unlikely Price: Use unrealistic price to prevent accidental fills
Synchronization Pattern
from threading import Event as ThreadEvent
class OrderTracker:
def __init__(self):
self.order_accepted = ThreadEvent()
self.order_canceled = ThreadEvent()
self.order_id = None
# Subscribe with closure to pass tracker
client.on("fix:order:accepted",
lambda **kw: on_accepted(tracker, **kw))
client.on("fix:order:canceled",
lambda **kw: on_canceled(tracker, **kw))
# Place order
tracker.order_id = client.place_order(...)
# Wait for acceptance (blocking)
tracker.order_accepted.wait(timeout=10)
# Cancel and wait for confirmation
client.cancel_order(tracker.order_id)
tracker.order_canceled.wait(timeout=10)
Cross-Matching
What It Does
Demonstrates how two sub-accounts (D1 and D2) can instantly match orders against each other for simulated trading.
Why Cross-Matching?
Paper Trading Without Market: In real markets, orders match against other traders. In paper trading, D2 acts as an "unconditional counterparty" that always matches D1's orders at the specified price. This enables strategy testing without needing real market liquidity.
Key Concepts
- D1 = Main Trader: Places orders based on strategy logic
- D2 = Matching Tool: Acts as counterparty to execute D1's orders
- use_sub_account(): Context manager for thread-safe account switching
- Same Price = Instant Match: When D1 SELLS and D2 BUYS at same price, immediate fill
Cross-Match Flow
# D1 sells first (creates order in book)
with client.use_sub_account("D1"):
order_d1 = client.place_order(
symbol, side="SELL", qty=1, price=1950.0
)
# Small delay to ensure D1 is in book
time.sleep(0.5)
# D2 buys (matches instantly with D1)
with client.use_sub_account("D2"):
order_d2 = client.place_order(
symbol, side="BUY", qty=1, price=1950.0
)
# Both accounts receive fix:order:filled events!
What It Does
Tests two execution patterns: (A) Full fill in one event, (B) Multiple orders with independent fills.
Why Test Fill Patterns?
Real Market Behavior: In production, large orders are rarely filled in one transaction. They get filled incrementally as matching orders arrive. Understanding cum_qty (cumulative filled) vs last_qty (this fill) is essential for proper position tracking.
Key Concepts
- Scenario A: D1 sells 5, D2 buys 5 → 1 fill event (complete match)
- Scenario B: D1 places 3 separate orders → D2 matches each → 3 fill events
- FIFO Matching: Same price orders matched by time priority (First In, First Out)
- cum_qty Tracking: Track cumulative filled quantity across multiple fills
Fill Tracking
def on_order_filled(self, cl_ord_id, last_px, last_qty,
cum_qty=None, **kw):
"""Track fill progress."""
order = self.orders[cl_ord_id]
# Update cumulative (use server value if available)
if cum_qty is not None:
order["cum_qty"] = cum_qty
else:
order["cum_qty"] += last_qty
# Check completion
is_complete = order["cum_qty"] >= order["qty_ordered"]
print(f"Fill: {last_qty} @ {last_px}")
print(f"Progress: {order['cum_qty']}/{order['qty_ordered']}")
Market Data
What It Does
Fetches current market quotes for multiple instruments using direct Redis GET operations.
When to Use Query Mode?
One-time Snapshots: Use query mode when you need current prices once (e.g., before placing an order). For continuous updates, use subscribe mode instead. Query mode is simpler but creates more Redis connections.
Query Pattern
from paperbroker.market_data import RedisMarketDataClient
client = RedisMarketDataClient(
host=os.getenv("MARKET_REDIS_HOST"),
port=int(os.getenv("MARKET_REDIS_PORT")),
password=os.getenv("MARKET_REDIS_PASSWORD"),
)
# Query single instrument
quote = await client.query("HNXDS:VN30F2511")
if quote:
print(f"Price: {quote.latest_matched_price}")
print(f"Bid: {quote.bid_price_1} x {quote.bid_quantity_1}")
print(f"Ask: {quote.ask_price_1} x {quote.ask_quantity_1}")
What It Does
Subscribes to real-time market updates. RAW mode shows only fields that changed in each update.
RAW vs MERGED Mode
RAW = Bandwidth Efficient: Only changed fields have values, unchanged fields are None. Best for high-frequency data where you're tracking changes, not absolute values. Requires you to maintain state.
RAW Subscribe Pattern
client = RedisMarketDataClient(
host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD,
merge_updates=False, # RAW mode
)
def on_update(instrument, quote):
# Only changed fields have values!
if quote.latest_matched_price is not None:
print(f"Price changed: {quote.latest_matched_price}")
if quote.bid_price_1 is not None:
print(f"Bid changed: {quote.bid_price_1}")
await client.subscribe("HNXDS:VN30F2511", on_update)
What It Does
Subscribes to real-time updates with MERGED mode - every callback receives complete quote data.
Why MERGED Mode?
MERGED = Application Friendly: Client maintains internal cache and fills unchanged fields automatically. You always get complete data without tracking state yourself. Slightly more memory, but much simpler code.
MERGED Subscribe Pattern
client = RedisMarketDataClient(
host=REDIS_HOST, port=REDIS_PORT, password=REDIS_PASSWORD,
merge_updates=True, # MERGED mode
)
def on_update(instrument, quote):
# ALL fields always populated!
print(f"Price: {quote.latest_matched_price}")
print(f"Bid: {quote.bid_price_1} x {quote.bid_quantity_1}")
print(f"Ask: {quote.ask_price_1} x {quote.ask_quantity_1}")
print(f"Spread: {quote.spread} ({quote.spread_bps} bps)")
await client.subscribe("HNXDS:VN30F2511", on_update)
What It Does
Connects to Kafka for market data streaming. Higher throughput and durability than Redis pub/sub.
Kafka vs Redis
Kafka = Production Grade: Kafka provides message durability, replay capability, and better scalability. Topic format is {env_id}.{exchange}.{symbol} (e.g., real.HNXDS.VN30F2602). Use Kafka for production; Redis for development/testing.
Kafka Subscribe Pattern
from paperbroker.market_data import KafkaMarketDataClient
client = KafkaMarketDataClient(
bootstrap_servers=os.getenv("PAPERBROKER_KAFKA_BOOTSTRAP_SERVERS"),
username=os.getenv("PAPERBROKER_KAFKA_USERNAME"),
password=os.getenv("PAPERBROKER_KAFKA_PASSWORD"),
env_id=os.getenv("PAPERBROKER_ENV_ID"), # e.g., 'real'
merge_updates=True
)
def on_quote(instrument, quote):
print(f"{instrument}: {quote.latest_matched_price}")
await client.subscribe("HNXDS:VN30F2602", on_quote)
await client.start() # Start consumer thread
Account
What It Does
Retrieves current portfolio positions with P&L and queries order history within a date range.
REST API Authentication
FIX Session Required: REST endpoints like get_portfolio_by_sub() require an active FIX session. The client automatically handles JWT token management after FIX login. Always connect and wait for logon before calling REST methods.
Query Pattern
# Portfolio
portfolio = client.get_portfolio_by_sub("D1")
for pos in portfolio.get("items", []):
print(f"{pos['instrument']}: {pos['quantity']} @ {pos['currentPrice']}")
print(f" P&L: {pos['pnl']}")
# Orders (last 7 days)
from datetime import datetime, timedelta
today = datetime.now().date()
start = (today - timedelta(days=7)).strftime("%Y-%m-%d")
end = today.strftime("%Y-%m-%d")
orders = client.get_orders(start, end)
for order in orders.get("items", []):
print(f"{order['symbol']}: {order['side']} {order['orderQty']}")
print(f" Filled: {order['cumQty']}, Status: {order['ordStatus']}")
What It Does
Retrieves transaction history (fills/executions) and demonstrates mapping transactions back to orders.
Orders vs Transactions
1:N Relationship: One order can have multiple transactions (fills). Use transaction.orderId to link back to order.orderId. Essential for analyzing execution quality and fees.
Transaction Query
# Get transactions
txns = client.get_transactions_by_date(start_date, end_date)
# Group by order
from collections import defaultdict
by_order = defaultdict(list)
for txn in txns.get("items", []):
order_id = txn.get("orderId")
by_order[order_id].append(txn)
# Analyze each order's fills
for order_id, fills in by_order.items():
total_qty = sum(f.get("quantity", 0) for f in fills)
avg_price = sum(f["price"] * f["quantity"] for f in fills) / total_qty
print(f"Order {order_id}: {len(fills)} fills, avg {avg_price}")
What It Does
Calculates maximum order quantity before placing, accounting for margin, fees, and cash balance.
Why Pre-calculate?
Avoid Order Rejection: Placing an order larger than buying power results in rejection. Use get_max_placeable() to calculate limits before placing. Different for BUY (need cash) vs SELL (might be unlimited).
Position Sizing
# Check before buying
result = client.get_max_placeable(
symbol="HNXDS:VN30F2511",
price=1950.0,
side="BUY"
)
if result.get("success"):
max_qty = result["maxQty"]
per_unit = result["perUnitCost"]
remain = result["remainCash"]
unlimited = result["unlimited"]
if unlimited:
print("No limit - can place any quantity")
else:
print(f"Max BUY: {max_qty} contracts")
print(f"Cost per unit: {per_unit}")
print(f"Available cash: {remain}")
# Use 80% of max for safety margin
safe_qty = int(max_qty * 0.8)
client.place_order(symbol, "BUY", safe_qty, 1950.0)