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
+5
View File
@@ -1,5 +1,10 @@
"""Strategy template — copy this file and modify.
Demonstrates:
- the minimal shape of a backtest: strategy, config, store, run
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/00_template.py
"""
+2
View File
@@ -8,6 +8,8 @@ Demonstrates:
- Diagnostics (lookahead, exposure stability, risk)
- result.summary() rich output
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/01_trend_following.py
"""
+2
View File
@@ -5,6 +5,8 @@ Demonstrates:
- Long and short positions
- Continuous sizing (signal * 0.25)
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/02_mean_reversion.py
"""
+2
View File
@@ -5,6 +5,8 @@ Demonstrates:
- Momentum via smoothed ROC on 12h bars
- Volatility-adjusted sizing
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/03_multi_asset_momentum.py
"""
+2
View File
@@ -11,6 +11,8 @@ The idea: fit a rolling OLS regression on price. When the slope is steep
and the R² is high (price moves in a straight line), we have a strong trend.
Size proportionally to slope strength * R² confidence.
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/04_linear_regression.py
"""
+2
View File
@@ -5,6 +5,8 @@ Demonstrates:
- Kalman filter for spread equilibrium
- Z-score mean-reversion sizing
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/05_stat_arb.py
"""
+7
View File
@@ -7,6 +7,13 @@ Strategy:
Demonstrates every plotting function available in manifoldbt.
Demonstrates:
- every plotting function in manifoldbt, on one result
- tearsheet, equity, drawdown, monthly and annual returns
- rolling Sharpe and volatility, returns histogram
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/06_full_visualization.py
"""
+2
View File
@@ -5,6 +5,8 @@ Demonstrates:
- param() for sweep-able parameters
- Walk-forward fold results inspection
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/07_walk_forward.py
"""
+2
View File
@@ -5,6 +5,8 @@ Demonstrates:
- run_sweep() for Cartesian grid search
- Heatmap visualization with mbt.plot.heatmap_2d()
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/08_sweep_2d_heatmap.py
"""
+2
View File
@@ -5,6 +5,8 @@ Demonstrates:
- run_sweep_lite() for fast parameter grid search
- 3D surface visualization with mbt.plot.surface_3d()
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/09_surface_3d.py
"""
+2
View File
@@ -5,6 +5,8 @@ Demonstrates:
- Monte Carlo fan chart visualization
- Risk metrics from simulated distributions
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/10_monte_carlo.py
"""
+2
View File
@@ -7,6 +7,8 @@ Demonstrates:
- Periodic rebalancing
- Per-strategy breakdown
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/11_portfolio.py
"""
+2
View File
@@ -5,6 +5,8 @@ Demonstrates:
- check_exposure_stability(): verify positions are consistent across time windows
- risk_check(): post-run risk metrics validation
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/12_diagnostics.py
"""
+3
View File
@@ -7,6 +7,9 @@ Demonstrates:
- CUDA GPU acceleration (device="cuda")
- All expressions compile to native Rust — full Rayon / CUDA parallelism
Data: none — this file simulates price paths, it does not backtest on any
series. The paths are its output, not its input.
Usage:
python examples/13_stochastic_simulation.py
"""
+9 -4
View File
@@ -10,6 +10,8 @@ Logic:
- 1h entry: RSI(14) < 35 during bullish regime → buy the dip
- Size: 50% of initial capital when conditions met, else flat
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/14_multi_timeframe.py
"""
@@ -23,9 +25,12 @@ from manifoldbt.helpers import time_range, Slippage, Interval
h12 = mbt.tf("12h") # references columns like "12h.close"
# -- Indicators ---------------------------------------------------------------
# Trend filter on 12-hour bars (forward-filled onto 1h grid)
trend_fast = ema(h12.close, 20)
trend_slow = ema(h12.close, 50)
# Trend filter on 12-hour bars. `apply()` evaluates the EMA on the 12h grid, so
# 20 and 50 count 12-HOUR candles. Written `ema(h12.close, 20)` they would count
# 20 rows of the 1h simulation grid over a step-held column -- under two 12h
# candles, not twenty. See `bt.tf`.
trend_fast = h12.apply(ema(close, 20))
trend_slow = h12.apply(ema(close, 50))
bullish = trend_fast > trend_slow
# Entry signal on 1-hour bars (native resolution)
@@ -59,7 +64,7 @@ config = mbt.BacktestConfig(
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=50,
warmup_bars=50 * 12, # 50 twelve-hour candles, counted in 1h simulation bars
extra_timeframes={
"12h": Interval.hours(12),
},
+11 -1
View File
@@ -1,4 +1,4 @@
"""Example 15: Cross-Exchange — Signal Binance, Execution dYdX.
"""Cross-exchange — signals on Binance, execution on dYdX.
Simple RSI mean-reversion:
- RSI computed on Binance BTC perp data
@@ -7,8 +7,18 @@ Simple RSI mean-reversion:
- Per-venue fees: each symbol is charged its own exchange's fee schedule
(see FeeConfig.multi_venue below)
Demonstrates:
- signals computed on one venue, orders filled on another
- a dict universe spanning two providers, with no special config
- per-venue fees: each symbol charged its own exchange's schedule
Data: shared store — real market data from `data/` (see examples/README.md)
Prerequisite:
Binance perp data (bars_1m/201.arrow) + dYdX data (dydx/1h/BTC-USD.arrow)
Usage:
python examples/15_cross_exchange.py
"""
import time
+13 -1
View File
@@ -1,4 +1,4 @@
"""Example 16: BTC-Hashrate Spread — Exogenous Data Strategy.
"""Exogenous data — a BTC/hashrate spread as a signal.
Thesis: Bitcoin hashrate is a proxy for miner commitment and network
security. When BTC price drops but hashrate holds (or rises), miners
@@ -10,6 +10,15 @@ The strategy normalizes both BTC price and hashrate via EMA ratios
A rolling z-score of the spread generates the signal: negative z means
price is cheap relative to hashrate (long), positive means expensive.
Demonstrates:
- an exogenous series ASOF-joined onto the bar grid
- a spread between two EMA-normalised series as a signal
- a rolling z-score turning that spread into a position
Data: shared store — real BTC bars from `data/`, plus a hashrate series
(fetched, or generated by the sample generator below when absent).
See examples/README.md.
Exogenous data flow:
1. Fetch hashrate CSV (or use sample generator below)
2. Register via mbt.register_exo("hashrate", df)
@@ -18,6 +27,9 @@ Exogenous data flow:
Prerequisite:
Binance BTC perp data + hashrate exo registered in data/mega/exo/
Usage:
python examples/16_hashrate_exogene.py
"""
import time
+8 -1
View File
@@ -1,4 +1,4 @@
"""Example 17: Per-Venue Fees — charge each symbol its own fee schedule.
"""Per-venue fees — charge each symbol its own fee schedule.
Real desks route different assets to different exchanges (or liquidity tiers),
each with its own maker/taker fees, funding column and borrow rate. ``FeeConfig``
@@ -9,6 +9,13 @@ Here a 4-asset momentum portfolio executes the majors (BTC, ETH) on a cheap
venue and the alts (XRP, DOT) on a more expensive one. Single-provider universe,
so it runs without Pro.
Demonstrates:
- FeeConfig.per_venue: named fee schedules in one book
- symbol_venue: which symbol trades where
- the cost gap between routing majors and alts differently
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/17_per_venue_fees.py
"""
+2
View File
@@ -8,6 +8,8 @@ Demonstrates:
The standard format is a header row + `timestamp,open,high,low,close,volume`
where timestamp is Unix milliseconds. MT4/MT5 exports are auto-detected.
Data: synthetic — a sample generated by this file, reproducible
Usage:
python examples/18_csv_import.py
"""
+84 -78
View File
@@ -1,109 +1,115 @@
"""Créer ses propres indicateurs (indicateurs absents de la base).
"""Writing your own indicators — the ones the library does not ship.
Demonstrates:
- an indicator as a plain function returning an `Expr`
- `scan` for stateful indicators no rolling window can express
- `param(...)` to make a custom indicator sweepable
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/19_custom_indicators.py
────────────────────────────────────────────────────────────────────────────
LE MODÈLE MENTAL
THE MENTAL MODEL
────────────────────────────────────────────────────────────────────────────
Un indicateur, ici, n'est RIEN d'autre qu'une fonction Python qui renvoie un
`Expr`. Un `Expr` est un *nœud dans un graphe de calcul* : quand vous écrivez
`(high + low) / 2`, aucune donnée n'est touchée — vous décrivez une opération.
Le graphe complet est ensuite compilé et évalué **en Rust**, en une passe,
vectorisé. C'est pour ça que vos indicateurs maison tournent à la vitesse des
indicateurs natifs : ils finissent dans le même moteur.
An indicator here is NOTHING but a Python function returning an `Expr`. An
`Expr` is a *node in a computation graph*: writing `(high + low) / 2` touches
no data — it describes an operation. The whole graph is then compiled and
evaluated **in Rust**, in one vectorised pass. That is why your own indicators
run at the speed of the built-in ones: they end up in the same engine.
Toute la lib `manifoldbt.indicators` est écrite comme ça (`sma` ==
`source.rolling_mean(period)`). Donc « ajouter un indicateur » = « écrire une
fonction qui compose des `Expr` ». Trois niveaux, du plus simple au plus rare.
The whole `manifoldbt.indicators` library is written this way (`sma` ==
`source.rolling_mean(period)`). So "adding an indicator" means "writing a
function that composes `Expr`s". Three levels, from the common to the rare.
"""
import os
from time import perf_counter
import manifoldbt as mbt
# Colonnes de base (ce sont déjà des Expr) + quelques helpers.
# Base columns (already Exprs) plus a few helpers.
from manifoldbt.indicators import open, high, low, close, volume, sma, rsi, ema
# Briques bas niveau : lit (constante), col (colonne par nom), when (if/else),
# scan/s (état récursif), param (paramètre balayable).
# Low-level bricks: lit (constant), col (column by name), when (if/else),
# scan/s (recursive state), param (sweepable parameter).
from manifoldbt.expr import lit, col, when, scan, s, param
from manifoldbt.helpers import time_range, Slippage, Interval
# ═══════════════════════════════════════════════════════════════════════════
# NIVEAU 1 — COMPOSER LES PRIMITIVES (99 % des cas)
# LEVEL 1 — COMPOSING PRIMITIVES (99% of cases)
# ═══════════════════════════════════════════════════════════════════════════
# On combine colonnes + opérateurs (+ - * /, > < >= & | ~) + méthodes d'Expr
# Combine columns + operators (+ - * /, > < >= & | ~) + Expr methods
# (rolling_mean/std/min/max/median, ewm_mean, zscore, pct_change, diff, lag,
# rsi, linreg_*, cross_above/below, cumsum, rank, ...). Chaque appel renvoie
# un Expr, donc tout se chaîne.
# rsi, linreg_*, cross_above/below, cumsum, rank, ...). Every call returns
# an Expr, so everything chains.
def awesome_oscillator(fast=5, slow=34):
"""Awesome Oscillator (Bill Williams) — ABSENT de la base.
"""Awesome Oscillator (Bill Williams) — NOT in the library.
AO = SMA(prix médian, 5) SMA(prix médian, 34), prix médian = (H+L)/2
AO = SMA(median price, 5) SMA(median price, 34), median = (H+L)/2
Momentum : positif = pression acheteuse, négatif = vendeuse.
Momentum: positive means buying pressure, negative means selling.
"""
median_price = (high + low) / 2 # Expr : opération sur 2 colonnes
return sma(median_price, fast) - sma(median_price, slow) # Expr résultat
median_price = (high + low) / 2 # Expr: an operation on 2 columns
return sma(median_price, fast) - sma(median_price, slow) # the result Expr
def dist_to_ma_pct(period=20):
"""Écart en % du prix à sa moyenne mobile — ABSENT de la base.
"""Distance from price to its moving average, in % — NOT in the library.
Négatif = le prix est SOUS sa moyenne (survendu) → brique idéale pour du
retour à la moyenne. Une seule ligne de composition.
Negative means the price sits BELOW its average (oversold), which makes it
a natural building block for mean reversion. One line of composition.
"""
ma = sma(close, period)
return (close - ma) / ma * 100.0
def intraday_range_pct():
"""Amplitude de la bougie en % du close — ABSENT de la base.
"""Bar range as a % of the close — NOT in the library.
Un proxy de volatilité instantané. Montre qu'on mélange librement les
colonnes OHLC.
An instant volatility proxy. Shows that OHLC columns mix freely.
"""
return (high - low) / close * 100.0
def rsi_zscore(period=14, lookback=365):
"""RSI standardisé : à quel point le RSI est extrême vs SA PROPRE histoire.
"""Standardised RSI: how extreme the RSI is against ITS OWN history.
Compose un indicateur natif (rsi) avec des stats roulantes. C'est
exactement le motif utilisé dans strategies/rsi_dynamic_alloc.py.
Composes a built-in indicator (rsi) with rolling statistics — the same
pattern used in strategies/rsi_dynamic_alloc.py.
"""
r = rsi(close, period)
return (r - r.rolling_mean(lookback)) / r.rolling_std(lookback)
# ═══════════════════════════════════════════════════════════════════════════
# NIVEAU 2 — `scan` : INDICATEURS À ÉTAT / RÉCURSIFS
# LEVEL 2 — `scan`: STATEFUL / RECURSIVE INDICATORS
# ═══════════════════════════════════════════════════════════════════════════
# Quand la valeur d'aujourd'hui dépend de celle d'HIER (récursion) et qu'aucun
# rolling ne suffit, on utilise `scan`. Il tourne comme une petite VM scalaire,
# entièrement en Rust (pas de callback Python par barre).
# When today's value depends on YESTERDAY's (recursion) and no rolling window
# suffices, reach for `scan`. It runs as a small scalar VM, entirely in Rust
# (no Python callback per bar).
#
# scan(state=..., update=..., output=...)
# • state : variables d'état + leur valeur initiale (1re ligne)
# • update : expressions évaluées à chaque barre, DANS L'ORDRE
# - s.prev("x") = valeur de "x" à la barre précédente
# - s.var("k") = valeur calculée plus tôt DANS LE MÊME pas
# - si un nom d'update == un nom d'état, on réécrit cet état
# • output : quelle variable émettre comme résultat
# • state : state variables and their initial value (first row)
# • update : expressions evaluated on every bar, IN ORDER
# - s.prev("x") = value of "x" on the previous bar
# - s.var("k") = value computed earlier WITHIN THE SAME step
# - an update name matching a state name rewrites that state
# • output : which variable to emit as the result
#
# Preuve que c'est puissant : le Kalman et le GARCH livrés sont écrits
# UNIQUEMENT avec scan (voir manifoldbt/indicators.py).
# Proof that it is enough: the shipped Kalman and GARCH are written with scan
# ALONE (see manifoldbt/indicators.py).
def up_streak():
"""Nombre de bougies HAUSSIÈRES consécutives — ABSENT de la base, et
impossible avec un simple rolling (il faut un compteur qui se réinitialise).
"""Count of consecutive UP bars — NOT in the library, and impossible with
a plain rolling window (it needs a counter that resets).
streak = streak_précédent + 1 si close > close(-1), sinon 0
streak = previous streak + 1 if close > close(-1), else 0
"""
is_up = close > close.lag(1) # Expr booléen (1.0 / 0.0) par barre
is_up = close > close.lag(1) # boolean Expr (1.0 / 0.0) per bar
return scan(
state={"n": lit(0.0)}, # compteur initialisé à 0
state={"n": lit(0.0)}, # counter seeded at 0
update={
# if is_up: prev(n) + 1 else: 0
"n": when(is_up, s.prev("n") + lit(1.0), lit(0.0)),
@@ -113,66 +119,66 @@ def up_streak():
def ema_from_scratch(alpha=0.1):
"""EMA « à la main » via scan — juste pour illustrer le mécanisme.
(L'EMA existe en natif : `ema(close, span)`. Ici c'est pédagogique.)
"""A hand-rolled EMA via scan — purely to show the mechanism.
(EMA is built in: `ema(close, span)`. This one is pedagogical.)
ema = alpha * close + (1 - alpha) * ema_précédent
ema = alpha * close + (1 - alpha) * previous ema
"""
return scan(
state={"ema": close}, # graine = 1er close
state={"ema": close}, # seeded with the first close
update={"ema": lit(alpha) * close + lit(1.0 - alpha) * s.prev("ema")},
output="ema",
)
# ═══════════════════════════════════════════════════════════════════════════
# NIVEAU 3 — LES LIMITES (À CONNAÎTRE)
# LEVEL 3 — THE LIMITS (WORTH KNOWING)
# ═══════════════════════════════════════════════════════════════════════════
# • PAS de callback Python par barre : `scan` s'exécute en Rust, on ne peut pas
# y injecter une fonction Python appelée sur chaque bougie (ce serait lent).
# Tant que la logique s'exprime avec Expr + when + scan, ça passe.
# • Un indicateur VRAIMENT nouveau, non exprimable ainsi, demande d'ajouter un
# variant `Expr` + son kernel côté Rust — chemin contributeur, pas utilisateur.
# • Données externes (hashrate, funding, sentiment…) : `mbt.register_exo(...)`
# puis `exo("nom")` renvoie un Expr utilisable comme n'importe quelle colonne.
# • NO Python callback per bar: `scan` runs in Rust, and you cannot inject a
# Python function called on every candle (it would be slow). As long as the
# logic expresses in Expr + when + scan, it works.
# • A GENUINELY new indicator, not expressible that way, needs a new `Expr`
# variant and its Rust kernel — the contributor path, not the user path.
# • External data (hashrate, funding, sentiment…): `mbt.register_exo(...)`,
# then `exo("name")` returns an Expr usable like any other column.
# ═══════════════════════════════════════════════════════════════════════════
# BONUS — RENDRE SON INDICATEUR BALAYABLE (sweep)
# BONUS — MAKING YOUR INDICATOR SWEEPABLE
# ═══════════════════════════════════════════════════════════════════════════
# Les périodes acceptent `param(...)` à la place d'un entier. Le moteur
# recompile alors une fois par combinaison et balaie la grille en parallèle,
# sans changer une ligne de l'indicateur :
# Periods accept `param(...)` in place of an integer. The engine then
# recompiles once per combination and sweeps the grid in parallel, without
# changing a line of the indicator:
#
# ao = awesome_oscillator(fast=param("fast"), slow=param("slow"))
# # puis, avec la grille passée séparément (l'indicateur ne change pas) :
# # then, with the grid passed separately (the indicator is unchanged):
# # batch = mbt.run_sweep_lite(
# # strategy,
# # {"fast": [3, 5, 8], "slow": [21, 34, 55]},
# # config, store,
# # )
#
# (voir examples/08_sweep_2d_heatmap.py pour le sweep complet.)
# (see examples/08_sweep_2d_heatmap.py for the full sweep.)
# ═══════════════════════════════════════════════════════════════════════════
# METTRE UN INDICATEUR MAISON DANS UNE STRATÉGIE + BACKTEST
# PUTTING A CUSTOM INDICATOR IN A STRATEGY AND BACKTESTING IT
# ═══════════════════════════════════════════════════════════════════════════
# On utilise `dist_to_ma_pct` (retour à la moyenne) : long quand le prix est
# nettement sous sa moyenne, on sort quand il l'a rejointe.
# Using `dist_to_ma_pct` (mean reversion): long when the price sits well below
# its average, out when it has caught up.
dist = dist_to_ma_pct(period=48) # notre indicateur maison
streak = up_streak() # et un second, pour l'exposer aussi
dist = dist_to_ma_pct(period=48) # our custom indicator
streak = up_streak() # a second one, exposed too
signal = when(dist < -5.0, 1.0, # >5 % sous la MM → achat du creux
when(dist > 0.0, 0.0)) # revenu à la MM → sortie, sinon hold
signal = when(dist < -5.0, 1.0, # >5% below the MA -> buy the dip
when(dist > 0.0, 0.0)) # back at the MA -> exit, else hold
strategy = (
mbt.Strategy.create("custom_indicator_demo")
.signal("dist_to_ma_%", dist) # .signal() = exposer pour le rapport
.signal("dist_to_ma_%", dist) # .signal() exposes it in the report
.signal("up_streak", streak)
.size(signal)
.describe("Retour à la moyenne piloté par un indicateur maison (écart à la MM)")
.describe("Mean reversion driven by a custom indicator (distance to the MA)")
)
# -- Config -------------------------------------------------------------------
@@ -185,9 +191,9 @@ config = mbt.BacktestConfig(
bar_interval=Interval.hours(1),
initial_capital=10_000,
execution=mbt.ExecutionConfig(allow_short=False, max_position_pct=1.0),
fees=mbt.FeeConfig.zero(), # sans frais, pour l'exemple
fees=mbt.FeeConfig.zero(), # fee-free, for the example
slippage=Slippage.fixed_bps(2),
warmup_bars=60, # >= la plus longue fenêtre utilisée
warmup_bars=60, # >= the longest window used
)
# -- Run ----------------------------------------------------------------------
+7
View File
@@ -8,6 +8,13 @@ the same signal four ways so the difference is visible in one place:
stop wait for a breakout, fill through the level (taker + gap)
limit on a signal rest on a level the DSL computes (here: 1 ATR below close)
Demonstrates:
- market, limit, stop and signal-priced entries, side by side
- passive fills (maker, no slippage) against aggressive ones
- an entry resting on a level the DSL computes
Data: shared store — real market data from `data/` (see examples/README.md)
Usage:
python examples/20_entry_orders.py
"""
+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."
)
+63
View File
@@ -0,0 +1,63 @@
"""Yahoo Finance -- stocks, ETFs, indices, FX and futures, free on all tiers.
Demonstrates:
- mbt.ingest(provider="yahoo") -- no API key, no license required
- Backtesting daily equity bars, exactly like a crypto connector
- Dividend-adjusted prices (same convention as yfinance's auto_adjust=True)
Yahoo imposes its own history limits: 1m bars go back 30 days, 1h about two
years, daily bars back to the listing date. Tickers follow Yahoo's own
notation: AAPL, SPY, ^GSPC (index), EURUSD=X (FX), ES=F (future),
BTC-USD (crypto), AIR.PA (Euronext).
Pass `dataset="raw"` to keep unadjusted quotes.
Data: self-contained (network) — ingested on each run from a free connector
Usage:
python examples/22_yahoo_equities.py
"""
import os
import tempfile
import manifoldbt as mbt
from manifoldbt.indicators import close, ema
from manifoldbt.helpers import time_range, Interval
# -- 1. Pull daily bars from Yahoo (free, all tiers) --------------------------
tmp = tempfile.mkdtemp()
store = mbt.ingest(
provider="yahoo",
symbol="AAPL",
symbol_id=1,
start="2020-01-01T00:00:00Z",
end="2024-01-01T00:00:00Z",
interval="1d",
asset_class="equity",
data_root=os.path.join(tmp, "data"),
metadata_db=os.path.join(tmp, "meta.sqlite"),
)
print("Ingested:", store.list_symbols())
# -- 2. Backtest on it like any other data ------------------------------------
strategy = (
mbt.Strategy.create("ema_cross")
.signal("fast", ema(close, 20))
.signal("slow", ema(close, 50))
.size(mbt.when(ema(close, 20) > ema(close, 50), 1.0, 0.0))
.describe("EMA(20/50) crossover on daily AAPL bars from Yahoo Finance")
)
start, end = time_range("2020-01-01", "2024-01-01")
config = mbt.BacktestConfig(
universe=[1],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.days(1),
initial_capital=10_000,
warmup_bars=60,
)
if __name__ == "__main__":
result = mbt.run(strategy, config, store)
print(result.summary())
+119
View File
@@ -0,0 +1,119 @@
"""Crypto options -- Deribit contracts that actually expire.
Demonstrates:
- mbt.ingest(provider="deribit", ...) -- no API key, expired contracts included
- Contract terms (strike, expiry, side, settlement) recorded alongside the bars
- Cash settlement at expiration, at intrinsic value
- config.option_underlyings -- which price series settles the contract
- Per-leg sizing with col("symbol_id"), for multi-leg structures
Why Deribit and not Binance: Deribit serves the history of contracts that have
already expired, which is the only data an option backtest can run on. Binance's
options API answers HTTP 400 for anything past its expiration date.
Three things worth knowing before reading the numbers:
- **Everything is in BTC.** A Deribit BTC option is quoted, margined and
settled in BTC, so `initial_capital` below is 10 BTC, not 10 dollars. The
payoff of a call is `max(0, S - K) / S` BTC per contract.
- **You choose the settlement reference.** Deribit settles against its own
`BTC_USD` index, whose ticker matches no series you can ingest.
`BTC-PERPETUAL` stands in for it here; the basis between the two is small
but real, and it is not modelled.
- **Positions are counted in units of the underlying.** On Deribit a contract
is one unit, so the two are the same thing. On a 100-multiplier listed
option, holding one contract means a position of 100.
Data: self-contained (network) — ingested on each run from a free connector
Usage:
python examples/23_deribit_options.py
"""
import os
import tempfile
import manifoldbt as mbt
from manifoldbt.indicators import col
from manifoldbt.helpers import time_range, Interval
# Two contracts that expired on 2025-06-27, and the series that settles them.
UNDERLYING, UNDERLYING_ID = "BTC-PERPETUAL", 1
CALL_100K, CALL_ID = "BTC-27JUN25-100000-C", 2
PUT_90K, PUT_ID = "BTC-27JUN25-90000-P", 3
START, END = "2025-05-01T00:00:00Z", "2025-07-01T00:00:00Z"
tmp = tempfile.mkdtemp()
common = dict(
start=START,
end=END,
interval="1d",
data_root=os.path.join(tmp, "data"),
metadata_db=os.path.join(tmp, "meta.sqlite"),
)
# -- 1. The settlement reference, then the contracts ---------------------------
store = mbt.ingest(
provider="deribit", symbol=UNDERLYING, symbol_id=UNDERLYING_ID,
asset_class="crypto_perp", **common
)
store = mbt.ingest(
provider="deribit", symbol=CALL_100K, symbol_id=CALL_ID,
asset_class="option", **common
)
store = mbt.ingest(
provider="deribit", symbol=PUT_90K, symbol_id=PUT_ID,
asset_class="option", **common
)
# The connector asked Deribit for the terms and the store kept them; nothing
# below is inferred from the instrument name.
for symbol_id, terms in sorted(store.option_contracts().items()):
print(f"{symbol_id}: {terms['option_type']} {terms['strike']:.0f}, "
f"settles {terms['settlement']}")
# -- 2. A risk reversal: long the 100k call, short the 90k put -----------------
# Legs are told apart by `col("symbol_id")`. Do NOT discriminate on price level
# (e.g. "close < 100"): a premium that crosses the threshold silently flips the
# leg to zero and the strategy closes its own position.
size = (
mbt.when(col("symbol_id") == float(CALL_ID), 1.0, 0.0)
+ mbt.when(col("symbol_id") == float(PUT_ID), -1.0, 0.0)
)
strategy = (
mbt.Strategy.create("risk_reversal")
.signal("leg", col("symbol_id"))
.size(size)
.describe("Long the 100k call, short the 90k put, both held to expiration")
)
start, end = time_range("2025-05-01", "2025-07-01")
config = mbt.BacktestConfig(
universe=[UNDERLYING_ID, CALL_ID, PUT_ID],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.days(1),
initial_capital=10.0, # 10 BTC
currency="BTC",
option_underlyings={CALL_ID: UNDERLYING_ID, PUT_ID: UNDERLYING_ID},
option_margin_model="deribit", # the short put pays margin
execution=mbt.ExecutionConfig(position_sizing_mode="Units", allow_short=True),
)
if __name__ == "__main__":
result = mbt.run(strategy, config, store)
trades = result.trades.to_pandas()
print("\nTrades on the option legs:")
print(
trades[trades.symbol_id != UNDERLYING_ID][
["symbol_id", "side", "quantity", "fill_price", "exit_reason"]
].to_string(index=False)
)
# exit_reason 5 is OptionExpiry: the venue settled the contract, the
# strategy did not sell it. The put settles at 0 because BTC finished far
# above its 90k strike.
print("\nFinal equity: %.6f BTC" % float(result.equity_curve[-1]))
if result.warnings:
print("Warnings:", result.warnings)
+101
View File
@@ -0,0 +1,101 @@
"""Option strategy -- a bull call spread, held to expiration.
Demonstrates:
- A two-leg option structure: long a low strike, short a higher one
- Per-leg sizing with col("symbol_id")
- A SHORT option paying margin under the venue's own formula
- Both legs cash-settled at expiry, which is what caps the payoff
The structure: buy the 100k call, sell the 110k call, same expiration. The
short leg pays for part of the long one, and in exchange it caps the gain at
the distance between the strikes. Classic, and the cheapest way to see the
engine settle two contracts on the same day with different outcomes.
**A currency trap worth knowing.** Every leg of a strategy has to be quoted in
the same currency, because the engine carries one cash balance. On Deribit an
option is quoted in BTC, but `BTC-PERPETUAL` is quoted in USD. So a covered
call (long the perpetual, short a call) would add dollars to bitcoin in a single
number and produce a meaningless equity curve. A spread has both legs in BTC,
which is why this example is a spread. The perpetual appears below only as the
settlement reference, never as a position.
Data: self-contained (network) — ingested on each run from a free connector
Usage:
python examples/24_option_spread.py
"""
import os
import tempfile
import manifoldbt as mbt
from manifoldbt.indicators import col
from manifoldbt.helpers import time_range, Interval
# Both legs expired on 2025-06-27, so the whole life of the trade is history.
UNDERLYING, UNDERLYING_ID = "BTC-PERPETUAL", 1
LONG_LEG, LONG_ID = "BTC-27JUN25-100000-C", 2
SHORT_LEG, SHORT_ID = "BTC-27JUN25-110000-C", 3
START, END = "2025-05-01T00:00:00Z", "2025-07-01T00:00:00Z"
tmp = tempfile.mkdtemp()
common = dict(
start=START,
end=END,
interval="1d",
data_root=os.path.join(tmp, "data"),
metadata_db=os.path.join(tmp, "meta.sqlite"),
)
store = mbt.ingest(
provider="deribit", symbol=UNDERLYING, symbol_id=UNDERLYING_ID,
asset_class="crypto_perp", **common
)
for symbol, symbol_id in ((LONG_LEG, LONG_ID), (SHORT_LEG, SHORT_ID)):
store = mbt.ingest(
provider="deribit", symbol=symbol, symbol_id=symbol_id,
asset_class="option", **common
)
# -- The strategy --------------------------------------------------------------
# Legs are told apart by symbol id. Never discriminate on price level: a premium
# crossing the threshold would flip its own leg to zero and close the position.
size = (
mbt.when(col("symbol_id") == float(LONG_ID), 1.0, 0.0) # buy the 100k call
+ mbt.when(col("symbol_id") == float(SHORT_ID), -1.0, 0.0) # sell the 110k call
)
strategy = (
mbt.Strategy.create("bull_call_spread")
.signal("leg", col("symbol_id"))
.size(size)
.describe("Long the 100k call, short the 110k call, held to expiration")
)
start, end = time_range("2025-05-01", "2025-07-01")
config = mbt.BacktestConfig(
universe=[UNDERLYING_ID, LONG_ID, SHORT_ID],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.days(1),
initial_capital=10.0, # 10 BTC: everything here is in BTC
currency="BTC",
option_underlyings={LONG_ID: UNDERLYING_ID, SHORT_ID: UNDERLYING_ID},
option_margin_model="deribit", # the short leg posts margin
execution=mbt.ExecutionConfig(position_sizing_mode="Units", allow_short=True),
)
if __name__ == "__main__":
result = mbt.run(strategy, config, store)
trades = result.trades.to_pandas()
names = {LONG_ID: "long 100k", SHORT_ID: "short 110k"}
print("\nTrades, by leg:")
for _, t in trades[trades.symbol_id != UNDERLYING_ID].iterrows():
what = "settled" if t.exit_reason == 5 else "traded"
print(f" {names[t.symbol_id]:<12} {what:<8} {t.quantity:>4.1f} @ {t.fill_price:.6f} BTC")
equity = float(result.equity_curve[-1])
print(f"\nFinal equity: {equity:.6f} BTC ({equity - 10.0:+.6f})")
# The short leg expiring worthless is what the spread pays for: it financed
# part of the long call, and capped the gain at the strike distance.
if result.warnings:
print("Warnings:", result.warnings)
+190
View File
@@ -0,0 +1,190 @@
"""Look-ahead — the leak the detector cannot see.
The classic beginner's leak, and the one that survives review: computing a
statistic over the *whole* dataset in the notebook, then using it as a strategy
parameter. Here it is the mean price of the entire asset, used from bar 0 —
where 749 of the 750 days it summarises are still in the future.
**There IS a leak in this strategy. It is deliberate.** The question the
example answers is not whether the leak exists, but which audit methods can
see it — and two of the three cannot. Read every "sees nothing" below as a
statement about the *method*, never as a clean bill of health for the
strategy.
What this example shows, in order:
1. the leak triples the return and doubles the Sharpe;
2. `detect_lookahead` sees nothing;
3. perturbing every future bar sees nothing either;
4. the one method that catches it: re-derive the parameter on truncated data.
**Why the first two are blind.** Both re-run the *same strategy* on shorter or
altered data. The mean is not recomputed by the engine: it is a number baked
into the strategy at research time. Re-running cannot see it, because the leak
already happened, in the notebook, before the backtest existed.
That is not a bug to fix in the detector, it is the shape of the problem. No
re-run method can audit a constant. The only defence is to treat every
parameter derived from data as part of the pipeline, and re-derive it on
whatever window you are testing.
Self-contained: generates its own synthetic data in a temp store.
Demonstrates:
- a parameter computed over the whole dataset, used from bar 0
- two audit methods that do not see it, and why
- the one that does: re-deriving the parameter per window
Data: synthetic (seed 11) — generated by this file, reproducible.
The fixture DETERMINES the outcome: see examples/README.md.
Usage:
python examples/25_lookahead_trap.py
"""
import os
import tempfile
import numpy as np
import pandas as pd
import manifoldbt as mbt
from manifoldbt.indicators import close
from manifoldbt.helpers import Interval, Slippage
# -- A mean-reverting series: exactly where knowing the mean is gold ----------
N_DAYS = 750
rng = np.random.default_rng(11)
level = np.cumsum(rng.normal(0.0, 0.018, N_DAYS))
px = 100.0 * np.exp(level - np.linspace(0, level[-1], N_DAYS))
o = px
c = np.roll(px, -1)
c[-1] = px[-1]
amp = np.abs(rng.normal(0.0, 0.004, N_DAYS))
frame = pd.DataFrame({
"timestamp": pd.date_range("2022-01-01", periods=N_DAYS, freq="1D", tz="UTC"),
"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_DAYS),
})
def store_of(df, tag):
root = tempfile.mkdtemp(prefix=f"mbt_ex25_{tag}_")
return mbt.import_dataframe(
df, symbol="SYNTH", symbol_id=1, interval="1d",
data_root=os.path.join(root, "data"),
metadata_db=os.path.join(root, "meta.sqlite"),
)
def config_of(df):
ts = pd.DatetimeIndex(df["timestamp"])
return mbt.BacktestConfig(
universe=[1],
time_range_start=int(ts[0].value),
time_range_end=int(ts[-1].value) + 86_400_000_000_000,
bar_interval=Interval.days(1),
initial_capital=10_000,
execution=mbt.ExecutionConfig(
signal_delay=1, max_position_pct=1.0,
allow_short=True, position_sizing_mode="FractionOfEquity",
),
slippage=Slippage.fixed_bps(0),
warmup_bars=0,
)
def leaky(mean_price):
"""THE LEAK: `mean_price` comes from `df.close.mean()` over everything."""
return (
mbt.Strategy.create("global_mean_leak")
.signal("edge", close)
.size(mbt.when(close < mean_price, 1.0, -1.0))
.describe("Long below the global mean, short above")
)
def causal(window):
"""The honest twin: a rolling mean knows only the past."""
return (
mbt.Strategy.create("rolling_mean")
.signal("edge", close)
.size(mbt.when(close < close.rolling_mean(window), 1.0, -1.0))
.describe("Long below the rolling mean")
)
def equity(result):
return np.array([float(x) for x in result.equity_curve])
# The two words this example turns on. "SEES NOTHING" describes the METHOD,
# never the strategy: the leak below is there whatever any audit reports.
BLIND, CAUGHT = "SEES NOTHING", "CATCHES THE LEAK"
if __name__ == "__main__":
global_mean = float(frame["close"].mean())
store = store_of(frame, "full")
# -- 1. The seduction -----------------------------------------------------
leaked = mbt.run(leaky(global_mean), config_of(frame), store)
honest = mbt.run(causal(60), config_of(frame), store)
print(f"Global mean over all {N_DAYS} days: {global_mean:.4f}")
print("(at bar 0, 749 of those days have not happened yet)")
print("\nThis strategy IS leaking. Below: which audits notice.\n")
for label, r in (("with the leak ", leaked), ("rolling mean ", honest)):
m = r.metrics
print(f" {label} return {100 * m['total_return']:+8.2f}% "
f"sharpe {m['sharpe']:5.2f} trades {r.trade_count}")
# -- 2. The detector -------------------------------------------------------
from manifoldbt.diagnostics import detect_lookahead
report = detect_lookahead(leaky(global_mean), config_of(frame), store, mode="all")
compared = sum(r.total_trades_overlap for r in report.reports)
verdict = BLIND if report.passed else CAUGHT
print(f"\n [1] detect_lookahead .......... {verdict}")
print(f" ({compared} trades compared, so the verdict is not empty)")
# -- 3. Perturbing the future ---------------------------------------------
split = 500
reference = equity(mbt.run(leaky(global_mean), config_of(frame), store))
corrupted = frame.copy()
tail = slice(split + 1, None)
factor = 1.0 + np.random.default_rng(99).uniform(-0.05, 0.05, N_DAYS - split - 1)
for col in ("open", "high", "low", "close"):
corrupted.loc[corrupted.index[tail], col] = corrupted[col].to_numpy()[tail] * factor
perturbed = equity(mbt.run(leaky(global_mean), config_of(frame),
store_of(corrupted, "pert")))
n = min(len(reference), len(perturbed), split + 1)
drift = float(np.abs(reference[:n] - perturbed[:n]).max())
verdict = CAUGHT if drift > 0 else BLIND
print(f" [2] future perturbation ....... {verdict}")
print(f" (past moved by {drift:.3e} while the future moved by "
f"{abs(reference[-1] - perturbed[-1]):.0f})")
# -- 4. The method that works ---------------------------------------------
# Treat the mean as what it is: a pipeline step, not a constant. A
# researcher standing at bar 500 only has the first 500 bars.
truncated = frame.iloc[:split + 1]
honest_mean = float(truncated["close"].mean())
with_future = equity(mbt.run(leaky(global_mean), config_of(truncated),
store_of(truncated, "t1")))
with_past = equity(mbt.run(leaky(honest_mean), config_of(truncated),
store_of(truncated, "t2")))
m = min(len(with_future), len(with_past))
gap = float(np.abs(with_future[:m] - with_past[:m]).max())
verdict = CAUGHT if gap > 0 else BLIND
print(f" [3] re-deriving the parameter . {verdict}")
print(f" mean from the whole set : {global_mean:.4f} -> "
f"final equity {with_future[-1]:,.0f}")
print(f" mean from the past only : {honest_mean:.4f} -> "
f"final equity {with_past[-1]:,.0f}")
print(f" same window, same bars, equity differs by {gap:.0f}")
print("\n Verdict: the leak is real, and only [3] found it. A")
print(" 'SEES NOTHING' is a statement about the method, not about")
print(" the strategy.")
+112
View File
@@ -0,0 +1,112 @@
# Examples
Each file demonstrates **one mechanism** of the engine and stops there. They are
documentation that happens to execute, not strategies: several would lose money
traded as written, and that is not a defect.
## The contract
Every example opens with the same four blocks, in this order:
```
"""One-line title — the mechanism, named.
Demonstrates:
- the two or three API surfaces the file exists to show
Data: <provenance>
Usage:
python examples/NN_name.py
"""
```
`Data:` is the block that matters most, and it is not optional. An example is
read by someone deciding whether to trust a number, so where the number came
from is part of the example.
## The three data provenances
**`Data: shared store`** — real market data from `data/`, ingested once and
reused. These examples need that store to exist (see below). Their results are
real in the sense that the prices are real; they are still not investment
advice, and none of the strategies is tuned.
**`Data: self-contained (network)`** — the file ingests what it needs on each
run, from a free connector. Runs on a fresh clone, needs a network.
**`Data: synthetic (seed N)`** — the file generates its own series. Always
reproducible, never a market claim.
## The rule on synthetic data
Synthetic data is legitimate and often the right choice: it isolates a
mechanism from market noise, and it lets an example run anywhere with no
dependency. It is used deliberately here, not as a fallback.
The one thing it must never do is let a reader mistake a fixture for a result.
So when the construction of the series **determines the outcome**, the file says
so before printing any figure, and marks the affected numbers. Two examples are
in that case:
| | What the fixture determines |
|---|---|
| `21_fill_at_computed_level.py` | the series is detrended, so mean reversion cannot lose on it — only the *gap* between two execution prices is meaningful |
| `25_lookahead_trap.py` | the leak is deliberate; the file exists to show which audits fail to see it |
A synthetic example whose fixture does **not** predetermine the outcome carries
no such warning, because there is nothing to warn about.
## The rule on performance
No example is selected, tuned, or presented for its returns. Where a figure
appears it illustrates the mechanism under discussion. If an example ever
produced a reproducible edge on real data, it would belong in a private
repository rather than in documentation.
## The shared store
Examples marked `Data: shared store` read `data/` and `metadata/`, which are not
in the repository. Populate them once:
```python
import manifoldbt as mbt
mbt.ingest(provider="binance", symbol="BTCUSDT", symbol_id=1,
start="2021-01-01T00:00:00Z", end="2025-01-01T00:00:00Z")
```
Add the symbols an example asks for; each names them in its `universe`. The
self-contained and synthetic examples need none of this.
## Index
| | Example | Mechanism | Data |
|---|---|---|---|
| 00 | `00_template.py` | the minimal shape of a backtest | shared store |
| 01 | `01_trend_following.py` | fluent builder, EMA, stop-loss, diagnostics | shared store |
| 02 | `02_mean_reversion.py` | bands, z-score, conditional sizing | shared store |
| 03 | `03_multi_asset_momentum.py` | cross-sectional ranking over a universe | shared store |
| 04 | `04_linear_regression.py` | rolling regression as a signal | shared store |
| 05 | `05_stat_arb.py` | pair spread, cross-asset references | shared store |
| 06 | `06_full_visualization.py` | the whole plotting surface | shared store |
| 07 | `07_walk_forward.py` | fold geometries, out-of-sample selection | shared store |
| 08 | `08_sweep_2d_heatmap.py` | a two-parameter sweep, read as a surface | shared store |
| 09 | `09_surface_3d.py` | the same surface in three dimensions | shared store |
| 10 | `10_monte_carlo.py` | bootstrap resampling, rare-event metrics | shared store |
| 11 | `11_portfolio.py` | several strategies in one book, rebalancing | shared store |
| 12 | `12_diagnostics.py` | look-ahead, exposure, risk checks | shared store |
| 13 | `13_stochastic_simulation.py` | SDE paths from the expression DSL | synthetic |
| 14 | `14_multi_timeframe.py` | higher timeframes, and how periods count | shared store |
| 15 | `15_cross_exchange.py` | signal on one venue, execution on another | shared store |
| 16 | `16_hashrate_exogene.py` | an exogenous series joined onto the bars | shared store |
| 17 | `17_per_venue_fees.py` | per-venue fee schedules in one book | shared store |
| 18 | `18_csv_import.py` | CSV / MT4 / MT5 import | synthetic |
| 19 | `19_custom_indicators.py` | composing an indicator from primitives | shared store |
| 20 | `20_entry_orders.py` | limit, stop and market-if-touched entries | shared store |
| 21 | `21_fill_at_computed_level.py` | filling at a level computed in advance | synthetic ⚠ |
| 22 | `22_yahoo_equities.py` | stocks, ETFs, indices, FX via Yahoo | network |
| 23 | `23_deribit_options.py` | option contracts that expire and settle | network |
| 24 | `24_option_spread.py` | a two-leg option structure | network |
| 25 | `25_lookahead_trap.py` | the look-ahead no re-run can detect | synthetic ⚠ |
⚠ marks the two files whose fixture determines the outcome, as described above.