BacktestOHLCOrders#

Description#

BacktestOHLCOrders runs a two-sided order-posting backtest on OHLC bars. Each bar the strategy posts a resting bid (bid_price, bid_size) and a resting ask (ask_price, ask_size). Both sides execute on the same bar (same-bar fill model, no deferral), so the position is whatever the bar's range allows.

Passive fills. A resting buy fills when the bar's low reaches the bid price. In "touch" mode (default) the fill triggers when low <= bid_price; in "breach" mode only when low < bid_price (the bar must trade strictly through). The fill quantity is min(bid_size, participation_ratio * bid_size, room), where room = max_position - current_position. The fill price is the bid price and pays maker_fee. The sell side mirrors on the high.

Market fills. A NaN bid or ask price is a market order (via market_limit): the buy fills at open + tick_size and the sell fills at open - tick_size, both paying taker_fee. An explicit MARKET constant (equal to +inf) also triggers this path.

Inventory bounds. Fills are capped at each step so the position stays in [min_position, max_position] (unbounded by default). Both sides may fill on the same bar when the bar's range is wide enough.

Inputs are (bid_price, bid_size, ask_price, ask_size, open, high, low, close). Outputs are the four standard backtest columns: 0 = equity (cumulative dollar PnL), 1 = pnl (per bar), 2 = position, 3 = cost (per bar). A NaN in any of the bar fields (open/high/low/close) skips the bar and returns all-NaN (nan_policy: ignore). A NaN bid_price or ask_price triggers a market order; a NaN or zero bid_size / ask_size suppresses that side entirely. backtest_report summarizes the resulting equity curve.

Limitations#

OHLC bars carry no intra-bar path, so the engine cannot distinguish whether the low preceded the high or vice versa. When both the bid and ask fill on the same bar the engine processes the buy first, then the sell; the ordering can affect the position and PnL when inventory bounds are active. Use BacktestL1Orders or BacktestL1TradesOrders with a tick-level or quote-level feed when intra-bar sequencing matters.

Examples#

Usage plot#

import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from screamer import BacktestOHLCOrders

rng = np.random.default_rng(7)
n = 300
t = np.arange(n)
# synthetic mean-reverting close price
close = 100 + 2 * np.sin(2 * np.pi * t / n * 6) + np.cumsum(rng.standard_normal(n) * 0.15)
o = close - rng.uniform(0.05, 0.15, n)
h = close + rng.uniform(0.2, 0.5, n)
l = close - rng.uniform(0.2, 0.5, n)

# post a bid 0.2 below close, ask 0.2 above; inventory bounded to +/-8
bid = close - 0.2
ask = close + 0.2
one = np.ones(n)
out = BacktestOHLCOrders(maker_fee=-0.0001, max_position=8.0, min_position=-8.0)(
    bid, one, ask, one, o, h, l, close)
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=close, name='close', 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='BacktestOHLCOrders: two-sided bar order poster, inventory capped at +/-8',
                  yaxis=dict(title='close'), 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()