mirror of
https://github.com/manifoldbt/manifoldbt.git
synced 2026-08-24 22:48:05 +00:00
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:
@@ -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()
|
||||
|
||||
@@ -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"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 -----------------------------------------
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
@@ -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.
Reference in New Issue
Block a user