Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,4 @@ venv
examples/tutorial/data
examples/tutorial/backtest_results
examples/tutorial/resources
.data_cache/
295 changes: 295 additions & 0 deletions docusaurus/docs/Advanced Concepts/blotter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
---
sidebar_position: 3
---

# Blotter

Learn how the Blotter system works and how to use slippage models, commission models, and custom order routing.

## Overview

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`.

```
Strategy (Context)
┌────────┐
│ Blotter │ ← slippage, commission, routing
└────┬───┘
OrderService → OrderExecutor → Exchange / Simulation
```

The framework automatically selects a blotter if you don't set one:

| Mode | Default Blotter | Behavior |
|-------------|----------------------|---------------------------------------------|
| Live trading | `DefaultBlotter` | Pass-through — no slippage or commission |
| Backtesting | `SimulationBlotter` | Configurable slippage and commission models |

You can override the default by calling `app.set_blotter(...)` with any `Blotter` subclass.

## Slippage Models

Slippage models determine how the execution price deviates from the intended order price. They are used by the `SimulationBlotter` during backtesting.

### NoSlippage (default)

Orders fill at the exact intended price.

```python
from investing_algorithm_framework import SimulationBlotter, NoSlippage

app.set_blotter(SimulationBlotter(
slippage_model=NoSlippage()
))
```

### PercentageSlippage

Buy orders fill at a slightly higher price, sell orders at a slightly lower price.

```python
from investing_algorithm_framework import SimulationBlotter, PercentageSlippage

# 0.1% slippage
app.set_blotter(SimulationBlotter(
slippage_model=PercentageSlippage(percentage=0.001)
))
```

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`.

### FixedSlippage

Adds or subtracts a fixed amount from the order price.

```python
from investing_algorithm_framework import SimulationBlotter, FixedSlippage

# $0.05 slippage per order
app.set_blotter(SimulationBlotter(
slippage_model=FixedSlippage(amount=0.05)
))
```

### Custom Slippage Model

Create your own by extending `SlippageModel`:

```python
from investing_algorithm_framework import SlippageModel

class VolumeWeightedSlippage(SlippageModel):
def __init__(self, base_pct=0.001, volume_factor=0.0001):
self.base_pct = base_pct
self.volume_factor = volume_factor

def calculate_slippage(self, price, order_side, amount=None):
pct = self.base_pct
if amount is not None:
pct += self.volume_factor * amount

if order_side == "BUY":
return price * (1 + pct)
return price * (1 - pct)
```

## Commission Models

Commission models determine the fee charged for each trade. They are used by the `SimulationBlotter` during backtesting.

### NoCommission (default)

Zero fees on all trades.

```python
from investing_algorithm_framework import SimulationBlotter, NoCommission

app.set_blotter(SimulationBlotter(
commission_model=NoCommission()
))
```

### PercentageCommission

Fee is a percentage of the total trade value (`price × amount`).

```python
from investing_algorithm_framework import SimulationBlotter, PercentageCommission

# 0.1% commission
app.set_blotter(SimulationBlotter(
commission_model=PercentageCommission(percentage=0.001)
))
```

### FixedCommission

A fixed fee per trade, regardless of trade size.

```python
from investing_algorithm_framework import SimulationBlotter, FixedCommission

# $1.00 per trade
app.set_blotter(SimulationBlotter(
commission_model=FixedCommission(amount=1.0)
))
```

### Custom Commission Model

Create your own by extending `CommissionModel`:

```python
from investing_algorithm_framework import CommissionModel

class TieredCommission(CommissionModel):
def calculate_commission(self, price, amount, order_side):
trade_value = price * amount
if trade_value > 10000:
return trade_value * 0.0005 # 0.05% for large trades
return trade_value * 0.001 # 0.1% for small trades
```

## SimulationBlotter

The `SimulationBlotter` applies slippage and commission models to every order and records each fill as a `Transaction`.

```python
from investing_algorithm_framework import (
SimulationBlotter,
PercentageSlippage,
PercentageCommission,
)

app.set_blotter(SimulationBlotter(
slippage_model=PercentageSlippage(0.001), # 0.1% slippage
commission_model=PercentageCommission(0.001), # 0.1% commission
))
```

:::info Automatic Setup
If you don't set a blotter and run a backtest, the framework automatically uses a `SimulationBlotter` with `NoSlippage` and `NoCommission`.
:::

## Transactions

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.

```python
class MyStrategy(TradingStrategy):
def run_strategy(self, algorithm, market_data):
# Place some orders...
self.create_limit_order(
target_symbol="BTC", price=50000, amount=0.1
)

# Get all transactions recorded by the blotter
transactions = self.get_transactions()

for tx in transactions:
print(f"{tx.symbol} {tx.order_side}: "
f"price={tx.price}, amount={tx.amount}, "
f"commission={tx.commission}, slippage={tx.slippage}")
```

