release: v0.4.6

- Cross-exchange backtesting (Pro)
- Dict universe format (provider-based symbol resolution)
- Exogenous data support (register_exo + exo() expressions)
- Provider-based data layout (binance/1h/TICKER.arrow)
- Preload fix for provider layout
- Exo column resampling for multi-resolution
- Pro gate for cross-exchange (clean exit)
- ATR/ADX rolling SMA fix
- Precise mode hybrid fills
This commit is contained in:
Jimmy7892
2026-04-01 01:18:20 +02:00
parent 4039012c58
commit 6ba4691a02
23 changed files with 792 additions and 118 deletions
+4 -2
View File
@@ -42,7 +42,7 @@ strategy = (
start, end = time_range("2021-01-01", "2026-01-01")
config = mbt.BacktestConfig(
universe=[1], # symbol IDs (1=BTC, 2=ETH, etc.)
universe={"binance": ["BTC-USDT:perp"]},
time_range_start=start,
time_range_end=end,
bar_interval=Interval.minutes(1), # bar resolution
@@ -60,9 +60,11 @@ config = mbt.BacktestConfig(
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
t0 = perf_counter()
+4 -2
View File
@@ -39,7 +39,7 @@ strategy = (
start, end = time_range("2022-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1],
universe={"binance": ["BTC-USDT:perp"]},
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(1),
@@ -58,9 +58,11 @@ config = mbt.BacktestConfig(
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
+4 -2
View File
@@ -33,7 +33,7 @@ strategy = (
start, end = time_range("2021-01-01", "2026-01-01")
config = mbt.BacktestConfig(
universe=[1],
universe={"binance": ["BTC-USDT:perp"]},
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(12),
@@ -50,9 +50,11 @@ config = mbt.BacktestConfig(
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
t0 = time.perf_counter()
+8 -2
View File
@@ -35,7 +35,11 @@ strategy = (
start, end = time_range("2022-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1, 2, 3, 4, 5],
universe={
"binance": ["BTC-USDT:perp", "ETH-USDT:perp", "LTC-USDT:perp",
"DOT-USDT:perp", "XRP-USDT:perp"],
},
# Legacy equivalent: universe=[201, 202, 204, 206, 208]
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(12),
@@ -53,9 +57,11 @@ config = mbt.BacktestConfig(
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
t0 = time.perf_counter()
+4 -2
View File
@@ -71,7 +71,7 @@ strategy = (
start, end = time_range("2022-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1, 2],
universe={"binance": ["BTC-USDT:perp", "ETH-USDT:perp"]},
time_range_start=start,
time_range_end=end,
bar_interval=Interval.days(1),
@@ -88,9 +88,11 @@ config = mbt.BacktestConfig(
# -- Run -----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
t0 = time.perf_counter()
+5 -4
View File
@@ -15,7 +15,7 @@ from manifoldbt.indicators import close, kalman
from manifoldbt.helpers import time_range, Slippage, Interval
# -- Spread construction ------------------------------------------------------
pair_close = mbt.symbol_ref("ETHUSDT", "close")
pair_close = mbt.symbol_ref("binance:ETH-USDT:perp", "close")
ratio = close / (pair_close + mbt.lit(1e-12))
# -- Kalman equilibrium -------------------------------------------------------
@@ -41,7 +41,7 @@ strategy = (
start, end = time_range("2022-01-01", "2026-01-01")
config = mbt.BacktestConfig(
universe=[1, 2, 5], # BTC, ETH, BNB
universe={"binance": ["BTC-USDT:perp", "ETH-USDT:perp", "BNB-USDT:perp"]}, # BTC, ETH, BNB
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(24),
@@ -53,15 +53,16 @@ config = mbt.BacktestConfig(
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__), "..")
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
t0 = time.perf_counter()
+11 -5
View File
@@ -43,7 +43,11 @@ strategy = (
# -- Config -------------------------------------------------------------------
start, end = time_range("2021-01-01", "2026-01-01")
ALL_SYMBOLS = list(range(1, 23)) # 22 symbols: BTCUSDT to ARBUSDT
ALL_SYMBOLS = {"binance": [
"BTC-USDT:perp", "ETH-USDT:perp", "LTC-USDT:perp", "BNB-USDT:perp",
"DOT-USDT:perp", "XRP-USDT:perp", "ADA-USDT:perp", "LINK-USDT:perp",
"DOGE-USDT:perp", "AVAX-USDT:perp",
]}
config = mbt.BacktestConfig(
universe=ALL_SYMBOLS,
@@ -65,9 +69,11 @@ config = mbt.BacktestConfig(
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
os.makedirs(os.path.join(root, "output"), exist_ok=True)
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
# -- 1. Single backtest --------------------------------------------------
@@ -82,15 +88,15 @@ if __name__ == "__main__":
print("Generating tearsheet...")
mbt.plot.tearsheet(
result, show=True,
save=os.path.join(root, "output", "tearsheet.png"),
save=os.path.join(root, "output", "tearsheet.html"),
)
# -- 3. Summary 3-panel ---------------------------------------------------
mbt.plot.summary(result, show=True)
# -- 4. Candlestick chart (symbol_id=1 matches universe) ----------------
# -- 4. Candlestick chart (first symbol in universe) --------------------
mbt.plot.chart(
result, store, symbol_id=1,
result, store, symbol_id=201,
emas=[10, 25],
smas=[50],
n_bars=120,
+4 -2
View File
@@ -36,7 +36,7 @@ strategy = (
start, end = time_range("2021-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1],
universe={"binance": ["BTC-USDT:perp"]},
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(12),
@@ -53,9 +53,11 @@ config = mbt.BacktestConfig(
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
wf_config = {
+4 -2
View File
@@ -31,7 +31,7 @@ strategy = (
start, end = time_range("2021-01-01", "2026-01-01")
config = mbt.BacktestConfig(
universe=[1],
universe={"binance": ["BTC-USDT:perp"]},
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(1),
@@ -49,9 +49,11 @@ config = mbt.BacktestConfig(
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
fast_values = list(range(5, 1000, 5))
+4 -2
View File
@@ -31,7 +31,7 @@ strategy = (
start, end = time_range("2021-01-01", "2026-01-01")
config = mbt.BacktestConfig(
universe=[1],
universe={"binance": ["BTC-USDT:perp"]},
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(1),
@@ -48,9 +48,11 @@ config = mbt.BacktestConfig(
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
fast_values = list(range(5, 1000, 6))
+4 -2
View File
@@ -34,7 +34,7 @@ strategy = (
start, end = time_range("2021-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1],
universe={"binance": ["BTC-USDT:perp"]},
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(12),
@@ -51,9 +51,11 @@ config = mbt.BacktestConfig(
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
# 1. Run base backtest
+4 -2
View File
@@ -40,7 +40,7 @@ portfolio = (
start, end = time_range("2021-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1, 2],
universe={"binance": ["BTC-USDT:perp", "ETH-USDT:perp"]},
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(12),
@@ -57,9 +57,11 @@ config = mbt.BacktestConfig(
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
print(f"Running portfolio: {portfolio}\n")
+4 -2
View File
@@ -32,7 +32,7 @@ strategy = (
start, end = time_range("2022-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1],
universe={"binance": ["BTC-USDT:perp"]},
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(12),
@@ -49,9 +49,11 @@ config = mbt.BacktestConfig(
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
# -- 1. Look-ahead bias detection -----------------------------------------
+4 -2
View File
@@ -47,7 +47,7 @@ strategy = (
start, end = time_range("2022-01-01", "2025-01-01")
config = mbt.BacktestConfig(
universe=[1],
universe={"binance": ["BTC-USDT:perp"]},
time_range_start=start,
time_range_end=end,
bar_interval=Interval.hours(1),
@@ -68,9 +68,11 @@ config = mbt.BacktestConfig(
# -- Run ----------------------------------------------------------------------
if __name__ == "__main__":
root = os.path.join(os.path.dirname(__file__), "..")
data_root = os.path.abspath(os.path.join(root, "data"))
store = mbt.DataStore(
data_root=os.path.abspath(os.path.join(root, "data")),
data_root=data_root,
metadata_db=os.path.abspath(os.path.join(root, "metadata", "metadata.sqlite")),
arrow_dir=os.path.join(data_root, "mega"),
)
t0 = time.perf_counter()
+92
View File
@@ -0,0 +1,92 @@
"""Example 15: Cross-Exchange — Signal Binance, Execution dYdX.
Simple RSI mean-reversion:
- RSI computed on Binance BTC perp data
- Trades executed at dYdX BTC-USD prices
- Both loaded via universe dict no special config needed
Prerequisite:
Binance perp data (bars_1m/201.arrow) + dYdX data (dydx/1h/BTC-USD.arrow)
"""
import time
import manifoldbt as mbt
from manifoldbt.indicators import rsi, ema
from manifoldbt.expr import col, symbol_ref, lit, when
from manifoldbt.helpers import time_range, Interval, Slippage
# =============================================================================
# Signal — RSI + EMA from Binance BTC, applied to dYdX BTC
# All SymbolRef expressions must be named signals (for pass 2b resolution)
# =============================================================================
bn_btc_close = symbol_ref("binance:BTC-USDT:perp", "close")
bn_btc_rsi = rsi(bn_btc_close, 14)
bn_ema_fast = ema(bn_btc_close, 15)
bn_ema_slow = ema(bn_btc_close, 30)
trend_up = bn_ema_fast > bn_ema_slow
# Size references named signals only (no inline SymbolRef)
signal = when(
(col("trend") > lit(0.5)) & (col("bn_rsi") > lit(70.0)), 1.0,
when((col("trend") < lit(0.5)) & (col("bn_rsi") < lit(30.0)), -1.0,
0.0),
)
# =============================================================================
# Strategy
# =============================================================================
strategy = (
mbt.Strategy.create("cross_exchange_rsi")
.signal("bn_rsi", bn_btc_rsi)
.signal("trend", when(trend_up, 1.0, 0.0))
.size(signal)
.describe("Signal: Binance RSI | Execution: dYdX")
)
# =============================================================================
# Config — everything in universe
# =============================================================================
START, END = time_range("2024-02-01", "2026-03-01")
config = mbt.BacktestConfig(
universe={
"dydx": ["BTC-USD:perp"], # execution (fills here)
"binance": ["BTC-USDT:perp"], # signal source (via symbol_ref)
},
time_range_start=START,
time_range_end=END,
bar_interval=Interval.hours(6),
initial_capital=10_000,
warmup_bars=30,
execution=mbt.ExecutionConfig(signal_delay=1),
fees=mbt.FeeConfig(maker_fee_bps=1.0, taker_fee_bps=2.5),
slippage=Slippage.fixed_bps(2),
)
# =============================================================================
# Run
# =============================================================================
if __name__ == "__main__":
import os
root = os.path.dirname(os.path.abspath(__file__))
data_root = os.path.abspath(os.path.join(root, "..", "data"))
meta_db = os.path.join(root, "..", "metadata", "metadata.sqlite")
store = mbt.DataStore(
data_root=data_root,
metadata_db=meta_db,
arrow_dir=os.path.join(data_root, "mega"),
)
print("Running: cross_exchange_rsi")
print(" Signal: binance:BTC-USDT:perp (RSI + EMA)")
print(" Execution: dydx:BTC-USD:perp")
print()
t0 = time.perf_counter()
result = mbt.run(strategy, config, store)
elapsed = time.perf_counter() - t0
print(result.summary())
print(f"\nElapsed: {elapsed:.3f}s")
result.plot_equity(show=True)
+214
View File
@@ -0,0 +1,214 @@
"""Example 16: BTC-Hashrate Spread — Exogenous Data Strategy.
Thesis: Bitcoin hashrate is a proxy for miner commitment and network
security. When BTC price drops but hashrate holds (or rises), miners
are still profitable and the sell-off is likely transient buy the dip.
When price rises but hashrate lags, the rally lacks fundamental backing.
The strategy normalizes both BTC price and hashrate via EMA ratios
(price/EMA and hashrate/EMA), then computes a spread between the two.
A rolling z-score of the spread generates the signal: negative z means
price is cheap relative to hashrate (long), positive means expensive.
Exogenous data flow:
1. Fetch hashrate CSV (or use sample generator below)
2. Register via mbt.register_exo("hashrate", df)
3. Declare in BacktestConfig(exo_data=["hashrate"])
4. Access with exo("hashrate") in expressions
Prerequisite:
Binance BTC perp data + hashrate exo registered in data/mega/exo/
"""
import time
import numpy as np
import manifoldbt as mbt
from manifoldbt.indicators import ema, close
from manifoldbt.expr import col, exo, lit, when, hold
from manifoldbt.helpers import time_range, Interval, Slippage
# =============================================================================
# Parameters
# =============================================================================
SMOOTH = 30 # EMA period for normalization
ZSCORE_WINDOW = 90 # Rolling z-score lookback (days)
ENTRY_Z = -1.5 # Long when spread z < -1.5 (price cheap vs hashrate)
EXIT_Z = 0.0 # Exit when spread reverts to mean
SHORT_Z = 1.5 # Short when spread z > 1.5 (price expensive vs hashrate)
SIZE = 0.5 # Position size (fraction of capital)
# =============================================================================
# Indicators
# =============================================================================
# Normalize price: ratio to its own EMA (>1 = above trend, <1 = below)
price_ratio = close / ema(close, SMOOTH)
# Normalize hashrate the same way
hr = exo("hashrate")
hr_ratio = hr / ema(hr, SMOOTH)
# Spread: price_ratio - hr_ratio
# Positive = price running ahead of hashrate, negative = price lagging
spread = price_ratio - hr_ratio
# Z-score of the spread (rolling mean & std)
spread_z = spread.zscore(ZSCORE_WINDOW)
# =============================================================================
# Sizing
# =============================================================================
z = col("spread_z")
size = when(
z < lit(ENTRY_Z), lit(SIZE), # price cheap vs hashrate -> long
when(z > lit(SHORT_Z), -lit(SIZE), # price expensive vs hashrate -> short
when((z > lit(EXIT_Z)) & (z < lit(SHORT_Z)), 0.0, # neutral zone -> flat
hold())),
)
# =============================================================================
# Strategy
# =============================================================================
strategy = (
mbt.Strategy.create("hashrate_spread")
.signal("price_ratio", price_ratio)
.signal("hr_ratio", hr_ratio)
.signal("spread", spread)
.signal("spread_z", spread_z)
.size(size)
.describe("BTC vs Hashrate spread z-score mean-reversion")
)
# =============================================================================
# Config
# =============================================================================
START, END = time_range("2021-06-01", "2026-03-01")
config = mbt.BacktestConfig(
universe={"binance": ["BTC-USDT:perp"]},
time_range_start=START,
time_range_end=END,
bar_interval=Interval.days(1),
initial_capital=10_000,
warmup_bars=ZSCORE_WINDOW + SMOOTH,
exo_data=["hashrate"],
execution=mbt.ExecutionConfig(signal_delay=1, allow_short=True),
fees=mbt.FeeConfig.binance_perps(),
slippage=Slippage.fixed_bps(3),
)
# =============================================================================
# Hashrate data helper
# =============================================================================
def fetch_hashrate_csv(path: str = "hashrate.csv"):
"""Load hashrate from a CSV with columns: timestamp, hashrate.
Public sources (daily, free):
- https://api.blockchain.info/charts/hash-rate?timespan=5years&format=csv
- Glassnode, CoinMetrics (API key)
The CSV should have:
timestamp date or datetime (parsed automatically)
hashrate daily avg hashrate in EH/s (float)
"""
import pandas as pd
df = pd.read_csv(path, parse_dates=["timestamp"])
df["timestamp"] = pd.to_datetime(df["timestamp"], utc=True)
df = df.sort_values("timestamp").reset_index(drop=True)
return df
def generate_sample_hashrate(start="2020-01-01", end="2026-03-01"):
"""Generate synthetic hashrate data for testing.
Mimics the real BTC hashrate trajectory:
- Exponential growth trend (~50% annual)
- China ban crash (May-Jul 2021): -50%
- Recovery + continued growth
- Random noise (~5% daily vol)
"""
import pandas as pd
dates = pd.date_range(start, end, freq="D", tz="UTC")
n = len(dates)
# Base: exponential growth from ~120 EH/s to ~800 EH/s
t = np.arange(n) / 365.25
base = 120 * np.exp(0.40 * t) # ~50% annual growth
# China ban shock: May-Jul 2021
ban_start = pd.Timestamp("2021-05-15", tz="UTC")
ban_end = pd.Timestamp("2021-07-15", tz="UTC")
recovery_end = pd.Timestamp("2022-01-01", tz="UTC")
shock = np.ones(n)
for i, d in enumerate(dates):
if ban_start <= d <= ban_end:
# Linear drop to 50%
frac = (d - ban_start) / (ban_end - ban_start)
shock[i] = 1.0 - 0.50 * frac
elif ban_end < d < recovery_end:
# Recovery from 50% back to 100%
frac = (d - ban_end) / (recovery_end - ban_end)
shock[i] = 0.50 + 0.50 * frac
# Random noise (geometric brownian)
rng = np.random.default_rng(42)
noise = np.exp(np.cumsum(rng.normal(0, 0.02, n)))
noise /= noise[0]
hashrate = base * shock * noise
return pd.DataFrame({"timestamp": dates, "hashrate": hashrate})
# =============================================================================
# Run
# =============================================================================
if __name__ == "__main__":
import os
root = os.path.dirname(os.path.abspath(__file__))
data_root = os.path.abspath(os.path.join(root, "..", "data"))
meta_db = os.path.join(root, "..", "metadata", "metadata.sqlite")
store = mbt.DataStore(
data_root=data_root,
metadata_db=meta_db,
arrow_dir=os.path.join(data_root, "mega"),
)
# -- Register hashrate exo data -------------------------------------------
csv_path = os.path.join(root, "hashrate.csv")
if os.path.exists(csv_path):
print("Loading hashrate from CSV...")
hr_df = fetch_hashrate_csv(csv_path)
else:
print("No hashrate.csv found — generating synthetic data for demo...")
hr_df = generate_sample_hashrate()
mbt.register_exo("hashrate", hr_df, store=store)
print(f" Registered {len(hr_df)} hashrate data points")
print(f" Range: {hr_df['timestamp'].iloc[0]} -> {hr_df['timestamp'].iloc[-1]}")
print()
# -- Run backtest ---------------------------------------------------------
print("Running: hashrate_spread")
print(" Long when spread z < -1.5 (price cheap vs hashrate)")
print(" Short when spread z > +1.5 (price expensive vs hashrate)")
print()
t0 = time.perf_counter()
result = mbt.run(strategy, config, store)
elapsed = time.perf_counter() - t0
print(result.summary())
print(f"\nElapsed: {elapsed:.3f}s")
result.plot_equity(show=True)
Binary file not shown.