Initial commit: manifoldbt public repo

Python DSL, examples, docs, benchmarks, and tests.
Rust engine distributed as pre-compiled wheel via PyPI.
This commit is contained in:
Jimmy7892
2026-03-17 16:15:40 +01:00
commit 67ab17280b
51 changed files with 10227 additions and 0 deletions
+73
View File
@@ -0,0 +1,73 @@
"""Strategy template — copy this file and modify.
Usage:
python examples/00_template.py
"""
import os
from time import perf_counter
import manifoldbt as mbt
from manifoldbt.indicators import close
from manifoldbt.helpers import time_range, Slippage, Interval
# -- Indicators ---------------------------------------------------------------
# All 45+ indicators available: rsi, ema, sma, bollinger, macd, atr, etc.
# See: from manifoldbt.indicators import <tab> for full list
zscore = close.zscore(60)
# -- Strategy -----------------------------------------------------------------
# mbt.when(condition, value_if_true, value_if_false)
# - Omit 3rd arg → hold current position
# - Nest mbt.when() for multiple conditions
#
# Examples:
# signal = mbt.when(rsi < 30, 0.5, mbt.when(rsi > 70, 0.0))
# signal = mbt.when(fast_ema > slow_ema, 1.0, -1.0)
signal = mbt.when(zscore < -1.0, 1.0, # oversold → long
mbt.when(zscore > 1.0, 0.0)) # overbought → exit, else hold
strategy = (
mbt.Strategy.create("my_strategy")
.signal("zscore", zscore)
.size(signal)
.describe("Z-score mean reversion")
# .stop_loss(pct=3.0)
# .take_profit(pct=5.0)
# .trailing_stop(pct=2.0)
)
# -- Config -------------------------------------------------------------------
start, end = time_range("2021-01-01", "2026-01-01")
config = mbt.BacktestConfig(
universe=[1], # symbol IDs (1=BTC, 2=ETH, etc.)
time_range_start=start,
time_range_end=end,
bar_interval=Interval.minutes(1), # bar resolution
initial_capital=10_000,
execution=mbt.ExecutionConfig(
allow_short=False,
max_position_pct=1.0,
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=60,
output_resolution=Interval.hours(1), # Pro: sub-daily, Community: capped to daily
)
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
)
t0 = perf_counter()
result = mbt.run(strategy, config, store)
print(result.summary())
print(f"\nElapsed: {perf_counter() - t0:.2f}s")
mbt.plot.tearsheet(result, show=True)
+75
View File
@@ -0,0 +1,75 @@
"""Trend Following -- EMA crossover with stop-loss and dynamic sizing.
Demonstrates:
- Fluent Strategy builder
- EMA indicators
- Conditional sizing with when()
- Stop-loss via .stop_loss()
- Diagnostics (lookahead, exposure stability, risk)
- result.summary() rich output
Usage:
python examples/01_trend_following.py
"""
import os
import time
import manifoldbt as mbt
from manifoldbt.indicators import ema, close, volume
from manifoldbt.helpers import time_range, Slippage, Interval
# -- Indicators ---------------------------------------------------------------
fast = ema(close, 12)
slow = ema(close, 26)
trend = fast - slow # MACD-like spread
vol_ma = volume.rolling_mean(20) # average volume filter
# -- Strategy -----------------------------------------------------------------
strategy = (
mbt.Strategy.create("trend_following")
.signal("fast", fast)
.signal("slow", slow)
.signal("trend", trend)
.signal("vol_filter", volume > vol_ma) # only trade on above-average volume
.size(mbt.when((trend > 0.0) & (volume > vol_ma), 0.5, 0.0))
.stop_loss(pct=3.0)
.describe("EMA(12/26) crossover, volume filter, 3% stop-loss")
)
# -- Config -------------------------------------------------------------------
start, end = time_range("2022-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(12),
initial_capital=10_000,
execution=mbt.ExecutionConfig(
allow_short=False,
max_position_pct=0.5,
position_sizing_mode="FractionOfInitialCapital",
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=30,
)
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
)
# Backtest
t0 = time.perf_counter()
result = mbt.run(strategy, config, store)
elapsed = time.perf_counter() - t0
print(result.summary())
print(f"\nElapsed: {elapsed:.3f}s")
# Plot
mbt.plot.summary(result, show=True)
+64
View File
@@ -0,0 +1,64 @@
"""Mean Reversion -- EMA crossover long/short.
Demonstrates:
- EMA crossover signal
- Long and short positions
- Continuous sizing (signal * 0.25)
Usage:
python examples/02_mean_reversion.py
"""
import os
import time
import manifoldbt as mbt
from manifoldbt.indicators import close, ema
from manifoldbt.helpers import time_range, Slippage, Interval
# -- Indicators ---------------------------------------------------------------
fast = ema(close, 12)
slow = ema(close, 26)
# -- Strategy -----------------------------------------------------------------
signal = mbt.when(fast > slow, 1.0, -1.0)
strategy = (
mbt.Strategy.create("ema_crossover")
.signal("fast", fast)
.signal("slow", slow)
.size(signal * 0.25)
.describe("EMA 12/26 crossover")
)
# -- Config -------------------------------------------------------------------
start, end = time_range("2021-01-01", "2026-01-01")
config = mbt.BacktestConfig(
universe=[1],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(12),
initial_capital=10_000,
execution=mbt.ExecutionConfig(
allow_short=True,
max_position_pct=0.5,
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=30,
)
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
)
t0 = time.perf_counter()
result = mbt.run(strategy, config, store)
elapsed = time.perf_counter() - t0
print(result.summary())
print(f"\nElapsed: {elapsed:.3f}s")
mbt.plot.summary(result, show=True)
+67
View File
@@ -0,0 +1,67 @@
"""Multi-Asset Momentum -- relative strength across 5 assets.
Demonstrates:
- Multi-asset universe (5 symbols)
- Momentum via smoothed ROC on 12h bars
- Volatility-adjusted sizing
Usage:
python examples/03_multi_asset_momentum.py
"""
import os
import time
import manifoldbt as mbt
from manifoldbt.indicators import close, ema, roc, high, low
from manifoldbt.helpers import time_range, Slippage, Interval
# -- Indicators ---------------------------------------------------------------
mom = ema(roc(close, 14), 6) # 7-day momentum, smoothed
avg_range = (high - low).rolling_mean(14)
norm_vol = avg_range / (close + mbt.lit(1e-12)) # normalized volatility
safe_vol = mbt.when(norm_vol > 0.0005, norm_vol, 0.0005)
# -- Strategy -----------------------------------------------------------------
signal = mbt.when(mom > 0.0, mom / safe_vol, 0.0)
strategy = (
mbt.Strategy.create("multi_momentum")
.signal("momentum", mom)
.signal("norm_vol", norm_vol)
.size(signal * 0.01)
.describe("Multi-asset momentum with volatility-adjusted sizing")
)
# -- Config -------------------------------------------------------------------
start, end = time_range("2022-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1, 2, 3, 4, 5],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(12),
initial_capital=10_000,
execution=mbt.ExecutionConfig(
signal_delay=1,
max_position_pct=0.3,
allow_short=False,
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=25,
)
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
)
t0 = time.perf_counter()
result = mbt.run(strategy, config, store)
elapsed = time.perf_counter() - t0
print(result.summary())
print(f"\nElapsed: {elapsed:.3f}s")
mbt.plot.summary(result, show=True)
+102
View File
@@ -0,0 +1,102 @@
"""Linear Regression Trend -- regression-based trend detection with confidence bands.
Demonstrates:
- linreg_slope / linreg_value / linreg_r2 indicators
- Confidence-weighted sizing (R² as conviction filter)
- Multi-timeframe: slope on 4h window, trade on 15min bars
- Trailing stop for trend exits
- Bracket orders (stop-loss + take-profit)
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.
Usage:
python examples/04_linear_regression.py
"""
import os
import time
import manifoldbt as mbt
from manifoldbt.indicators import close, high, low, volume
from manifoldbt.helpers import time_range, Slippage, Interval
# -- Regression indicators ----------------------------------------------------
# Rolling linear regression over 16 bars (16 * 15min = 4h window)
window = 16
slope = close.linreg_slope(window) # price change per bar (trend direction)
fitted = close.linreg_value(window) # regression fitted value
r2 = close.linreg_r2(window) # goodness of fit (0=noise, 1=perfect line)
# Normalize slope by price to get a percentage rate
norm_slope = slope / (close + mbt.lit(1e-12))
# -- Volatility filter ---------------------------------------------------------
# ATR-like: average true range normalized by price
avg_range = (high - low).rolling_mean(window)
norm_vol = avg_range / (close + mbt.lit(1e-12))
# -- Signal construction -------------------------------------------------------
# Conviction = R² (0 to 1). Only trade when R² > 0.6 (strong linear trend)
has_conviction = r2 > 0.6
# Direction: positive slope = long, negative = short
# Magnitude: |normalized slope| / volatility = trend strength vs noise
trend_strength = norm_slope / (norm_vol + mbt.lit(1e-12))
# Final signal: direction * conviction, gated by R² threshold
# Clamp to [-1, 1] range via division by expected max
raw_signal = mbt.when(
has_conviction,
trend_strength * r2 * mbt.lit(0.1), # scale down
0.0,
)
# -- Strategy ------------------------------------------------------------------
strategy = (
mbt.Strategy.create("linreg_trend")
.signal("slope", norm_slope)
.signal("r2", r2)
.signal("fitted", fitted)
.signal("trend_strength", trend_strength)
.size(raw_signal)
.trailing_stop(pct=2.0)
.describe(
"Rolling OLS regression: trade strong linear trends (high R²), "
"size by slope strength * confidence, trailing stop exit"
)
)
# -- Config --------------------------------------------------------------------
start, end = time_range("2022-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1, 2],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.minutes(15),
initial_capital=10_000,
execution=mbt.ExecutionConfig(
allow_short=True,
max_position_pct=0.5,
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=20,
)
# -- Run -----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
)
t0 = time.perf_counter()
result = mbt.run(strategy, config, store)
elapsed = time.perf_counter() - t0
print(result.summary())
print(f"\nElapsed: {elapsed:.3f}s")
mbt.plot.summary(result, show=True)
+73
View File
@@ -0,0 +1,73 @@
"""Statistical Arbitrage -- spread z-score vs ETH anchor.
Demonstrates:
- symbol_ref() for cross-asset signals
- Kalman filter for spread equilibrium
- Z-score mean-reversion sizing
Usage:
python examples/05_stat_arb.py
"""
import os
import time
import manifoldbt as mbt
from manifoldbt.indicators import close, kalman
from manifoldbt.helpers import time_range, Slippage, Interval
# -- Spread construction ------------------------------------------------------
pair_close = mbt.symbol_ref("ETHUSDT", "close")
ratio = close / (pair_close + mbt.lit(1e-12))
# -- Kalman equilibrium -------------------------------------------------------
equilibrium = kalman(ratio, q=1e-4, r=1e-2)
spread = ratio - equilibrium
# -- Z-score signal -----------------------------------------------------------
spread_z = spread.zscore(28)
signal = -spread_z # mean-revert: short when z > 0, long when z < 0
# -- Strategy -----------------------------------------------------------------
strategy = (
mbt.Strategy.create("stat_arb")
.signal("pair_close", pair_close)
.signal("spread", spread)
.signal("spread_z", spread_z)
.signal("signal", signal)
.size(mbt.col("signal"))
.describe("Spread z-score mean reversion vs ETH")
)
# -- Config -------------------------------------------------------------------
start, end = time_range("2022-01-01", "2026-01-01")
config = mbt.BacktestConfig(
universe=[1, 2, 5], # BTC, ETH, BNB
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(24),
initial_capital=10_000,
execution=mbt.ExecutionConfig(
allow_short=True,
max_position_pct=5,
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=30,
symbol_names={"BTCUSDT": 1, "ETHUSDT": 2, "BNBUSDT": 5},
)
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
)
t0 = time.perf_counter()
result = mbt.run(strategy, config, store)
elapsed = time.perf_counter() - t0
print(result.summary())
print(f"\nElapsed: {elapsed:.3f}s")
mbt.plot.summary(result, show=True)
+281
View File
@@ -0,0 +1,281 @@
"""Full Visualization Suite -- Bollinger Bands mean-reversion + all plots.
Strategy:
- Long when price touches lower band (oversold)
- Short when price touches upper band (overbought)
- Size proportional to distance from middle band
- Stop-loss 2%, take-profit 4%
Demonstrates every plotting function available in manifoldbt.
Usage:
python examples/06_full_visualization.py
"""
import os
import time
import manifoldbt as mbt
from manifoldbt.indicators import close, bollinger_bands, ema
from manifoldbt.helpers import time_range, Slippage, Interval
upper, middle, lower = bollinger_bands(close, period=20, num_std=2.0)
trend_ema = ema(close, 100)
# Z-score: how far price is from the mean, normalized by band width
band_width = upper - lower
zscore = (close - middle) / (band_width + mbt.lit(1e-12))
# Trend filter: EMA(100) above close = downtrend (no longs), below = uptrend (no shorts)
is_uptrend = close > trend_ema
is_downtrend = close < trend_ema
# -- Strategy -----------------------------------------------------------------
# Entry: touch lower band → long (only in uptrend), touch upper band → short (only in downtrend)
# Exit: long exits at upper band, short exits at lower band
# Size flips to 0 at opposite band = exit
# Long signal: price near lower band + uptrend
long_entry = (zscore < -0.5) & is_uptrend
# Short signal: price near upper band + downtrend
short_entry = (zscore > 0.5) & is_downtrend
# Long exits at upper band (zscore > 0.5), short exits at lower band (zscore < -0.5)
# When neither entry nor in opposite-band exit zone → flat (0)
signal = mbt.when(
long_entry, 1.0, # long
mbt.when(short_entry, -1.0, 0.0), # short / flat
)
strategy = (
mbt.Strategy.create("Reversion_strategy")
.signal("upper", upper)
.signal("lower", lower)
.signal("ema100", trend_ema)
.signal("zscore", zscore)
.size(signal * 0.25)
.describe(
"Bollinger Bands mean-reversion: long at lower band, short at upper band, "
"exit at opposite band. EMA(100) trend filter — no shorts in uptrend, "
"no longs in downtrend."
)
)
# -- Config -------------------------------------------------------------------
start, end = time_range("2021-01-01", "2026-01-01")
ALL_SYMBOLS = list(range(1, 23)) # 22 symbols: BTCUSDT to ARBUSDT
config = mbt.BacktestConfig(
universe=ALL_SYMBOLS,
time_range_start=start,
time_range_end=end,
bar_interval=Interval.minutes(120),
initial_capital=100_000,
execution=mbt.ExecutionConfig(
allow_short=True,
max_position_pct=0.5,
position_sizing_mode="FractionOfInitialCapital",
),
fees=mbt.FeeConfig.zero(),
slippage=Slippage.fixed_bps(0),
warmup_bars=25,
)
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
os.makedirs(os.path.join(root, "output"), exist_ok=True)
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
)
# -- 1. Single backtest --------------------------------------------------
print("Running backtest...")
t0 = time.perf_counter()
result = mbt.run(strategy, config, store)
elapsed = time.perf_counter() - t0
print(result.summary())
print(f"Elapsed: {elapsed:.3f}s\n")
# -- 2. Tearsheet (3 figures: overview, returns, rolling) ---------------
print("Generating tearsheet...")
mbt.plot.tearsheet(
result, show=True,
save=os.path.join(root, "output", "tearsheet.png"),
)
# -- 3. Summary 3-panel ---------------------------------------------------
mbt.plot.summary(result, show=True)
# -- 4. Candlestick chart (symbol_id=1 matches universe) ----------------
mbt.plot.chart(
result, store, symbol_id=1,
emas=[10, 25],
smas=[50],
n_bars=120,
interactive=False,
show=True,
)
# -- 5. Individual charts -------------------------------------------------
mbt.plot.equity(result, show=True)
mbt.plot.drawdown(result, show=True)
mbt.plot.monthly_returns(result, show=True)
mbt.plot.annual_returns(result, show=True)
mbt.plot.returns_histogram(result, show=True)
mbt.plot.var_chart(result, show=True)
mbt.plot.rolling_sharpe(result, show=True)
mbt.plot.rolling_volatility(result, show=True)
# -- 6. Sweep heatmap 2D -------------------------------------------------
# Sweep over BB period and num_std by rebuilding strategies
print("\nRunning 2D sweep (BB period × num_std)...")
t0 = time.perf_counter()
periods = [10, 15, 20, 30]
stds = [1.5, 2.0, 2.5, 3.0]
sweep_strategies = []
for p in periods:
for ns in stds:
u, m, l = bollinger_bands(close, period=p, num_std=ns)
bw = u - l
zs = (close - m) / (bw + mbt.lit(1e-12))
up = close > trend_ema
dn = close < trend_ema
sig = mbt.when(
(zs < -0.5) & up, 1.0,
mbt.when((zs > 0.5) & dn, -1.0, 0.0),
)
s = (
mbt.Strategy.create(f"bb_p{p}_s{ns}")
.signal("zscore", zs)
.size(sig * 0.25)
.stop_loss(pct=2.0)
.take_profit(pct=4.0)
)
sweep_strategies.append(s)
batch_results = mbt.run_batch_lite(sweep_strategies, config, store)
# Build a sweep_result dict compatible with heatmap_2d
metric_grid = []
idx = 0
for _ in periods:
row = []
for _ in stds:
r = batch_results[idx]
row.append(r.metrics.get("sharpe", 0.0))
idx += 1
metric_grid.append(row)
sweep_result = {
"x_param": "num_std",
"y_param": "period",
"x_values": stds,
"y_values": periods,
"metric": "sharpe",
"metric_grid": metric_grid,
}
print(f"Sweep done in {time.perf_counter() - t0:.1f}s")
mbt.plot.heatmap_2d(sweep_result, show=True)
# -- 7. Walk-forward validation -------------------------------------------
# Manual walk-forward: split 2024 into 5 folds
print("\nRunning walk-forward (manual folds)...")
t0 = time.perf_counter()
fold_months = [
("2024-01-01", "2024-07-01", "2024-07-01", "2024-09-01"),
("2024-01-01", "2024-08-01", "2024-08-01", "2024-10-01"),
("2024-01-01", "2024-09-01", "2024-09-01", "2024-11-01"),
("2024-01-01", "2024-10-01", "2024-10-01", "2024-12-01"),
("2024-01-01", "2024-11-01", "2024-11-01", "2025-01-01"),
]
wf_folds = []
for train_start, train_end, test_start, test_end in fold_months:
ts, te = time_range(train_start, train_end)
train_cfg = mbt.BacktestConfig(
universe=ALL_SYMBOLS, time_range_start=ts, time_range_end=te,
bar_interval=Interval.minutes(60), initial_capital=100_000,
execution=config.execution, fees=config.fees,
slippage=config.slippage, warmup_bars=25,
)
ts2, te2 = time_range(test_start, test_end)
test_cfg = mbt.BacktestConfig(
universe=ALL_SYMBOLS, time_range_start=ts2, time_range_end=te2,
bar_interval=Interval.minutes(60), initial_capital=100_000,
execution=config.execution, fees=config.fees,
slippage=config.slippage, warmup_bars=25,
)
train_r = mbt.run(strategy, train_cfg, store)
test_r = mbt.run(strategy, test_cfg, store)
train_m = train_r.metrics
test_m = test_r.metrics
wf_folds.append({
"train_metric": train_m.get("sharpe", 0.0),
"test_metric": test_m.get("sharpe", 0.0),
})
wf_result = {
"metric": "sharpe",
"folds": wf_folds,
}
print(f"Walk-forward done in {time.perf_counter() - t0:.1f}s")
mbt.plot.walk_forward(wf_result, show=True)
# -- 8. Monte Carlo -------------------------------------------------------
print("\nRunning Monte Carlo (1000 paths)...")
mc_result = mbt.py_run_monte_carlo(result.raw, 1000, 42)
mbt.plot.monte_carlo(mc_result, show=True)
# -- 9. Parameter stability -----------------------------------------------
print("\nRunning stability analysis (BB period)...")
t0 = time.perf_counter()
stability_periods = [10, 12, 15, 18, 20, 25, 30, 40]
stability_metrics = []
for p in stability_periods:
u, m, l = bollinger_bands(close, period=p, num_std=2.0)
bw = u - l
zs = (close - m) / (bw + mbt.lit(1e-12))
up = close > trend_ema
dn = close < trend_ema
sig = mbt.when(
(zs < -0.5) & up, 1.0,
mbt.when((zs > 0.5) & dn, -1.0, 0.0),
)
s = (
mbt.Strategy.create(f"bb_stab_{p}")
.signal("zscore", zs)
.size(sig * 0.25)
.stop_loss(pct=2.0)
.take_profit(pct=4.0)
)
r = mbt.run(s, config, store)
stability_metrics.append(r.metrics.get("sharpe", 0.0))
import numpy as np
mean_m = float(np.mean(stability_metrics))
std_m = float(np.std(stability_metrics))
stab_result = {
"param_name": "period",
"metric": "sharpe",
"values": stability_periods,
"metric_values": stability_metrics,
"mean_metric": mean_m,
"std_metric": std_m,
"stability_score": 1.0 - (std_m / abs(mean_m)) if mean_m != 0 else 0.0,
}
print(f"Stability done in {time.perf_counter() - t0:.1f}s")
mbt.plot.stability(stab_result, show=True)
# -- 10. Research report (composite) --------------------------------------
print("\nGenerating research report...")
mbt.plot.research_report(
sweep_result=sweep_result,
wf_result=wf_result,
stability_result=stab_result,
show=True,
save=os.path.join(root, "output", "research.png"),
)
print("\nDone — all visualizations generated.")
print(f"PNGs saved to {os.path.join(root, 'output')}")
+89
View File
@@ -0,0 +1,89 @@
"""Walk-Forward Optimization -- find robust parameters across time (Pro).
Demonstrates:
- run_walk_forward() with anchored method
- param() for sweep-able parameters
- Walk-forward fold results inspection
Usage:
python examples/07_walk_forward.py
"""
import os
import time
import manifoldbt as mbt
from manifoldbt.indicators import close, ema
from manifoldbt.helpers import time_range, Slippage, Interval
# -- Strategy with tunable parameters ----------------------------------------
# Indicators use concrete defaults; the Rust sweep engine replaces param()
# references at runtime with each grid value.
fast = ema(close, 12)
slow = ema(close, 26)
signal = mbt.when(fast > slow, 1.0, mbt.when(fast < slow, -1.0, 0.0))
strategy = (
mbt.Strategy.create("wfo_ema")
.signal("fast", fast)
.signal("slow", slow)
.size(signal * 0.25)
.param("fast", default=12, range=(5, 30))
.param("slow", default=26, range=(20, 60))
.describe("EMA crossover with walk-forward parameter optimization")
)
# -- Config -------------------------------------------------------------------
start, end = time_range("2021-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(12),
initial_capital=10_000,
execution=mbt.ExecutionConfig(
allow_short=True,
max_position_pct=0.5,
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=60,
)
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
)
wf_config = {
"method": "Anchored",
"n_splits": 5,
"train_ratio": 0.7,
"optimize_metric": "sharpe",
"param_grid": {
"fast": [5, 8, 12, 16, 20],
"slow": [25, 35, 50],
},
"max_parallelism": 0,
}
print("Running walk-forward optimization (Pro)...\n")
t0 = time.perf_counter()
result = mbt.run_walk_forward(strategy, wf_config, config, store)
elapsed = time.perf_counter() - t0
folds = result.get("folds", [])
best_params = result.get("best_params_per_fold", [])
for i, (fold, params) in enumerate(zip(folds, best_params)):
train = fold.get("train_metric", 0)
test = fold.get("test_metric", 0)
print(f" Fold {i+1}: train={train:+.3f} test={test:+.3f} params={params}")
print(f"\n{len(folds)} folds in {elapsed:.2f}s")
if folds:
mbt.plot.walk_forward({"metric": "sharpe", "folds": folds}, show=True)
+88
View File
@@ -0,0 +1,88 @@
"""2D Parameter Sweep Heatmap -- EMA crossover t-stat(alpha).
Demonstrates:
- param() in indicator periods (engine re-compiles per combo)
- run_sweep() for Cartesian grid search
- Heatmap visualization with mbt.plot.heatmap_2d()
Usage:
python examples/08_sweep_2d_heatmap.py
"""
import os
import time
import manifoldbt as mbt
from manifoldbt.indicators import close, ema
from manifoldbt.helpers import time_range, Slippage, Interval
# -- Strategy (single definition, param() in periods) ------------------------
fast = ema(close, mbt.param("fast"))
slow = ema(close, mbt.param("slow"))
signal = mbt.when(fast > slow, 0.25, mbt.when(fast < slow, -0.25, 0.0))
strategy = (
mbt.Strategy.create("ema_cross")
.signal("fast", fast)
.signal("slow", slow)
.size(signal)
)
# -- Config -------------------------------------------------------------------
start, end = time_range("2021-01-01", "2026-01-01")
config = mbt.BacktestConfig(
universe=[1],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(1),
initial_capital=10_000,
execution=mbt.ExecutionConfig(
allow_short=True,
max_position_pct=0.5,
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=80,
output_resolution=Interval.days(1),
)
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
)
fast_values = list(range(5, 1000, 5))
slow_values = list(range(10, 5000, 5))
print(f"Running 2D sweep ({len(fast_values)*len(slow_values)} combos)...")
t0 = time.perf_counter()
batch = mbt.run_sweep_lite(
strategy,
{"fast": fast_values, "slow": slow_values},
config,
store,
)
elapsed = time.perf_counter() - t0
# run_sweep_lite iterates sorted keys: fast (outer) × slow (inner)
# Reshape into grid[slow][fast] for heatmap (y=slow, x=fast)
metric_grid = [[0.0] * len(fast_values) for _ in slow_values]
idx = 0
for fi, f_val in enumerate(fast_values):
for si, s_val in enumerate(slow_values):
metric_grid[si][fi] = batch[idx].metrics.get("tstat_alpha", 0.0)
idx += 1
print(f"\n{len(batch)} combos in {elapsed:.2f}s")
mbt.plot.heatmap_2d({
"x_param": "fast",
"y_param": "slow",
"x_values": fast_values,
"y_values": slow_values,
"metric": "t-stat(alpha)",
"metric_grid": metric_grid,
}, show=True)
+85
View File
@@ -0,0 +1,85 @@
"""3D Surface Plot -- EMA crossover t-stat(alpha) surface.
Demonstrates:
- param() in indicator periods
- run_sweep_lite() for fast parameter grid search
- 3D surface visualization with mbt.plot.surface_3d()
Usage:
python examples/09_surface_3d.py
"""
import os
import time
import manifoldbt as mbt
from manifoldbt.indicators import close, ema
from manifoldbt.helpers import time_range, Slippage, Interval
# -- Strategy -----------------------------------------------------------------
fast = ema(close, mbt.param("fast"))
slow = ema(close, mbt.param("slow"))
signal = mbt.when(fast > slow, 0.25, mbt.when(fast < slow, -0.25, 0.0))
strategy = (
mbt.Strategy.create("ema_cross")
.signal("fast", fast)
.signal("slow", slow)
.size(signal)
)
# -- Config -------------------------------------------------------------------
start, end = time_range("2021-01-01", "2026-01-01")
config = mbt.BacktestConfig(
universe=[1],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(1),
initial_capital=10_000,
execution=mbt.ExecutionConfig(
allow_short=True,
max_position_pct=0.5,
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=80,
)
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
)
fast_values = list(range(5, 1000, 6))
slow_values = list(range(10, 5000, 6))
print(f"Running sweep ({len(fast_values)*len(slow_values)} combos)...")
t0 = time.perf_counter()
batch = mbt.run_sweep_lite(
strategy,
{"fast": fast_values, "slow": slow_values},
config,
store,
)
elapsed = time.perf_counter() - t0
metric_grid = [[0.0] * len(fast_values) for _ in slow_values]
idx = 0
for fi, f_val in enumerate(fast_values):
for si, s_val in enumerate(slow_values):
metric_grid[si][fi] = batch[idx].metrics.get("tstat_alpha", 0.0)
idx += 1
print(f"{len(batch)} combos in {elapsed:.2f}s")
mbt.plot.surface_3d({
"x_param": "fast",
"y_param": "slow",
"x_values": fast_values,
"y_values": slow_values,
"metric": "t-stat(alpha)",
"metric_grid": metric_grid,
}, show=True)
+67
View File
@@ -0,0 +1,67 @@
"""Monte Carlo Simulation -- confidence intervals on equity paths (Pro).
Demonstrates:
- py_run_monte_carlo() for bootstrapped equity paths
- Monte Carlo fan chart visualization
- Risk metrics from simulated distributions
Usage:
python examples/10_monte_carlo.py
"""
import os
import time
import manifoldbt as mbt
from manifoldbt.indicators import close, ema
from manifoldbt.helpers import time_range, Slippage, Interval
# -- Strategy -----------------------------------------------------------------
fast = ema(close, 12)
slow = ema(close, 26)
trend = fast - slow
strategy = (
mbt.Strategy.create("mc_ema_cross")
.signal("fast", fast)
.signal("slow", slow)
.signal("trend", trend)
.size(mbt.when(trend > 0.0, 0.5, 0.0))
.stop_loss(pct=3.0)
.describe("EMA crossover for Monte Carlo analysis")
)
# -- Config -------------------------------------------------------------------
start, end = time_range("2021-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(12),
initial_capital=10_000,
execution=mbt.ExecutionConfig(
allow_short=False,
max_position_pct=0.5,
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=30,
)
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
)
# 1. Run base backtest
print("Running base backtest...")
t0 = time.perf_counter()
result = mbt.run(strategy, config, store)
print(result.summary())
print(f"Elapsed: {time.perf_counter() - t0:.3f}s\n")
# 2. Monte Carlo fan chart
mbt.plot.monte_carlo(result, n_simulations=10000, seed=42, show=True)
+73
View File
@@ -0,0 +1,73 @@
"""Multi-Strategy Portfolio -- combine strategies with risk management.
Demonstrates:
- Portfolio builder with weighted strategies
- Importing strategies from separate files
- Risk rules (max drawdown, gross exposure cap)
- Periodic rebalancing
- Per-strategy breakdown
Usage:
python examples/11_portfolio.py
"""
import os
import sys
import time
# Allow importing sibling example files as modules
sys.path.insert(0, os.path.dirname(__file__))
import manifoldbt as mbt
from manifoldbt.helpers import time_range, Slippage, Interval
# -- Import strategies from dedicated files -----------------------------------
from importlib import import_module
strategy_a = import_module("01_trend_following").strategy
strategy_b = import_module("02_mean_reversion").strategy
# -- Portfolio ----------------------------------------------------------------
portfolio = (
mbt.Portfolio()
.strategy(strategy_a, weight=0.6)
.strategy(strategy_b, weight=0.4)
.max_drawdown(pct=20.0)
.max_gross_exposure(pct=150.0)
.rebalance_periodic(every_n_bars=30)
)
# -- Config -------------------------------------------------------------------
start, end = time_range("2021-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1, 2],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(12),
initial_capital=10_000,
execution=mbt.ExecutionConfig(
allow_short=True,
max_position_pct=0.5,
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=60,
)
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
)
print(f"Running portfolio: {portfolio}\n")
t0 = time.perf_counter()
result = mbt.run_portfolio(portfolio, config, store)
elapsed = time.perf_counter() - t0
print(result.summary())
print(f"\nElapsed: {elapsed:.3f}s")
mbt.plot.tearsheet(result, show=True)
+89
View File
@@ -0,0 +1,89 @@
"""Diagnostics -- look-ahead bias detection and exposure stability checks.
Demonstrates:
- detect_lookahead(): split-test for look-ahead bias
- check_exposure_stability(): verify positions are consistent across time windows
- risk_check(): post-run risk metrics validation
Usage:
python examples/12_diagnostics.py
"""
import os
import time
import manifoldbt as mbt
from manifoldbt.indicators import close, ema
from manifoldbt.helpers import time_range, Slippage, Interval
# -- Strategy -----------------------------------------------------------------
fast = ema(close, 12)
slow = ema(close, 50)
signal = mbt.when(fast > slow, 0.5, 0.0)
strategy = (
mbt.Strategy.create("ema_trend")
.signal("fast", fast)
.signal("slow", slow)
.size(signal)
.stop_loss(pct=3.0)
)
# -- Config -------------------------------------------------------------------
start, end = time_range("2022-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1],
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(12),
initial_capital=10_000,
execution=mbt.ExecutionConfig(
allow_short=False,
max_position_pct=0.5,
),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(2),
warmup_bars=60,
)
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
)
# -- 1. Look-ahead bias detection -----------------------------------------
# Splits the time range and compares trades from shorter runs against
# the full run. If trades differ, the strategy uses future data.
print("1. Look-ahead bias detection")
print("-" * 40)
t0 = time.perf_counter()
lookahead = mbt.diagnostics.detect_lookahead(strategy, config, store)
print(lookahead)
print(f" Elapsed: {time.perf_counter() - t0:.2f}s\n")
# -- 2. Exposure stability -------------------------------------------------
# Verifies that utilization and per-symbol exposure are identical
# across different time windows. Catches position sizing that leaks
# future data (e.g. z-score over the entire series).
print("2. Exposure stability")
print("-" * 40)
t0 = time.perf_counter()
stability = mbt.diagnostics.check_exposure_stability(strategy, config, store)
print(stability)
print(f" Elapsed: {time.perf_counter() - t0:.2f}s\n")
# -- 3. Backtest + risk check ----------------------------------------------
# Run the strategy, then validate risk metrics against thresholds.
print("3. Backtest + risk check")
print("-" * 40)
t0 = time.perf_counter()
result = mbt.run(strategy, config, store)
print(result.summary())
print(f" Elapsed: {time.perf_counter() - t0:.2f}s\n")
risk = mbt.diagnostics.risk_check(result)
print("Risk check:")
print(risk)
Binary file not shown.