Each `Transaction` contains:

| Field | Description |
|-------|-------------|
| `order_id` | ID of the order |
| `symbol` | Target symbol (e.g. `"BTC"`) |
| `order_side` | `"BUY"` or `"SELL"` |
| `price` | Actual fill price (after slippage) |
| `amount` | Fill amount |
| `cost` | Total cost (`price × amount`) |
| `commission` | Commission charged |
| `slippage` | Slippage amount (`abs(fill_price - intended_price)`) |
| `timestamp` | UTC timestamp of the fill |

You can serialize a transaction with `tx.to_dict()`.

## Batch Orders

The `batch_order()` method lets you place multiple orders at once through the blotter:

```python
class MyStrategy(TradingStrategy):
def run_strategy(self, algorithm, market_data):
orders = [
{
"target_symbol": "BTC",
"order_side": "BUY",
"price": 50000,
"amount": 0.1,
},
{
"target_symbol": "ETH",
"order_side": "BUY",
"price": 3000,
"amount": 1.0,
},
]
created_orders = self.batch_order(orders)
```

The default implementation places orders sequentially. Override `batch_order()` in a custom blotter for atomic batch behavior or smart order routing.

## Custom Blotter

Create your own blotter by extending the `Blotter` class and implementing `place_order()` and `cancel_order()`:

```python
from investing_algorithm_framework import Blotter

class SmartOrderRouter(Blotter):
def place_order(self, order_data, context):
"""
Custom order routing logic.
"""
symbol = order_data.get("target_symbol")

# Example: route large orders differently
amount = order_data.get("amount", 0)
if amount > 100:
# Split into smaller orders
half = amount / 2
order_data["amount"] = half
order1 = context.order_service.create(order_data)
order2 = context.order_service.create(order_data)
return order1 # Return the first order

return context.order_service.create(order_data)

def cancel_order(self, order_id, context):
"""
Cancel a specific order.
"""
order = context.order_service.get(order_id)
if order is None:
raise Exception(f"Order {order_id} not found")

context.order_service.update(
order_id, {"status": "CANCELED"}
)
return context.order_service.get(order_id)

# Register the custom blotter
app.set_blotter(SmartOrderRouter())
```

### Blotter API

| Method | Required | Description |
|--------|----------|-------------|
| `place_order(order_data, context)` | Yes | Place a single order. Must be implemented. |
| `cancel_order(order_id, context)` | Yes | Cancel an order. Must be implemented. |
| `batch_order(orders_data, context)` | No | Place multiple orders. Default calls `place_order()` sequentially. |
| `get_open_orders(context, target_symbol)` | No | Get open orders. Default delegates to context. |
| `get_transactions()` | No | Get recorded transactions. |
| `record_transaction(transaction)` | No | Record a fill. |
| `clear_transactions()` | No | Clear all recorded transactions. |
| `prune_orders(context)` | No | Clean up stale orders. Default is a no-op. |
4 changes: 4 additions & 0 deletions docusaurus/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,10 @@ const sidebars = {
type: 'category',
label: 'Advanced Concepts',
items: [
{
type: 'doc',
id: 'Advanced Concepts/blotter',
},
{
type: 'doc',
id: 'Advanced Concepts/custom-data-providers',
Expand Down
25 changes: 23 additions & 2 deletions investing_algorithm_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@
APPLICATION_DIRECTORY, DataSource, OrderExecutor, PortfolioProvider, \
SnapshotInterval, AWS_S3_STATE_BUCKET_NAME, BacktestEvaluationFocus, \
save_backtests_to_directory, BacktestMetrics, DATA_DIRECTORY, \
retag_backtests
retag_backtests, \
Blotter, DefaultBlotter, SimulationBlotter, Transaction, \
SlippageModel, NoSlippage, PercentageSlippage, FixedSlippage, \
VolumeImpactSlippage, \
CommissionModel, NoCommission, PercentageCommission, FixedCommission, \
FillModel, FullFill, VolumeBasedFill
from .infrastructure import AzureBlobStorageStateHandler, \
CSVOHLCVDataProvider, CSVTickerDataProvider, CSVURLDataProvider, \
JSONURLDataProvider, ParquetURLDataProvider, \
Expand Down Expand Up @@ -229,5 +234,21 @@
"download_v2",
"DownloadResult",
"create_data_storage_path",
"DATA_DIRECTORY"
"DATA_DIRECTORY",
"Blotter",
"DefaultBlotter",
"SimulationBlotter",
"Transaction",
"SlippageModel",
"NoSlippage",
"PercentageSlippage",
"FixedSlippage",
"VolumeImpactSlippage",
"CommissionModel",
"NoCommission",
"PercentageCommission",
"FixedCommission",
"FillModel",
"FullFill",
"VolumeBasedFill",
]
Loading
Loading