Skip to content

Commit 27c99a2

Browse files
committed
feat: add Blotter system for trade documentation and order book management
- Add Blotter ABC with place_order, cancel_order, batch_order, and transaction recording - Add SimulationBlotter with configurable slippage and commission models - Add SlippageModel (NoSlippage, PercentageSlippage, FixedSlippage) - Add CommissionModel (NoCommission, PercentageCommission, FixedCommission) - Add Transaction data class for trade documentation - Integrate blotter into App (set_blotter/get_blotter) and Context (batch_order/get_transactions) - Add 43 unit tests Closes #457
1 parent c5fdea2 commit 27c99a2

6 files changed

Lines changed: 1273 additions & 3 deletions

File tree

investing_algorithm_framework/__init__.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@
2424
APPLICATION_DIRECTORY, DataSource, OrderExecutor, PortfolioProvider, \
2525
SnapshotInterval, AWS_S3_STATE_BUCKET_NAME, BacktestEvaluationFocus, \
2626
save_backtests_to_directory, BacktestMetrics, DATA_DIRECTORY, \
27-
retag_backtests
27+
retag_backtests, \
28+
Blotter, SimulationBlotter, Transaction, \
29+
SlippageModel, NoSlippage, PercentageSlippage, FixedSlippage, \
30+
CommissionModel, NoCommission, PercentageCommission, FixedCommission
2831
from .infrastructure import AzureBlobStorageStateHandler, \
2932
CSVOHLCVDataProvider, CSVTickerDataProvider, CSVURLDataProvider, \
3033
JSONURLDataProvider, ParquetURLDataProvider, \
@@ -229,5 +232,16 @@
229232
"download_v2",
230233
"DownloadResult",
231234
"create_data_storage_path",
232-
"DATA_DIRECTORY"
235+
"DATA_DIRECTORY",
236+
"Blotter",
237+
"SimulationBlotter",
238+
"Transaction",
239+
"SlippageModel",
240+
"NoSlippage",
241+
"PercentageSlippage",
242+
"FixedSlippage",
243+
"CommissionModel",
244+
"NoCommission",
245+
"PercentageCommission",
246+
"FixedCommission",
233247
]

investing_algorithm_framework/app/app.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,10 +71,16 @@ def __init__(self, state_handler=None, name=None):
7171
self._state_handler = state_handler
7272
self._run_history = None
7373
self._name = name
74+
self._blotter = None
7475

7576
@property
7677
def context(self):
77-
return self.container.context()
78+
ctx = self.container.context()
79+
80+
if self._blotter is not None:
81+
ctx._blotter = self._blotter
82+
83+
return ctx
7884

7985
@property
8086
def resource_directory_path(self):
@@ -2207,6 +2213,39 @@ def add_market(
22072213
)
22082214
self.add_market_credential(market_credential)
22092215

2216+
def set_blotter(self, blotter):
2217+
"""
2218+
Set a blotter for order book management. The blotter sits
2219+
between the strategy and the order execution layer, enabling
2220+
batch ordering, transaction tracking, and custom order routing.
2221+
2222+
Args:
2223+
blotter: Instance of Blotter
2224+
2225+
Returns:
2226+
None
2227+
"""
2228+
from investing_algorithm_framework.domain.blotter import Blotter
2229+
2230+
if inspect.isclass(blotter):
2231+
blotter = blotter()
2232+
2233+
if not isinstance(blotter, Blotter):
2234+
raise OperationalException(
2235+
"Blotter should be an instance of Blotter"
2236+
)
2237+
2238+
self._blotter = blotter
2239+
2240+
def get_blotter(self):
2241+
"""
2242+
Get the configured blotter.
2243+
2244+
Returns:
2245+
Blotter or None: The configured blotter instance.
2246+
"""
2247+
return self._blotter
2248+
22102249
def add_order_executor(self, order_executor):
22112250
"""
22122251
Function to add an order executor to the app. The order executor

investing_algorithm_framework/app/context.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def __init__(
5050
trade_stop_loss_service
5151
self.trade_take_profit_service: TradeTakeProfitService = \
5252
trade_take_profit_service
53+
self._blotter = None
5354

5455
def _validate_target_symbol(self, target_symbol, market=None):
5556
"""
@@ -2196,3 +2197,111 @@ def run_strategy(self, context, data):
21962197
self._parquet_url_providers[url] = provider
21972198

