|
| 1 | +--- |
| 2 | +sidebar_position: 3 |
| 3 | +--- |
| 4 | + |
| 5 | +# Blotter |
| 6 | + |
| 7 | +Learn how the Blotter system works and how to use slippage models, commission models, and custom order routing. |
| 8 | + |
| 9 | +## Overview |
| 10 | + |
| 11 | +The **Blotter** sits between your strategy and the order execution layer. Every order you create — whether via `create_limit_order()`, `create_market_order()`, or `batch_order()` — flows through the blotter before reaching the `OrderService`. |
| 12 | + |
| 13 | +``` |
| 14 | +Strategy (Context) |
| 15 | + │ |
| 16 | + ▼ |
| 17 | + ┌────────┐ |
| 18 | + │ Blotter │ ← slippage, commission, routing |
| 19 | + └────┬───┘ |
| 20 | + │ |
| 21 | + ▼ |
| 22 | + OrderService → OrderExecutor → Exchange / Simulation |
| 23 | +``` |
| 24 | + |
| 25 | +The framework automatically selects a blotter if you don't set one: |
| 26 | + |
| 27 | +| Mode | Default Blotter | Behavior | |
| 28 | +|-------------|----------------------|---------------------------------------------| |
| 29 | +| Live trading | `DefaultBlotter` | Pass-through — no slippage or commission | |
| 30 | +| Backtesting | `SimulationBlotter` | Configurable slippage and commission models | |
| 31 | + |
| 32 | +You can override the default by calling `app.set_blotter(...)` with any `Blotter` subclass. |
| 33 | + |
| 34 | +## Slippage Models |
| 35 | + |
| 36 | +Slippage models determine how the execution price deviates from the intended order price. They are used by the `SimulationBlotter` during backtesting. |
| 37 | + |
| 38 | +### NoSlippage (default) |
| 39 | + |
| 40 | +Orders fill at the exact intended price. |
| 41 | + |
| 42 | +```python |
| 43 | +from investing_algorithm_framework import SimulationBlotter, NoSlippage |
| 44 | + |
| 45 | +app.set_blotter(SimulationBlotter( |
| 46 | + slippage_model=NoSlippage() |
| 47 | +)) |
| 48 | +``` |
| 49 | + |
| 50 | +### PercentageSlippage |
| 51 | + |
| 52 | +Buy orders fill at a slightly higher price, sell orders at a slightly lower price. |
| 53 | + |
| 54 | +```python |
| 55 | +from investing_algorithm_framework import SimulationBlotter, PercentageSlippage |
| 56 | + |
| 57 | +# 0.1% slippage |
| 58 | +app.set_blotter(SimulationBlotter( |
| 59 | + slippage_model=PercentageSlippage(percentage=0.001) |
| 60 | +)) |
| 61 | +``` |
| 62 | + |
| 63 | +For a buy order at price `100.0` with `0.1%` slippage, the fill price becomes `100.10`. For a sell order, it becomes `99.90`. |
| 64 | + |
| 65 | +### FixedSlippage |
| 66 | + |
| 67 | +Adds or subtracts a fixed amount from the order price. |
| 68 | + |
| 69 | +```python |
| 70 | +from investing_algorithm_framework import SimulationBlotter, FixedSlippage |
| 71 | + |
| 72 | +# $0.05 slippage per order |
| 73 | +app.set_blotter(SimulationBlotter( |
| 74 | + slippage_model=FixedSlippage(amount=0.05) |
| 75 | +)) |
| 76 | +``` |
| 77 | + |
| 78 | +### Custom Slippage Model |
| 79 | + |
| 80 | +Create your own by extending `SlippageModel`: |
| 81 | + |
| 82 | +```python |
| 83 | +from investing_algorithm_framework import SlippageModel |
| 84 | + |
| 85 | +class VolumeWeightedSlippage(SlippageModel): |
| 86 | + def __init__(self, base_pct=0.001, volume_factor=0.0001): |
| 87 | + self.base_pct = base_pct |
| 88 | + self.volume_factor = volume_factor |
| 89 | + |
| 90 | + def calculate_slippage(self, price, order_side, amount=None): |
| 91 | + pct = self.base_pct |
| 92 | + if amount is not None: |
| 93 | + pct += self.volume_factor * amount |
| 94 | + |
| 95 | + if order_side == "BUY": |
| 96 | + return price * (1 + pct) |
| 97 | + return price * (1 - pct) |
| 98 | +``` |
| 99 | + |
| 100 | +## Commission Models |
| 101 | + |
| 102 | +Commission models determine the fee charged for each trade. They are used by the `SimulationBlotter` during backtesting. |
| 103 | + |
| 104 | +### NoCommission (default) |
| 105 | + |
| 106 | +Zero fees on all trades. |
| 107 | + |
| 108 | +```python |
| 109 | +from investing_algorithm_framework import SimulationBlotter, NoCommission |
| 110 | + |
| 111 | +app.set_blotter(SimulationBlotter( |
| 112 | + commission_model=NoCommission() |
| 113 | +)) |
| 114 | +``` |
| 115 | + |
| 116 | +### PercentageCommission |
| 117 | + |
| 118 | +Fee is a percentage of the total trade value (`price × amount`). |
| 119 | + |
| 120 | +```python |
| 121 | +from investing_algorithm_framework import SimulationBlotter, PercentageCommission |
| 122 | + |
| 123 | +# 0.1% commission |
| 124 | +app.set_blotter(SimulationBlotter( |
| 125 | + commission_model=PercentageCommission(percentage=0.001) |
| 126 | +)) |
| 127 | +``` |
| 128 | + |
| 129 | +### FixedCommission |
| 130 | + |
| 131 | +A fixed fee per trade, regardless of trade size. |
| 132 | + |
| 133 | +```python |
| 134 | +from investing_algorithm_framework import SimulationBlotter, FixedCommission |
| 135 | + |
| 136 | +# $1.00 per trade |
| 137 | +app.set_blotter(SimulationBlotter( |
| 138 | + commission_model=FixedCommission(amount=1.0) |
| 139 | +)) |
| 140 | +``` |
| 141 | + |
| 142 | +### Custom Commission Model |
| 143 | + |
| 144 | +Create your own by extending `CommissionModel`: |
| 145 | + |
| 146 | +```python |
| 147 | +from investing_algorithm_framework import CommissionModel |
| 148 | + |
| 149 | +class TieredCommission(CommissionModel): |
| 150 | + def calculate_commission(self, price, amount, order_side): |
| 151 | + trade_value = price * amount |
| 152 | + if trade_value > 10000: |
| 153 | + return trade_value * 0.0005 # 0.05% for large trades |
| 154 | + return trade_value * 0.001 # 0.1% for small trades |
| 155 | +``` |
| 156 | + |
| 157 | +## SimulationBlotter |
| 158 | + |
| 159 | +The `SimulationBlotter` applies slippage and commission models to every order and records each fill as a `Transaction`. |
| 160 | + |
| 161 | +```python |
| 162 | +from investing_algorithm_framework import ( |
| 163 | + SimulationBlotter, |
| 164 | + PercentageSlippage, |
| 165 | + PercentageCommission, |
| 166 | +) |
| 167 | + |
| 168 | +app.set_blotter(SimulationBlotter( |
| 169 | + slippage_model=PercentageSlippage(0.001), # 0.1% slippage |
| 170 | + commission_model=PercentageCommission(0.001), # 0.1% commission |
| 171 | +)) |
| 172 | +``` |
| 173 | + |
| 174 | +:::info Automatic Setup |
| 175 | +If you don't set a blotter and run a backtest, the framework automatically uses a `SimulationBlotter` with `NoSlippage` and `NoCommission`. |
| 176 | +::: |
| 177 | + |
| 178 | +## Transactions |
| 179 | + |
| 180 | +Every order placed through the blotter is recorded as a `Transaction`. Transactions provide an audit trail of all fills, including the actual execution price, slippage, and commission. |
| 181 | + |
| 182 | +```python |
| 183 | +class MyStrategy(TradingStrategy): |
| 184 | + def run_strategy(self, algorithm, market_data): |
| 185 | + # Place some orders... |
| 186 | + self.create_limit_order( |
| 187 | + target_symbol="BTC", price=50000, amount=0.1 |
| 188 | + ) |
| 189 | + |
| 190 | + # Get all transactions recorded by the blotter |
| 191 | + transactions = self.get_transactions() |
| 192 | + |
| 193 | + for tx in transactions: |
| 194 | + print(f"{tx.symbol} {tx.order_side}: " |
| 195 | + f"price={tx.price}, amount={tx.amount}, " |
| 196 | + f"commission={tx.commission}, slippage={tx.slippage}") |
| 197 | +``` |
| 198 | + |
| 199 | +Each `Transaction` contains: |
| 200 | + |
| 201 | +| Field | Description | |
| 202 | +|-------|-------------| |
| 203 | +| `order_id` | ID of the order | |
| 204 | +| `symbol` | Target symbol (e.g. `"BTC"`) | |
| 205 | +| `order_side` | `"BUY"` or `"SELL"` | |
| 206 | +| `price` | Actual fill price (after slippage) | |
| 207 | +| `amount` | Fill amount | |
| 208 | +| `cost` | Total cost (`price × amount`) | |
| 209 | +| `commission` | Commission charged | |
| 210 | +| `slippage` | Slippage amount (`abs(fill_price - intended_price)`) | |
| 211 | +| `timestamp` | UTC timestamp of the fill | |
| 212 | + |
| 213 | +You can serialize a transaction with `tx.to_dict()`. |
| 214 | + |
| 215 | +## Batch Orders |
| 216 | + |
| 217 | +The `batch_order()` method lets you place multiple orders at once through the blotter: |
| 218 | + |
| 219 | +```python |
| 220 | +class MyStrategy(TradingStrategy): |
| 221 | + def run_strategy(self, algorithm, market_data): |
| 222 | + orders = [ |
| 223 | + { |
| 224 | + "target_symbol": "BTC", |
| 225 | + "order_side": "BUY", |
| 226 | + "price": 50000, |
| 227 | + "amount": 0.1, |
| 228 | + }, |
| 229 | + { |
| 230 | + "target_symbol": "ETH", |
| 231 | + "order_side": "BUY", |
| 232 | + "price": 3000, |
| 233 | + "amount": 1.0, |
| 234 | + }, |
| 235 | + ] |
| 236 | + created_orders = self.batch_order(orders) |
| 237 | +``` |
| 238 | + |
| 239 | +The default implementation places orders sequentially. Override `batch_order()` in a custom blotter for atomic batch behavior or smart order routing. |
| 240 | + |
| 241 | +## Custom Blotter |
| 242 | + |
| 243 | +Create your own blotter by extending the `Blotter` class and implementing `place_order()` and `cancel_order()`: |
| 244 | + |
| 245 | +```python |
| 246 | +from investing_algorithm_framework import Blotter |
| 247 | + |
| 248 | +class SmartOrderRouter(Blotter): |
| 249 | + def place_order(self, order_data, context): |
| 250 | + """ |
| 251 | + Custom order routing logic. |
| 252 | + """ |
| 253 | + symbol = order_data.get("target_symbol") |
| 254 | + |
| 255 | + # Example: route large orders differently |
| 256 | + amount = order_data.get("amount", 0) |
| 257 | + if amount > 100: |
| 258 | + # Split into smaller orders |
| 259 | + half = amount / 2 |
| 260 | + order_data["amount"] = half |
| 261 | + order1 = context.order_service.create(order_data) |
| 262 | + order2 = context.order_service.create(order_data) |
| 263 | + return order1 # Return the first order |
| 264 | + |
| 265 | + return context.order_service.create(order_data) |
| 266 | + |
| 267 | + def cancel_order(self, order_id, context): |
| 268 | + """ |
| 269 | + Cancel a specific order. |
| 270 | + """ |
| 271 | + order = context.order_service.get(order_id) |
| 272 | + if order is None: |
| 273 | + raise Exception(f"Order {order_id} not found") |
| 274 | + |
| 275 | + context.order_service.update( |
| 276 | + order_id, {"status": "CANCELED"} |
| 277 | + ) |
| 278 | + return context.order_service.get(order_id) |
| 279 | + |
| 280 | +# Register the custom blotter |
| 281 | +app.set_blotter(SmartOrderRouter()) |
| 282 | +``` |
| 283 | + |
| 284 | +### Blotter API |
| 285 | + |
| 286 | +| Method | Required | Description | |
| 287 | +|--------|----------|-------------| |
| 288 | +| `place_order(order_data, context)` | Yes | Place a single order. Must be implemented. | |
| 289 | +| `cancel_order(order_id, context)` | Yes | Cancel an order. Must be implemented. | |
| 290 | +| `batch_order(orders_data, context)` | No | Place multiple orders. Default calls `place_order()` sequentially. | |
| 291 | +| `get_open_orders(context, target_symbol)` | No | Get open orders. Default delegates to context. | |
| 292 | +| `get_transactions()` | No | Get recorded transactions. | |
| 293 | +| `record_transaction(transaction)` | No | Record a fill. | |
| 294 | +| `clear_transactions()` | No | Clear all recorded transactions. | |
| 295 | +| `prune_orders(context)` | No | Clean up stale orders. Default is a no-op. | |
0 commit comments