mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-08-24 22:48:05 +00:00
Python DSL, examples, docs, benchmarks, and tests. Rust engine distributed as pre-compiled wheel via PyPI.
282 lines
10 KiB
Python
282 lines
10 KiB
Python
"""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')}")
|