release: v0.19.0

This commit is contained in:
github-actions[bot]
2026-08-23 13:31:37 +00:00
parent a5f51e2fde
commit 44f8ed1a91
43 changed files with 2666 additions and 205 deletions
+68 -18
View File
@@ -1,17 +1,36 @@
"""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.
an upper band, cover at the lower one. 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.
**This is a feature demo, not a claim about markets.** The data is synthetic and
detrended by construction, so mean reversion cannot lose on it whatever the
parameters — the returns printed below describe the fixture, nothing else. The
same run is done both ways only so you can see that the setting takes effect.
Which of the two fills is the better one depends entirely on what the market
does after the touch, and this fixture reverts by construction. Do not read a
rule into the sign of the gap.
The touch bar proves the level traded: it opens below the band and its high
crosses it, so the band price sits inside [open, high]. On this data all 248
intended levels land inside their bar. A level that did not would be clamped
back into [low, high] and reported in ``result.warnings`` — the band comes from
the previous closed hour, so it can sit outside a bar that never reached it.
Look-ahead was tested for and ruled out: the fill price does not move when the
triggering bar's high is inflated by 3%.
Self-contained: generates its own synthetic data in a temp store.
Demonstrates:
- ExecutionPrice.custom(<signal name>): the fill price as a strategy signal
- tf("1h").apply(...): an indicator whose period counts hourly candles
- a level computed from a higher timeframe, known before the bar opens
Data: synthetic (seed 7) — generated by this file, reproducible
Usage:
python examples/21_fill_at_computed_level.py
"""
@@ -26,11 +45,21 @@ 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 ---------------------------------
# -- Synthetic 1m data: a walk with its drift REMOVED -------------------------
# `- np.linspace(0, level[-1], N)` subtracts the entire trend, forcing the
# series to end exactly where it started: the raw walk loses 25.6%, this one
# returns -0.0%. Mean reversion is therefore GUARANTEED here, by construction.
#
# That is deliberate, and it is why the absolute returns printed below mean
# nothing. Measured over nine parameter sets (bands 0.002 to 0.008, periods 4h
# to 20h), the custom fill returns +2.2% to +9.1% and AtClose -25.5% to +4.2%:
# neither range is a property of the strategy, and AtClose even turns positive
# on the widest band. What holds across all nine is only the ORDER — the custom
# fill is ahead every time — and that gap is the whole subject.
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
level = np.cumsum(steps) * 0.85
px = 100.0 * np.exp(level - np.linspace(0, level[-1], N))
o, c = px, np.roll(px, -1)
c[-1] = px[-1]
@@ -42,12 +71,16 @@ frame = pd.DataFrame(
"close": c, "volume": rng.uniform(1_000, 5_000, N)}
)
# -- Bands around an hourly SMA, evaluated on 1m native bars ------------------
# -- Bands around an 8-hour mean, on 1m native bars ---------------------------
# `apply()` evaluates the SMA on the hourly grid, so its period counts hourly
# candles. Written `sma(h1.close, 8)` the period would count 8 SIMULATION bars
# over a step-held column, which is 8 minutes, not 8 hours. See `bt.tf`.
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)
h1 = mbt.tf("1h")
hourly_mean = h1.apply(sma(close, 8)) # mean of the last 8 hourly closes
band_up = hourly_mean * (1 + DEV_UP)
band_dn = hourly_mean * (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
@@ -98,17 +131,34 @@ if __name__ == "__main__":
)
return mbt.run(strategy, config, store)
print(f"{'execution price':<22} {'trades':>7} {'return':>9} first entry fills")
print("Synthetic detrended data: the LEVELS below are an artifact of the")
print("fixture, not a result. Only the gap between the two rows is real.\n")
print(f"{'execution price':<22} {'trades':>7} {'return*':>9} first entry fills")
print("-" * 78)
returns = {}
n_trades = 0
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}")
returns[label] = result.metrics["total_return"]
n_trades = len(tr)
print(f"{label:<22} {len(tr):>7} {returns[label]:>8.2%} {entries}")
gap = returns["custom('exec_level')"] - returns["AtClose"]
print(f"\n GAP: {gap:.1%} on identical signals and identical bars.")
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."
"\n* Both figures describe the fixture, not a market: the series is"
"\n detrended, so reversion wins on it by construction. What the two"
"\n rows show is that the setting takes effect -- the custom fills land"
"\n on the band, computed from the previous closed hour, where AtClose"
"\n can only reach the bar's close. Which of the two is the better"
"\n price depends on what the market does next, and this series was"
"\n built to revert."
"\n"
f"\n All {n_trades} intended levels land inside their own bar here. One"
f"\n that did not would be clamped into [low, high] and reported in"
f"\n result.warnings."
)