mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-08-24 14:38:04 +00:00
release: v0.2.0
This commit is contained in:
@@ -74,7 +74,7 @@ config = mbt.BacktestConfig(
|
|||||||
universe=[1, 2],
|
universe=[1, 2],
|
||||||
time_range_start=start,
|
time_range_start=start,
|
||||||
time_range_end=end,
|
time_range_end=end,
|
||||||
bar_interval=Interval.minutes(15),
|
bar_interval=Interval.days(1),
|
||||||
initial_capital=10_000,
|
initial_capital=10_000,
|
||||||
execution=mbt.ExecutionConfig(
|
execution=mbt.ExecutionConfig(
|
||||||
allow_short=True,
|
allow_short=True,
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"""Full Visualization Suite -- Bollinger Bands mean-reversion + all plots.
|
"""Full Visualization Suite -- RSI mean-reversion + all plots.
|
||||||
|
|
||||||
Strategy:
|
Strategy:
|
||||||
- Long when price touches lower band (oversold)
|
- Long when RSI < 30 (oversold)
|
||||||
- Short when price touches upper band (overbought)
|
- Short when RSI > 70 (overbought)
|
||||||
- Size proportional to distance from middle band
|
- Exit long when RSI > 50, exit short when RSI < 50
|
||||||
- Stop-loss 2%, take-profit 4%
|
|
||||||
|
|
||||||
Demonstrates every plotting function available in manifoldbt.
|
Demonstrates every plotting function available in manifoldbt.
|
||||||
|
|
||||||
@@ -14,48 +13,30 @@ Usage:
|
|||||||
import os
|
import os
|
||||||
import time
|
import time
|
||||||
import manifoldbt as mbt
|
import manifoldbt as mbt
|
||||||
from manifoldbt.indicators import close, bollinger_bands, ema
|
from manifoldbt.indicators import close, rsi
|
||||||
from manifoldbt.helpers import time_range, Slippage, Interval
|
from manifoldbt.helpers import time_range, Slippage, Interval
|
||||||
|
|
||||||
upper, middle, lower = bollinger_bands(close, period=20, num_std=2.0)
|
rsi_14 = rsi(close, 14)
|
||||||
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 -----------------------------------------------------------------
|
# -- Strategy -----------------------------------------------------------------
|
||||||
# Entry: touch lower band → long (only in uptrend), touch upper band → short (only in downtrend)
|
# Entry: RSI < 30 → long, RSI > 70 → short
|
||||||
# Exit: long exits at upper band, short exits at lower band
|
# Exit: RSI crosses 50
|
||||||
# Size flips to 0 at opposite band = exit
|
|
||||||
|
|
||||||
# Long signal: price near lower band + uptrend
|
long_entry = rsi_14 < mbt.lit(30.0)
|
||||||
long_entry = (zscore < -0.5) & is_uptrend
|
short_entry = rsi_14 > mbt.lit(70.0)
|
||||||
# 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(
|
signal = mbt.when(
|
||||||
long_entry, 1.0, # long
|
long_entry, 1.0,
|
||||||
mbt.when(short_entry, -1.0, 0.0), # short / flat
|
mbt.when(short_entry, -1.0, 0.0),
|
||||||
)
|
)
|
||||||
|
|
||||||
strategy = (
|
strategy = (
|
||||||
mbt.Strategy.create("Reversion_strategy")
|
mbt.Strategy.create("RSI_strategy")
|
||||||
.signal("upper", upper)
|
.signal("rsi14", rsi_14)
|
||||||
.signal("lower", lower)
|
|
||||||
.signal("ema100", trend_ema)
|
|
||||||
.signal("zscore", zscore)
|
|
||||||
.size(signal * 0.25)
|
.size(signal * 0.25)
|
||||||
.describe(
|
.describe(
|
||||||
"Bollinger Bands mean-reversion: long at lower band, short at upper band, "
|
"RSI(14) mean-reversion: long when RSI<30, short when RSI>70, "
|
||||||
"exit at opposite band. EMA(100) trend filter — no shorts in uptrend, "
|
"exit when RSI crosses 50."
|
||||||
"no longs in downtrend."
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,7 +58,7 @@ config = mbt.BacktestConfig(
|
|||||||
),
|
),
|
||||||
fees=mbt.FeeConfig.zero(),
|
fees=mbt.FeeConfig.zero(),
|
||||||
slippage=Slippage.fixed_bps(0),
|
slippage=Slippage.fixed_bps(0),
|
||||||
warmup_bars=25,
|
warmup_bars=20,
|
||||||
)
|
)
|
||||||
|
|
||||||
# -- Run ----------------------------------------------------------------------
|
# -- Run ----------------------------------------------------------------------
|
||||||
@@ -128,49 +109,46 @@ if __name__ == "__main__":
|
|||||||
mbt.plot.rolling_volatility(result, show=True)
|
mbt.plot.rolling_volatility(result, show=True)
|
||||||
|
|
||||||
# -- 6. Sweep heatmap 2D -------------------------------------------------
|
# -- 6. Sweep heatmap 2D -------------------------------------------------
|
||||||
# Sweep over BB period and num_std by rebuilding strategies
|
# Sweep over RSI period and oversold threshold
|
||||||
print("\nRunning 2D sweep (BB period × num_std)...")
|
print("\nRunning 2D sweep (RSI period × oversold threshold)...")
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
|
|
||||||
periods = [10, 15, 20, 30]
|
periods = [7, 10, 14, 21]
|
||||||
stds = [1.5, 2.0, 2.5, 3.0]
|
thresholds = [20, 25, 30, 35] # oversold level (overbought = 100 - threshold)
|
||||||
sweep_strategies = []
|
sweep_strategies = []
|
||||||
for p in periods:
|
for p in periods:
|
||||||
for ns in stds:
|
for thr in thresholds:
|
||||||
u, m, l = bollinger_bands(close, period=p, num_std=ns)
|
r14 = rsi(close, p)
|
||||||
bw = u - l
|
ob = mbt.lit(float(100 - thr))
|
||||||
zs = (close - m) / (bw + mbt.lit(1e-12))
|
os_ = mbt.lit(float(thr))
|
||||||
up = close > trend_ema
|
|
||||||
dn = close < trend_ema
|
|
||||||
sig = mbt.when(
|
sig = mbt.when(
|
||||||
(zs < -0.5) & up, 1.0,
|
r14 < os_, 1.0,
|
||||||
mbt.when((zs > 0.5) & dn, -1.0, 0.0),
|
mbt.when(r14 > ob, -1.0, 0.0),
|
||||||
)
|
)
|
||||||
s = (
|
s = (
|
||||||
mbt.Strategy.create(f"bb_p{p}_s{ns}")
|
mbt.Strategy.create(f"rsi_p{p}_t{thr}")
|
||||||
.signal("zscore", zs)
|
.signal("rsi", r14)
|
||||||
.size(sig * 0.25)
|
.size(sig * 0.05)
|
||||||
.stop_loss(pct=2.0)
|
.stop_loss(pct=2.0)
|
||||||
.take_profit(pct=4.0)
|
.take_profit(pct=4.0)
|
||||||
)
|
)
|
||||||
sweep_strategies.append(s)
|
sweep_strategies.append(s)
|
||||||
|
|
||||||
batch_results = mbt.run_batch_lite(sweep_strategies, config, store)
|
batch_results = mbt.run_batch_lite(sweep_strategies, config, store)
|
||||||
# Build a sweep_result dict compatible with heatmap_2d
|
|
||||||
metric_grid = []
|
metric_grid = []
|
||||||
idx = 0
|
idx = 0
|
||||||
for _ in periods:
|
for _ in periods:
|
||||||
row = []
|
row = []
|
||||||
for _ in stds:
|
for _ in thresholds:
|
||||||
r = batch_results[idx]
|
r = batch_results[idx]
|
||||||
row.append(r.metrics.get("sharpe", 0.0))
|
row.append(r.metrics.get("sharpe", 0.0))
|
||||||
idx += 1
|
idx += 1
|
||||||
metric_grid.append(row)
|
metric_grid.append(row)
|
||||||
|
|
||||||
sweep_result = {
|
sweep_result = {
|
||||||
"x_param": "num_std",
|
"x_param": "oversold_thr",
|
||||||
"y_param": "period",
|
"y_param": "period",
|
||||||
"x_values": stds,
|
"x_values": thresholds,
|
||||||
"y_values": periods,
|
"y_values": periods,
|
||||||
"metric": "sharpe",
|
"metric": "sharpe",
|
||||||
"metric_grid": metric_grid,
|
"metric_grid": metric_grid,
|
||||||
@@ -179,7 +157,6 @@ if __name__ == "__main__":
|
|||||||
mbt.plot.heatmap_2d(sweep_result, show=True)
|
mbt.plot.heatmap_2d(sweep_result, show=True)
|
||||||
|
|
||||||
# -- 7. Walk-forward validation -------------------------------------------
|
# -- 7. Walk-forward validation -------------------------------------------
|
||||||
# Manual walk-forward: split 2024 into 5 folds
|
|
||||||
print("\nRunning walk-forward (manual folds)...")
|
print("\nRunning walk-forward (manual folds)...")
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
|
|
||||||
@@ -197,22 +174,20 @@ if __name__ == "__main__":
|
|||||||
universe=ALL_SYMBOLS, time_range_start=ts, time_range_end=te,
|
universe=ALL_SYMBOLS, time_range_start=ts, time_range_end=te,
|
||||||
bar_interval=Interval.minutes(60), initial_capital=100_000,
|
bar_interval=Interval.minutes(60), initial_capital=100_000,
|
||||||
execution=config.execution, fees=config.fees,
|
execution=config.execution, fees=config.fees,
|
||||||
slippage=config.slippage, warmup_bars=25,
|
slippage=config.slippage, warmup_bars=20,
|
||||||
)
|
)
|
||||||
ts2, te2 = time_range(test_start, test_end)
|
ts2, te2 = time_range(test_start, test_end)
|
||||||
test_cfg = mbt.BacktestConfig(
|
test_cfg = mbt.BacktestConfig(
|
||||||
universe=ALL_SYMBOLS, time_range_start=ts2, time_range_end=te2,
|
universe=ALL_SYMBOLS, time_range_start=ts2, time_range_end=te2,
|
||||||
bar_interval=Interval.minutes(60), initial_capital=100_000,
|
bar_interval=Interval.minutes(60), initial_capital=100_000,
|
||||||
execution=config.execution, fees=config.fees,
|
execution=config.execution, fees=config.fees,
|
||||||
slippage=config.slippage, warmup_bars=25,
|
slippage=config.slippage, warmup_bars=20,
|
||||||
)
|
)
|
||||||
train_r = mbt.run(strategy, train_cfg, store)
|
train_r = mbt.run(strategy, train_cfg, store)
|
||||||
test_r = mbt.run(strategy, test_cfg, store)
|
test_r = mbt.run(strategy, test_cfg, store)
|
||||||
train_m = train_r.metrics
|
|
||||||
test_m = test_r.metrics
|
|
||||||
wf_folds.append({
|
wf_folds.append({
|
||||||
"train_metric": train_m.get("sharpe", 0.0),
|
"train_metric": train_r.metrics.get("sharpe", 0.0),
|
||||||
"test_metric": test_m.get("sharpe", 0.0),
|
"test_metric": test_r.metrics.get("sharpe", 0.0),
|
||||||
})
|
})
|
||||||
|
|
||||||
wf_result = {
|
wf_result = {
|
||||||
@@ -224,28 +199,23 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
# -- 8. Monte Carlo -------------------------------------------------------
|
# -- 8. Monte Carlo -------------------------------------------------------
|
||||||
print("\nRunning Monte Carlo (1000 paths)...")
|
print("\nRunning Monte Carlo (1000 paths)...")
|
||||||
mc_result = mbt.py_run_monte_carlo(result.raw, 1000, 42)
|
mbt.plot.monte_carlo(result, n_simulations=1000, seed=42, show=True)
|
||||||
mbt.plot.monte_carlo(mc_result, show=True)
|
|
||||||
|
|
||||||
# -- 9. Parameter stability -----------------------------------------------
|
# -- 9. Parameter stability -----------------------------------------------
|
||||||
print("\nRunning stability analysis (BB period)...")
|
print("\nRunning stability analysis (RSI period)...")
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
stability_periods = [10, 12, 15, 18, 20, 25, 30, 40]
|
stability_periods = [5, 7, 9, 11, 14, 18, 21, 28]
|
||||||
stability_metrics = []
|
stability_metrics = []
|
||||||
for p in stability_periods:
|
for p in stability_periods:
|
||||||
u, m, l = bollinger_bands(close, period=p, num_std=2.0)
|
r14 = rsi(close, p)
|
||||||
bw = u - l
|
|
||||||
zs = (close - m) / (bw + mbt.lit(1e-12))
|
|
||||||
up = close > trend_ema
|
|
||||||
dn = close < trend_ema
|
|
||||||
sig = mbt.when(
|
sig = mbt.when(
|
||||||
(zs < -0.5) & up, 1.0,
|
r14 < mbt.lit(30.0), 1.0,
|
||||||
mbt.when((zs > 0.5) & dn, -1.0, 0.0),
|
mbt.when(r14 > mbt.lit(70.0), -1.0, 0.0),
|
||||||
)
|
)
|
||||||
s = (
|
s = (
|
||||||
mbt.Strategy.create(f"bb_stab_{p}")
|
mbt.Strategy.create(f"rsi_stab_{p}")
|
||||||
.signal("zscore", zs)
|
.signal("rsi", r14)
|
||||||
.size(sig * 0.25)
|
.size(sig * 0.05)
|
||||||
.stop_loss(pct=2.0)
|
.stop_loss(pct=2.0)
|
||||||
.take_profit(pct=4.0)
|
.take_profit(pct=4.0)
|
||||||
)
|
)
|
||||||
@@ -254,7 +224,7 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
mean_m = float(np.mean(stability_metrics))
|
mean_m = float(np.mean(stability_metrics))
|
||||||
std_m = float(np.std(stability_metrics))
|
std_m = float(np.std(stability_metrics))
|
||||||
stab_result = {
|
stab_result = {
|
||||||
"param_name": "period",
|
"param_name": "period",
|
||||||
"metric": "sharpe",
|
"metric": "sharpe",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "manifoldbt"
|
name = "manifoldbt"
|
||||||
version = "0.1.3"
|
version = "0.2.0"
|
||||||
description = "Rust-powered backtesting engine for quantitative research"
|
description = "Rust-powered backtesting engine for quantitative research"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -275,12 +275,18 @@ def _resolve_store(config: BacktestConfig, store: DataStore) -> DataStore:
|
|||||||
if target == current:
|
if target == current:
|
||||||
return store
|
return store
|
||||||
|
|
||||||
|
# Try the target dataset; if it doesn't exist (no active version),
|
||||||
|
# fall back to bars_1m — the engine will resample automatically.
|
||||||
try:
|
try:
|
||||||
return DataStore(
|
candidate = DataStore(
|
||||||
data_root=store.data_root(),
|
data_root=store.data_root(),
|
||||||
metadata_db=store.metadata_db(),
|
metadata_db=store.metadata_db(),
|
||||||
dataset=target,
|
dataset=target,
|
||||||
)
|
)
|
||||||
|
# Verify the dataset actually has an active version
|
||||||
|
if candidate.active_version(target) is None:
|
||||||
|
return store
|
||||||
|
return candidate
|
||||||
except Exception:
|
except Exception:
|
||||||
return store
|
return store
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user