mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-08-24 14:38:04 +00:00
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:
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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')}")
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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.
Reference in New Issue
Block a user