Skip to content

Commit 6994680

Browse files
committed
refactor: route all orders through Blotter, add DefaultBlotter, update docs
- Add DefaultBlotter as pass-through blotter for live trading - Refactor SimulationBlotter to call order_service.create() directly - Route all Context order methods through the Blotter - Refactor create_limit_buy/sell_order to delegate to create_limit_order - Remove conditional blotter checks (always set via App) - Auto-set DefaultBlotter (live) or SimulationBlotter (backtest) - Remove unused OrderType import (flake8) - Add Blotter documentation page (Advanced Concepts) - Update sidebar navigation - Update tests (48 passing)
1 parent 27c99a2 commit 6994680

8 files changed

Lines changed: 598 additions & 239 deletions

File tree

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
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. |

docusaurus/sidebars.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ const sidebars = {
7777
type: 'category',
7878
label: 'Advanced Concepts',
7979
items: [
80+
{
81+
type: 'doc',
82+
id: 'Advanced Concepts/blotter',
83+
},
8084
{
8185
type: 'doc',
8286
id: 'Advanced Concepts/custom-data-providers',

investing_algorithm_framework/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
SnapshotInterval, AWS_S3_STATE_BUCKET_NAME, BacktestEvaluationFocus, \
2626
save_backtests_to_directory, BacktestMetrics, DATA_DIRECTORY, \
2727
retag_backtests, \
28-
Blotter, SimulationBlotter, Transaction, \
28+
Blotter, DefaultBlotter, SimulationBlotter, Transaction, \
2929
SlippageModel, NoSlippage, PercentageSlippage, FixedSlippage, \
3030
CommissionModel, NoCommission, PercentageCommission, FixedCommission
3131
from .infrastructure import AzureBlobStorageStateHandler, \
@@ -234,6 +234,7 @@
234234
"create_data_storage_path",
235235
"DATA_DIRECTORY",
236236
"Blotter",
237+
"DefaultBlotter",
237238
"SimulationBlotter",
238239
"Transaction",
239240
"SlippageModel",

investing_algorithm_framework/app/app.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,10 +75,12 @@ def __init__(self, state_handler=None, name=None):
7575

7676
@property
7777
def context(self):
78-
ctx = self.container.context()
78+
from investing_algorithm_framework.domain.blotter import \
79+
DefaultBlotter
7980

80-
if self._blotter is not None:
81-
ctx._blotter = self._blotter
81+
ctx = self.container.context()
82+
ctx._blotter = self._blotter \
83+
if self._blotter is not None else DefaultBlotter()
8284

8385
return ctx
8486

@@ -2325,10 +2327,15 @@ def initialize_order_executors(self):
23252327
first check if the app is running in backtest mode or not. If it is
23262328
running in backtest mode, all order executors will be removed and
23272329
a single BacktestOrderExecutor will be added to the order executors.
2330+
It will also set the SimulationBlotter as the default blotter if
2331+
no custom blotter has been configured.
23282332
23292333
If it is not running in backtest mode, it will add the default
23302334
CCXTOrderExecutor with a priority 3.
23312335
"""
2336+
from investing_algorithm_framework.domain.blotter import \
2337+
SimulationBlotter
2338+
23322339
logger.info("Adding order executors")
23332340
order_executor_lookup = self.container.order_executor_lookup()
23342341
environment = self.config[ENVIRONMENT]
@@ -2341,6 +2348,11 @@ def initialize_order_executors(self):
23412348
order_executor_lookup.add_order_executor(
23422349
BacktestOrderExecutor(priority=1)
23432350
)
2351+
2352+
# Auto-set SimulationBlotter for backtesting if no
2353+
# custom blotter has been configured
2354+
if self._blotter is None:
2355+
self._blotter = SimulationBlotter()
23442356
else:
23452357
order_executor_lookup.add_order_executor(
23462358
CCXTOrderExecutor(priority=3)

0 commit comments

Comments
 (0)