BacktestL1Orders#

Description#

BacktestL1Orders is a two-sided market-making backtest against a top-of-book (L1) stream. Each event carries the strategy's own quote (bid_price, bid_size, ask_price, ask_size) and the market's current book (market_bid, market_ask, market_bid_size, market_ask_size). Both sides rest, and either or both can be lifted on one event, so the position is whatever the market fills.

Fills follow where the market moves relative to your quote. When the market ask trades through bid_price (market_ask < bid_price), the resting buy is run over and fills its full remaining at bid_price, a maker fill paying maker_fee. In "touch" mode a lock (market_ask == bid_price) also fills once on entry, for min(remaining, participation_ratio * market_ask_size) at bid_price; further size changes at the same locked price are ignored (a static quote cannot be told from a cancel, so it does not re-fill). The "breach" default keeps only the through case. A quote that appears already crossing the spread (bid_price >= market_ask on submission) is a taker: it fills its full size at the market, walking tick_size for the part beyond the displayed quote, and pays taker_fee. The sell side is symmetric. Fills are capped so the position stays within [min_position, max_position] (unbounded by default). Positions mark to the mid.

Inputs are (bid_price, bid_size, ask_price, ask_size, market_bid, market_ask, market_bid_size, market_ask_size). 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 any market quote field skips the event (nan_policy: ignore); a NaN own-quote price means that side is not quoted. backtest_report summarizes the resulting equity curve.

For a target-position variant that takes the book immediately instead of posting a resting order, see BacktestL1Target. When a trade feed is available, BacktestL1TradesOrders uses real prints to drive fills rather than quote-level heuristics.

Limitations#

Quotes-only fills are a heuristic: a size change at a static price is fill, cancel, and re-quote all at once, and cannot be told apart. The "breach" default fills only when the market trades through your quote, so it can under-fill. "touch" adds a participation partial once per lock episode and can over-fill when a lock's size came from cancels rather than trades. Prefer BacktestL1TradesOrders when a trade feed is available: real trades drive the fills and remove this ambiguity. 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 BacktestL1Orders, Lag

rng = np.random.default_rng(4)
n = 600
t = np.arange(n)
tick = 0.5
# a discrete tick-grid, mean-reverting mid (real books are gridded, so locks occur)
mid = np.round((100 + 3 * np.sin(2 * np.pi * t / n * 4) + rng.standard_normal(n) * 0.4) / tick) * tick
market_bid, market_ask = mid - tick, mid + tick
five, one = np.full(n, 5.0), np.ones(n)

# rest last event's touch on both sides; the market trading back through fills it.
# touch mode captures a participation share on each lock; inventory bounded.
my_bid = np.nan_to_num(Lag(1)(market_bid), nan=market_bid[0])
my_ask = np.nan_to_num(Lag(1)(market_ask), nan=market_ask[0])
out = BacktestL1Orders(fill="touch", maker_fee=-0.0001, participation_ratio=0.5,
                       max_position=8.0, min_position=-8.0)(
    my_bid, one, my_ask, one,
    market_bid, market_ask, five, five)
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 (gridded)', 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='mediumpurple')), row=3, col=1)
fig.update_layout(title='BacktestL1Orders: a quotes-only maker (heuristic fills), inventory capped',
                  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()