BacktestPriceTarget#
Description#
BacktestPriceTarget turns a target position on a value series (a price or mark)
into a costed profit-and-loss curve by reaching that target through taker liquidity.
The signal is the target position in units (its sign is long, short, or flat, any
magnitude); price is the mark (mid). Each bar the position moves to the signal via
a market order that crosses half of the fractional spread (a buy fills at
price * (1 + spread/2), a sell at price * (1 - spread/2)) and pays the
fractional fee on the traded notional. With the default spread = fee = 0 the
backtest is frictionless.
A target outside [min_position, max_position] is clamped to the nearest boundary
before the order is computed; the default bounds are unbounded so the behavior is
unchanged when the cap is not set.
It emits four positional columns: 0 = equity (cumulative dollar PnL), 1 = pnl
(per bar), 2 = position, and 3 = cost (per bar). It is causal: the signal at
t enters PnL only at t+1, through the position it sets, so a future signal
never changes a past row. A NaN signal or price skips the bar (an all-NaN row,
the position held across the gap; nan_policy: ignore).
The equity curve feeds the existing risk operators directly (dollar drawdown is
equity - CumMax(equity)), and backtest_report bundles
the common statistics (drawdown, cost, turnover, trades, Sharpe) in one call.
Examples#
Usage plot#
import numpy as np
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from screamer import BacktestPriceTarget, RollingMean
rng = np.random.default_rng(0)
n = 500
price = 100 + np.cumsum(rng.standard_normal(n) * 0.3)
# a trend signal: long when the fast average is above the slow one, else short
fast = RollingMean(10)(price)
slow = RollingMean(50)(price)
signal = np.sign(np.nan_to_num(fast - slow))
free = BacktestPriceTarget()(signal, price)[:, 0] # frictionless equity
costed = BacktestPriceTarget(spread=0.001, fee=0.0002)(signal, price)
eq = costed[:, 0]
dd = eq - np.maximum.accumulate(eq) # running dollar drawdown
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, row_heights=[0.6, 0.4],
vertical_spacing=0.08)
fig.add_trace(go.Scatter(y=free, name='equity (frictionless)',
line=dict(color='gray', dash='dot')), row=1, col=1)
fig.add_trace(go.Scatter(y=eq, name='equity (with cost)',
line=dict(color='steelblue')), row=1, col=1)
fig.add_trace(go.Scatter(y=dd, name='drawdown', line=dict(color='crimson'),
fill='tozeroy'), row=2, col=1)
fig.update_layout(title='BacktestPriceTarget: a trend signal turned into a costed equity curve',
yaxis=dict(title='equity ($)'), yaxis2=dict(title='drawdown ($)'),
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()