mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-08-24 22:48:05 +00:00
release: v0.19.0
This commit is contained in:
@@ -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."
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user