BacktestL1TradesOrders#
Description#
BacktestL1TradesOrders is the preferred market-making backtest: it takes both the
top-of-book quote and the trade tape, so fills come from actual executions rather
than inferred from quote-size changes. Each event carries the strategy's own quote
(bid_price, bid_size, ask_price, ask_size), the market top-of-book
(market_bid, market_ask, market_bid_size, market_ask_size), and a print
(trade_price, trade_size) (align the three streams with combine_latest upstream).
Quotes mark the position (to the mid) and seed context; trades drive the
fills. A sell-print through bid_price fills the full remaining, a sell-print at
bid_price fills min(remaining, participation_ratio * trade_size), both maker
fills at bid_price paying maker_fee. A market quote that crosses your resting
price with no explaining trade is the run-over fallback (a full maker fill), and a
quote submitted already crossing the spread is a taker (fills at the market, walking
tick_size for the overflow, paying taker_fee). The sell side is symmetric.
Fills are capped to [min_position, max_position].
Input contract: quotes are as-of state (forward-fill them upstream); trades are
not forward-filled. A NaN trade is a quote-only update (mark, no fill), which
is nan_policy: ignore, so each real trade appears on exactly one row and fills at
most once. There is no double-counting and no fill-versus-cancel ambiguity.
Inputs are (bid_price, bid_size, ask_price, ask_size, market_bid, market_ask, market_bid_size, market_ask_size, trade_price, trade_size), where the first four
slots carry the strategy's own resting quotes and the next four carry the top-of-book
market quote. It emits the four positional columns shared
by the backtest family: 0 = equity (cumulative dollar PnL), 1 = pnl (per
event), 2 = position, and 3 = cost (per event). A NaN in the market quote
skips the event; a NaN own-quote price means that side is not quoted.
backtest_report summarizes the resulting equity curve.
Limitations#
Queue position is not tracked from order identity (that needs an L3 / market-by-order
feed), so at-price fills use participation_ratio as a front-of-queue proxy.
Orders are counterfactual (zero volume, no market impact); a marketable order fills
beyond the displayed size only under the tick_size slippage assumption.
Examples#
Usage plot#
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from screamer import BacktestL1TradesOrders
rng = np.random.default_rng(1)
n = 600
t = np.arange(n)
mid = 100 + 2 * np.sin(2 * np.pi * t / n * 3) + rng.standard_normal(n) * 0.2
half = 0.25
bid, ask = mid - half, mid + half
five, one = np.full(n, 5.0), np.ones(n)
# quote at the touch on both sides; trades print at the bid (a seller hits it) or
# the ask (a buyer lifts it), and those prints drive the fills.
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 = BacktestL1TradesOrders(fill="touch", maker_fee=-0.0001, participation_ratio=0.3,
max_position=15.0, min_position=-15.0)(
bid, one, ask, one, bid, ask, five, five, 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='BacktestL1TradesOrders: 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()