Files
manifoldbt/examples/21_fill_at_computed_level.py
T

115 lines
4.4 KiB
Python
Raw Normal View History

2026-08-17 00:02:26 +00:00
"""Filling at a computed level — ExecutionPrice.custom(<signal name>).
A mean-reversion band strategy on native 1-minute bars: short at the touch of
an upper band around an hourly SMA, cover at the lower band. The engine always
knew how to COMPUTE the band; this example shows the fill landing ON it.
The touch bar itself proves the level traded: it opens below the band and its
high crosses it, so the band price sits inside [open, high]. Yet with
``AtClose`` the only reachable fill is the bar's close — on a mean-reverting
touch, systematically on the wrong side of the level. The same run is done
both ways so the difference is visible in one place.
Self-contained: generates its own synthetic data in a temp store.
Usage:
python examples/21_fill_at_computed_level.py
"""
import os
import tempfile
import numpy as np
import pandas as pd
import manifoldbt as mbt
from manifoldbt.indicators import close, high, low, open as open_px, sma
from manifoldbt.helpers import ExecutionPrice, Interval, Slippage
# -- Synthetic 1m data: a mean-reverting walk ---------------------------------
N = 30 * 1440 # 30 days of 1-minute bars
rng = np.random.default_rng(7)
steps = rng.normal(0.0, 0.0010, N)
level = np.cumsum(steps) * 0.85 # pull the walk back toward its mean
px = 100.0 * np.exp(level - np.linspace(0, level[-1], N))
o, c = px, np.roll(px, -1)
c[-1] = px[-1]
amp = np.abs(rng.normal(0.0, 0.0012, N))
ts = pd.date_range("2024-01-01", periods=N, freq="1min", tz="UTC")
frame = pd.DataFrame(
{"timestamp": ts, "open": o,
"high": np.maximum(o, c) * (1 + amp), "low": np.minimum(o, c) * (1 - amp),
"close": c, "volume": rng.uniform(1_000, 5_000, N)}
)
# -- Bands around an hourly SMA, evaluated on 1m native bars ------------------
DEV_UP, DEV_DN = 0.004, 0.003
h1 = mbt.tf("1h") # hourly columns, as of the last closed hour
band_up = sma(h1.close, 8) * (1 + DEV_UP)
band_dn = sma(h1.close, 8) * (1 - DEV_DN)
touch_up = high >= band_up # entry: short at the touch of the upper band
touch_dn = low <= band_dn # exit: cover at the touch of the lower band
target = mbt.when(touch_dn, 0.0, mbt.when(touch_up, -1.0))
# The level each fill should land on. The nesting mirrors the target's
# priority, and a bar that opens through a band fills at its open.
exec_level = mbt.when(
touch_dn, mbt.when(open_px <= band_dn, open_px, band_dn),
mbt.when(touch_up, mbt.when(open_px >= band_up, open_px, band_up), close),
)
strategy = (
mbt.Strategy.create("band_touch_short")
.signal("position", target)
.signal("exec_level", exec_level)
.size(target)
.stop_loss(pct=25.0)
)
# -- Run the same strategy both ways ------------------------------------------
if __name__ == "__main__":
root = tempfile.mkdtemp(prefix="mbt_example21_")
store = mbt.import_dataframe(
frame, symbol="SYNTH", symbol_id=1, interval="1m",
data_root=os.path.join(root, "data"),
metadata_db=os.path.join(root, "meta.sqlite"),
)
def run(execution_price):
config = mbt.BacktestConfig(
universe=[1],
time_range_start=0,
time_range_end=int(ts[-1].value) + 86_400_000_000_000,
bar_interval=Interval.minutes(1),
initial_capital=10_000,
execution=mbt.ExecutionConfig(
signal_delay=0,
execution_price=execution_price,
max_position_pct=0.4,
allow_short=True,
position_sizing_mode="FractionOfEquity",
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=60 * 10,
extra_timeframes={"1h": Interval.hours(1)},
)
return mbt.run(strategy, config, store)
print(f"{'execution price':<22} {'trades':>7} {'return':>9} first entry fills")
print("-" * 78)
for label, price in (("AtClose", "AtClose"),
("custom('exec_level')", ExecutionPrice.custom("exec_level"))):
result = run(price)
tr = result.trades_df()
entries = tr[tr["fill_price"] > 0].head(3)["fill_price"].round(4).tolist()
print(f"{label:<22} {len(tr):>7} {result.metrics['total_return']:>8.2%} {entries}")
print(
"\nSame signals, same bars: only WHERE the order fills changed. The"
"\ncustom fills land on the band level (inside the touch bar's range),"
"\nnot on its close. A fill outside [low, high] would be warned about."
)