Blotter Architecture¶
Complete guide to RustyBT's order management and routing system.
Overview¶
The Blotter is RustyBT's central order management system responsible for:
- Order validation and routing
- Order state management
- Fill processing and transaction creation
- Integration with brokers (live trading) or simulation (backtesting)
Architecture¶
Strategy Algorithm
│
│ order(asset, amount, style)
▼
┌──────────────────────────────────┐
│ Blotter (Abstract) │
│ ┌────────────────────────────┐ │
│ │ Order Validation │ │
│ │ • Asset tradeable? │ │
│ │ • Sufficient funds? │ │
│ │ • Position limits OK? │ │
│ └────────────────────────────┘ │
│ ┌────────────────────────────┐ │
│ │ Order State Management │ │
│ │ • Track open orders │ │
│ │ • Update order status │ │
│ │ • Handle cancellations │ │
│ └────────────────────────────┘ │
│ ┌────────────────────────────┐ │
│ │ Order Routing │ │
│ │ • Route to broker/sim │ │
│ │ • Process fills │ │
│ │ • Apply costs │ │
│ └────────────────────────────┘ │
└───────────┬──────────────────────┘
│
┌───────┴─────────┐
│ │
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Simulation │ │ Broker │
│ Blotter │ │ Adapter │
│ (Backtest) │ │ (Live) │
└─────────────┘ └─────────────┘
Blotter Interface¶
Base Class¶
from rustybt.finance.blotter import Blotter
class Blotter(ABC):
"""Abstract base class for order management."""
def __init__(self, cancel_policy=None):
"""Initialize blotter.
Parameters
----------
cancel_policy : CancelPolicy, optional
Policy for automatic order cancellation
"""
self.cancel_policy = cancel_policy or NeverCancel()
self.current_dt = None
self.orders = {} # All orders by ID
self.open_orders = {} # Open orders by asset
@abstractmethod
def order(self, asset, amount, style, order_id=None):
"""Place an order.
Parameters
----------
asset : Asset
Asset to trade
amount : int
Quantity (positive=buy, negative=sell)
style : ExecutionStyle
Order execution style
order_id : str, optional
Custom order ID
Returns
-------
order_id : str or None
Order ID if accepted, None if rejected
"""
raise NotImplementedError
@abstractmethod
def cancel(self, order_id, relay_status=True):
"""Cancel an order.
Parameters
----------
order_id : str
Order ID to cancel
relay_status : bool
Whether to update order status
"""
raise NotImplementedError
@abstractmethod
def execute_cancel_policy(self, event):
"""Execute cancellation policy.
Parameters
----------
event : Event
Current simulation/market event
"""
raise NotImplementedError
SimulationBlotter¶
The SimulationBlotter implements the Blotter interface for backtesting.
Initialization¶
from rustybt.finance.blotter import SimulationBlotter
from rustybt.finance.slippage import VolumeShareSlippage
from rustybt.finance.commission import PerShare
# Create simulation blotter
blotter = SimulationBlotter(
data_frequency='daily',
slippage=VolumeShareSlippage(),
commission=PerShare(cost=0.001, min_trade_cost=1.0)
)
Order Processing Flow¶
1. order() called
│
▼
2. Validate order
├─ Asset tradeable? ──NO──> REJECT
├─ Amount non-zero? ──NO──> REJECT
└─ Funds sufficient? ──NO──> REJECT
│ YES
▼
3. Create Order object
│
▼
4. Check cancel policy
└─ Auto-cancel previous? ──YES──> Cancel old orders
│
▼
5. Add to open_orders
│
▼
6. Return order_id
Order Execution Flow (Per Bar)¶
handle_data() called
│
▼
1. Process all open orders
│
▼
2. For each order:
├─ Check triggers (stop/limit)
│ └─ Update stop_reached/limit_reached
├─ If triggered:
│ ├─ Get current price
│ ├─ Apply slippage model
│ ├─ Check volume constraints
│ ├─ Create transaction(s)
│ └─ Apply commission
└─ Update order status
│
▼
3. Update portfolio positions
Order Validation¶
Validation Checks¶
class SimulationBlotter(Blotter):
def validate_order(self, asset, amount, style):
"""Validate order before placement.
Checks:
- Asset exists and is tradeable
- Amount is non-zero integer
- Stop/limit prices are valid
- Sufficient buying power (if enforced)
Returns
-------
is_valid : bool
rejection_reason : str or None
"""
# Check asset tradeable
if not self.is_asset_tradeable(asset):
return False, f"Asset {asset.symbol} is not tradeable"
# Check amount non-zero
if amount == 0:
return False, "Order amount cannot be zero"
# Check prices valid
if style.get_limit_price(amount > 0) is not None:
if style.get_limit_price(amount > 0) <= 0:
return False, "Limit price must be positive"
if style.get_stop_price(amount > 0) is not None:
if style.get_stop_price(amount > 0) <= 0:
return False, "Stop price must be positive"
return True, None
Rejection Handling¶
def order(self, asset, amount, style, order_id=None):
# Validate order
is_valid, rejection_reason = self.validate_order(asset, amount, style)
if not is_valid:
# Create rejected order for tracking
order = Order(
dt=self.current_dt,
asset=asset,
amount=amount,
id=order_id
)
order.reject(rejection_reason)
self.orders[order.id] = order
logger.warning(f"Order rejected: {rejection_reason}")
return None # No order ID returned
# ... continue with order placement
Order State Management¶
Tracking Open Orders¶
class SimulationBlotter(Blotter):
def add_order(self, order):
"""Add order to tracking."""
# Add to all orders
self.orders[order.id] = order
# Add to open orders by asset
if order.asset not in self.open_orders:
self.open_orders[order.asset] = []
self.open_orders[order.asset].append(order)
def remove_order(self, order):
"""Remove order from open tracking."""
if order.asset in self.open_orders:
self.open_orders[order.asset].remove(order)
# Clean up empty lists
if not self.open_orders[order.asset]:
del self.open_orders[order.asset]
Order Status Updates¶
def process_order_fills(self, order, current_bar):
"""Process fills for an order."""
# Check if order triggered
current_price = current_bar['close']
order.check_triggers(current_price, self.current_dt)
if not order.triggered:
return # Order not ready to execute
# Calculate fill
fill_price, fill_amount = self.calculate_fill(
order, current_bar
)
if fill_amount > 0:
# Create transaction
transaction = self.create_transaction(
order, fill_price, fill_amount
)
# Update order
order.filled += fill_amount
order.commission += transaction.commission
# Update status
if order.filled >= order.amount:
order.status = ORDER_STATUS.FILLED
self.remove_order(order) # Remove from open orders
else:
order.status = ORDER_STATUS.PARTIALLY_FILLED
Fill Processing¶
Calculate Fill Amount¶
def calculate_fill(self, order, current_bar):
"""Calculate how much of order can fill.
Parameters
----------
order : Order
Order to fill
current_bar : dict
Current bar data (OHLCV)
Returns
-------
fill_price : float
Execution price after slippage
fill_amount : int
Number of shares filled
"""
# Get base price
if order.limit is not None:
base_price = order.limit
else:
base_price = current_bar['close']
# Apply slippage model
fill_price, fill_amount = self.slippage_model.process_order(
order=order,
bar=current_bar,
base_price=base_price
)
# Limit fill amount to available
max_fill = min(order.open_amount, fill_amount)
return fill_price, max_fill
Create Transaction¶
def create_transaction(self, order, price, amount):
"""Create transaction from order fill.
Parameters
----------
order : Order
Order being filled
price : float
Fill price
amount : int
Shares filled
Returns
-------
transaction : Transaction
Created transaction object
"""
# Calculate commission
commission = self.commission_model.calculate(
order=order,
transaction_amount=amount,
transaction_price=price
)
# Create transaction
transaction = Transaction(
asset=order.asset,
amount=amount,
dt=self.current_dt,
price=price,
order_id=order.id,
commission=commission
)
return transaction
Cancel Policy¶
Control automatic order cancellation.
Never Cancel (Default)¶
from rustybt.finance.cancel_policy import NeverCancel
# Orders remain open until filled or manually cancelled
blotter = SimulationBlotter(cancel_policy=NeverCancel())
Cancel All Orders on Bar¶
from rustybt.finance.cancel_policy import EODCancel
# Cancel all open orders at end of each day
blotter = SimulationBlotter(cancel_policy=EODCancel())
Custom Cancel Policy¶
from rustybt.finance.cancel_policy import CancelPolicy
class CustomCancelPolicy(CancelPolicy):
def should_cancel(self, order, event):
"""Determine if order should be cancelled.
Parameters
----------
order : Order
Order to check
event : BarData
Current event
Returns
-------
should_cancel : bool
"""
# Cancel orders older than 5 days
if (event.dt - order.dt).days > 5:
return True
# Cancel stop orders if unreasonably far from current price
if order.stop is not None:
current_price = event.current(order.asset, 'close')
if abs(order.stop - current_price) / current_price > 0.20:
return True # More than 20% away
return False
Batch Order Processing¶
Process multiple orders efficiently.
def batch_order(self, order_arg_lists):
"""Place multiple orders at once.
Parameters
----------
order_arg_lists : list[tuple]
List of (asset, amount, style) tuples
Returns
-------
order_ids : list[str or None]
Order IDs for each order
"""
order_ids = []
for asset, amount, style in order_arg_lists:
order_id = self.order(asset, amount, style)
order_ids.append(order_id)
return order_ids
# Usage:
orders_to_place = [
(asset1, 100, LimitOrder(150.0)),
(asset2, -50, MarketOrder()),
(asset3, 200, StopOrder(95.0))
]
order_ids = blotter.batch_order(orders_to_place)
Integration with Strategy¶
Accessing Blotter¶
class MyStrategy(TradingAlgorithm):
def handle_data(self, context, data):
# Blotter available via context
blotter = context.blotter
# Get all open orders
all_open = blotter.open_orders
# Get orders for specific asset
asset_orders = all_open.get(asset, [])
# Check specific order
order = blotter.orders.get(order_id)
Manual Order Management¶
def handle_data(self, context, data):
# Place order through blotter directly
order_id = context.blotter.order(
asset=asset,
amount=100,
style=LimitOrder(150.0)
)
# Cancel order through blotter
context.blotter.cancel(order_id)
# Check order status
order = context.blotter.orders.get(order_id)
print(f"Order status: {order.status.name}")
Performance Considerations¶
Order Volume Limits¶
Configure slippage to respect volume constraints:
from rustybt.finance.slippage import VolumeShareSlippage
# Limit to 2.5% of bar volume
blotter = SimulationBlotter(
slippage=VolumeShareSlippage(
volume_limit=0.025,
price_impact=0.1
)
)
Batch Processing Optimization¶
# Process all orders for a bar at once
def process_bar(self, bar_data):
"""Process all open orders for current bar."""
orders_to_process = []
# Collect all triggered orders
for asset, orders in self.open_orders.items():
for order in orders:
if order.triggered:
orders_to_process.append(order)
# Process in batch
for order in orders_to_process:
self.process_order_fills(order, bar_data)
Best Practices¶
✅ DO¶
- Use Cancel Policies: Prevent stale orders from accumulating
- Monitor Open Orders: Track order states actively
- Handle Rejections: Log and respond to rejected orders
- Validate Before Submission: Check order parameters
- Process Partial Fills: Account for incomplete fills
❌ DON'T¶
- Bypass Blotter: Always use blotter, don't manipulate orders directly
- Ignore Order Status: Check status before assuming fills
- Place Duplicate Orders: Check for existing orders first
- Forget Volume Limits: Configure realistic slippage
- Skip Commission Modeling: Always model transaction costs
Troubleshooting¶
Orders Not Filling¶
Symptoms: - Orders remain in OPEN state - No fills occurring
Causes: - Limit/stop prices not reached - Insufficient volume - Order not triggered
Solutions:
# Check order triggers
order = context.blotter.orders.get(order_id)
print(f"Stop reached: {order.stop_reached}")
print(f"Limit reached: {order.limit_reached}")
print(f"Triggered: {order.triggered}")
# Adjust prices or use market order
if not order.triggered:
cancel_order(order)
order(asset, amount, style=MarketOrder())
Unexpected Rejections¶
Symptoms: - Orders immediately rejected - None returned from order()
Causes: - Insufficient funds - Invalid prices - Position limits
Solutions:
# Check rejected orders
for order in context.blotter.orders.values():
if order.status == ORDER_STATUS.REJECTED:
print(f"Rejection reason: {order.reason}")
# Adjust based on reason
if "INSUFFICIENT_FUNDS" in order.reason:
# Reduce order size
elif "INVALID_PRICE" in order.reason:
# Check stop/limit prices
Related Documentation¶
- Simulation Blotter (Coming soon) - Backtesting execution details
- Fill Processing (Coming soon) - Fill calculation and transaction creation
- Order Lifecycle - Order state transitions
- Transaction Costs - Cost modeling
Next Steps¶
- Study Simulation Blotter (Coming soon) for backtesting specifics
- Review Fill Processing (Coming soon) for execution details
- Explore Transaction Costs for realistic modeling