BacktestTradesOrders#
Description#
BacktestTradesOrders backtests a two-sided order-posting strategy against the
raw trade tape. Each event is a print (trade_price, trade_size) paired with
the strategy's resting quote (bid_price, bid_size, ask_price, ask_size).
Fill logic per event:
Buy side. A resting bid at
bid_pricefills when a sell-print crosses it. Withfill="touch"a print at exactlybid_pricefillsmin(bid_size, participation_ratio * trade_size, room)atbid_price(maker fill, payingmaker_fee). A print strictly belowbid_pricesweeps the full remainingmin(bid_size, room)without a participation cap. Withfill="breach"the at-price case is skipped; only a strict breach fills.Sell side. Symmetric for a resting ask against buy-prints.
Market orders. A
NaNbid price is treated as a market order (+inf limit), which sweeps on any print; that fill usestaker_fee. PassMARKET(which equalsmath.inf) directly to the price argument for the same effect.Inventory caps. All fills are capped so the position stays in
[min_position, max_position].Mark. The position marks to the last trade price.
If both sides fill on the same print (possible when a spread collapses), each side updates the account in sequence, so the final equity and position reflect both fills.
Input contract: a NaN trade_price or trade_size is a no-trade event. The
nan_policy is ignore: the row emits all-NaN outputs and state is unchanged.
Only real prints drive fills. Inputs are
(bid_price, bid_size, ask_price, ask_size, trade_price, trade_size).
Outputs are (equity, pnl, position, cost), the four standard backtest columns
accepted by backtest_report.
Limitations#
Queue priority is not tracked per order; at-price fills use participation_ratio
as a front-of-queue proxy.
tick_size is accepted for interface uniformity with BacktestL1TradesOrders and
BacktestOHLCOrders, but it is inert on the tape: a marketable resting order
fills at the print price without walking the book, so there is no displayed-size
overflow to price. It is kept as a no-op parameter so strategies can share a
uniform parameter set across engines.
Examples#
Usage plot#
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from screamer import BacktestTradesOrders
rng = np.random.default_rng(3)
n = 600
t = np.arange(n)
mid = 100 + 2 * np.sin(2 * np.pi * t / n * 3) + rng.standard_normal(n) * 0.15
half = 0.20
bid, ask = mid - half, mid + half
one = np.ones(n)
# alternate between seller-initiated and buyer-initiated prints
at_ask = rng.standard_normal(n) > 0
trade_price = np.where(at_ask, ask, bid)
trade_size = np.abs(rng.standard_normal(n)) + 0.5
out = BacktestTradesOrders(maker_fee=-0.0001, participation_ratio=0.4,
max_position=12.0, min_position=-12.0)(
bid, one, ask, one, trade_price, trade_size)
eq, pos = out[:, 0], out[:, 2]
fig = make_subplots(rows=3, cols=1, shared_xaxes=True, row_heights=[0.4, 0.3, 0.3],
vertical_spacing=0.06)
fig.add_trace(go.Scatter(y=mid, name='mid', line=dict(color='gray')), row=1, col=1)
fig.add_trace(go.Scatter(y=eq, name='equity', line=dict(color='steelblue')), row=2, col=1)
fig.add_trace(go.Scatter(y=pos, name='inventory', line=dict(color='seagreen')), row=3, col=1)
fig.update_layout(title='BacktestTradesOrders: fills driven by the trade tape, inventory bounded',
yaxis=dict(title='mid'), yaxis2=dict(title='equity ($)'),
yaxis3=dict(title='inventory'),
margin=dict(l=20, r=20, t=60, b=20),
legend=dict(orientation='h', yanchor='bottom', y=1.02, xanchor='right', x=1))
fig.show()