BacktestL1Target#
Description#
BacktestL1Target is a lean directional backtest on the L1 quote stream. It is
immediate: the target_position you pass on event t executes on the same quote
update, unlike BacktestOHLCTarget which defers to the next bar's open.
Each event the engine computes the desired trade as
clamp(target, min_position, max_position) - current_position and takes it as a
marketable (taker) order against the displayed L1 book. A buy order sweeps the ask
side; a sell order sweeps the bid side. The fill pays taker_fee on the traded
notional. If the displayed size is smaller than the desired size, the overflow is
filled at ask + tick_size (buy) or bid - tick_size (sell). Positions mark to
the mid. If the target equals the current position, no order is placed and the
event is a mark-only step.
Inputs are (target_position, market_bid, market_ask, market_bid_size, market_ask_size). Outputs are the four standard backtest columns: 0 = equity
(cumulative dollar PnL), 1 = pnl (per event), 2 = position, and 3 = cost
(per event). A NaN in any market field skips the event and returns all-NaN
(nan_policy: ignore). backtest_report summarizes the
resulting equity curve.
The optional min_position and max_position parameters cap the inventory. The
target is clamped before the order size is computed, so the engine never carries a
position outside that range.
For a two-sided resting-order variant, see
BacktestL1Orders. When a trade feed is available,
BacktestTradesTarget uses real prints instead of
quote updates.
Limitations#
The fill model is optimistic: the order executes immediately at the displayed book
price with no queue or market impact beyond the tick_size overflow slippage.
Orders are counterfactual (zero market impact), so the book is unaffected by the
strategy's activity.
Examples#
Usage plot#
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from screamer import BacktestL1Target
rng = np.random.default_rng(6)
n = 400
t = np.arange(n)
mid = 100 + 2 * np.sin(2 * np.pi * t / n * 4) + np.cumsum(rng.standard_normal(n) * 0.1)
tick = 0.1
market_bid, market_ask = mid - tick, mid + tick
size = np.ones(n)
# a simple mean-reversion signal: go long when cheap, short when rich
signal = np.sign(100 - mid)
out = BacktestL1Target(taker_fee=0.0002, max_position=3.0, min_position=-3.0)(
signal, market_bid, market_ask, size, 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='position', line=dict(color='darkorange', shape='hv')),
row=3, col=1)
fig.update_layout(title='BacktestL1Target: immediate taker fills on each L1 quote update',
yaxis=dict(title='mid'), yaxis2=dict(title='equity ($)'),
yaxis3=dict(title='position'),
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()