21982199
return self._parquet_url_providers[url].get_data()
2200+
2201+
def batch_order(self, orders, market=None):
2202+
"""
2203+
Place multiple orders at once.
2204+
2205+
If a blotter is configured (via ``app.set_blotter()``), the
2206+
orders are routed through the blotter. Otherwise, orders are
2207+
created directly.
2208+
2209+
Each order dict supports the same parameters as
2210+
``create_limit_order`` and ``create_market_order``.
2211+
2212+
Args:
2213+
orders (list[dict]): List of order dicts. Each dict should
2214+
contain at minimum ``target_symbol``, ``order_side``,
2215+
and either ``amount`` or ``percentage_of_portfolio``.
2216+
Include ``price`` for limit orders.
2217+
Include ``order_type`` to specify MARKET orders
2218+
(default is LIMIT).
2219+
market (str, optional): Default market for all orders.
2220+
Can be overridden per order.
2221+
2222+
Returns:
2223+
list[Order]: The created orders.
2224+
2225+
Example::
2226+
2227+
context.batch_order([
2228+
{
2229+
"target_symbol": "BTC",
2230+
"order_side": OrderSide.BUY,
2231+
"percentage_of_portfolio": 5.0,
2232+
"price": 45000,
2233+
},
2234+
{
2235+
"target_symbol": "ETH",
2236+
"order_side": OrderSide.BUY,
2237+
"percentage_of_portfolio": 3.0,
2238+
"price": 3000,
2239+
},
2240+
])
2241+
"""
2242+
if self._blotter is not None:
2243+
# Add default market to each order if not set
2244+
for order_data in orders:
2245+
if "market" not in order_data and market is not None:
2246+
order_data["market"] = market
2247+
2248+
return self._blotter.batch_order(orders, self)
2249+
2250+
# Default: create orders directly
2251+
results = []
2252+
2253+
for order_data in orders:
2254+
order_market = order_data.get("market", market)
2255+
order_type = order_data.get("order_type", OrderType.LIMIT.value)
2256+
2257+
if OrderType.MARKET.equals(order_type):
2258+
order = self.create_market_order(
2259+
target_symbol=order_data["target_symbol"],
2260+
order_side=order_data["order_side"],
2261+
amount=order_data.get("amount"),
2262+
percentage_of_portfolio=order_data.get(
2263+
"percentage_of_portfolio"
2264+
),
2265+
percentage_of_position=order_data.get(
2266+
"percentage_of_position"
2267+
),
2268+
precision=order_data.get("precision"),
2269+
market=order_market,
2270+
metadata=order_data.get("metadata"),
2271+
)
2272+
else:
2273+
order = self.create_limit_order(
2274+
target_symbol=order_data["target_symbol"],
2275+
price=order_data["price"],
2276+
order_side=order_data["order_side"],
2277+
amount=order_data.get("amount"),
2278+
percentage_of_portfolio=order_data.get(
2279+
"percentage_of_portfolio"
2280+
),
2281+
percentage_of_position=order_data.get(
2282+
"percentage_of_position"
2283+
),
2284+
precision=order_data.get("precision"),
2285+
market=order_market,
2286+
metadata=order_data.get("metadata"),
2287+
)
2288+
2289+
results.append(order)
2290+
2291+
return results
2292+
2293+
def get_transactions(self):
2294+
"""
2295+
Get all recorded transactions from the blotter.
2296+
2297+
Returns a list of Transaction objects representing all
2298+
fills/executions that have been processed through the blotter.
2299+
2300+
Returns:
2301+
list[Transaction]: Recorded transactions, or an empty
2302+
list if no blotter is configured.
2303+
"""
2304+
if self._blotter is not None:
2305+
return self._blotter.get_transactions()
2306+
2307+
return []

investing_algorithm_framework/domain/__init__.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
TakeProfitRule, StopLossRule, PositionSize, ScalingRule, TradingCost
2626
from .order_executor import OrderExecutor
2727
from .portfolio_provider import PortfolioProvider
28+
from .blotter import Blotter, SimulationBlotter, Transaction, \
29+
SlippageModel, NoSlippage, PercentageSlippage, FixedSlippage, \
30+
CommissionModel, NoCommission, PercentageCommission, FixedCommission
2831
from .services import MarketCredentialService, AbstractPortfolioSyncService, \
2932
RoundingService, StateHandler
3033
from .stateless_actions import StatelessActions
@@ -152,4 +155,15 @@
152155
"save_backtests_to_directory",
153156
"retag_backtests",
154157
"generate_algorithm_id",
158+
"Blotter",
159+
"SimulationBlotter",
160+
"Transaction",
161+
"SlippageModel",
162+
"NoSlippage",
163+
"PercentageSlippage",
164+
"FixedSlippage",
165+
"CommissionModel",
166+
"NoCommission",
167+
"PercentageCommission",
168+
"FixedCommission",
155169
]

0 commit comments

Comments
 (0)