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()