How the math works, end to end
A complete trace from raw on-chain bytes + HL JSON → a net PnL number, using one real trade from a backtest run.
Layer 1 — Raw data we fetch
A. Hyperliquid (HL) — REST API
POST https://api.hyperliquid.xyz/info
{
"type": "candleSnapshot",
"req": { "coin": "BTC", "interval": "1m", "startTime": ..., "endTime": ... }
}
response (one row per minute):
{ "t": 1779490560000, "o": 75694.0, "h": 75699.0, "l": 75694.0, "c": 75698.0, "v": 0.9752 }t is in milliseconds; we divide by 1000 → unix seconds. Stored as-is in btc_hl_1m.
B. PulseChain — JSON-RPC eth_getLogs on the pair contract 0xff1efdf6… (pWBTC/DAI).
Topic Sync(uint112,uint112) — emitted after every reserve change:
block_number = 26597446 ts = 1779490585 # via eth_getBlock(blockNumber).timestamp data (64 B) = 0x [two uint256 packed] → reserve0 = 199521059 → reserve1 = 150382922579043721873020
Topic Swap(address,uint256,uint256,uint256,uint256,address) — emitted per swap:
amount0_in = 0 amount1_in = 8925811399659818152 amount0_out = 11808 amount1_out = 0
C. HL funding — separate paginated call:
POST /info {"type":"fundingHistory","coin":"BTC", ... }
→ [{ "time": ..., "fundingRate": 5.9325e-06, "premium": -0.0004525 }, ...]Layer 2 — Decode raw → human units
Pool meta (resolved at ingest): token_is_token0=1, btc_decimals=8, quote_decimals=18.
reserve_pbtc = 199521059 / 10^8 = 1.99521059 pBTC
reserve_quote = 150382922579043721873020 / 10^18 = 150382.9226 DAI
implied_pBTC_price = reserve_quote / reserve_pbtc
= 150382.92 / 1.99521 = 75371.95 DAI ≈ $75,372For the Swap event: someone paid 8925811399659818152 / 10^18 = 8.926 DAI to receive 11808 / 10^8 = 0.00011808 pBTC — a $9 buy.
Layer 3 — Aggregate Sync + Swap → 1m bars
pbtc_pulsex_1m stores one row per minute:
price= the last Sync's price in that minute (carry-forward if no Sync that minute).swap_volume_pbtc= Σ BTC-side amount of every Swap in the minute.
ts=1779490560 price=75371.95 reserve_pbtc=1.99521 reserve_quote=150382.92 swap_volume_pbtc=0.00011808
Layer 3.5 — Multi-pool blend (live)
For the live monitor we now read two pools instead of one and combine them by liquidity weight. The backtest still uses V1/DAI alone (it's the dataset that exists historically); live trading uses the blend, which refreshes more often because two pools see independent swap flow.
pool1_usd = reserve_DAI(V1) / reserve_pBTC(V1) # ≈ $300k pool, ~37min between swaps pool2_usd = reserve_WPLS(V2) × PLS_USD / reserve_pBTC(V2) # ≈ $62k pool, more frequent swaps w1 = 2 × reserve_DAI(V1) # USD-equivalent liquidity, both sides w2 = 2 × reserve_WPLS(V2) × PLS_USD blended_btc = (pool1_usd × w1 + pool2_usd × w2) / (w1 + w2)
PLS_USD itself comes from the WPLS/DAI pool on V2, read with one extra eth_call per tick. Falls back to pool1_usd if V2 is unreachable.
Layer 4 — Joined frame + basis + z-score
df = INNER JOIN btc_hl_1m ⋈ pbtc_pulsex_1m ON ts df["basis"] = (pbtc_price - btc_hl_close) / btc_hl_close df["basis_mean"] = basis.rolling(240, min_periods=60).mean() df["basis_std"] = basis.rolling(240, min_periods=60).std() df["z"] = (basis - basis_mean) / basis_std
Plug in the most recent bar:
btc_hl_close = 75698.00 pbtc_price = 75371.95 basis = (75371.95 - 75698.00) / 75698.00 = -0.004307 = -43.07 bps z (vs 4h) ≈ 0.41 (basis less negative than the 4h average)
Layer 5 — Decision logic
abs(z) < z_entry (3.0) → HOLD abs(basis_bps) < min_basis_bps (5) → HOLD btc_5m_realized_vol > 0.005 → HOLD (whipsaw) else: if z > 0 (pool > HL): open hl_long if z < 0 (pool < HL): open hl_short
The thesis: when basis blows out, arbitrageurs close the gap by trading HL. We front-run them on HL — the deeper, faster venue.
Layer 6 — Fill simulator (with realistic slippage)
move_pct = (btc_hl_close_exit - btc_hl_close_entry) / btc_hl_close_entry sign = +1 if hl_long else -1 gross_pnl = sign × move_pct × notional # realistic fee model fee_bps = p_fill_maker × maker_fee + (1 − p_fill_maker) × taker_fee slip_bps = p_fill_maker × maker_adverse + (1 − p_fill_maker) × taker_slip fees_usd = 2 × notional × fee_bps / 10000 slippage_usd = 2 × notional × slip_bps / 10000 funding_usd = funding.cost(notional, side, ts_open, ts_close) # walks hourly buckets net_pnl_usd = gross_pnl − fees_usd − slippage_usd − funding_usd
Defaults: p_fill_maker=0.8, taker_slip_bps=1.0, maker_adverse_bps=0.5.
Worked example — Trade #1 (run hl_signal-366ebc62)
Setup
ts_entry: 1775041500 = 2026-04-22 21:45:00 UTC ts_exit: 1775047500 = 2026-04-22 23:25:00 UTC (held 100 min) side: hl_short notional: $1,000
Signal context
ts hl_close pbtc_price basis_bps 1775041200 (-5m) 68576.00 68231.68 -50.21 1775041260 (-4m) 68576.00 68231.68 -50.21 1775041320 (-3m) 68576.00 68231.68 -50.21 1775041380 (-2m) 68576.00 68231.68 -50.21 1775041440 (-1m) 68576.00 68120.18 -66.47 1775041500 (NOW) 68576.00 67972.51 -88.00 ← basis blew out
Basis went from -50 → -88 bps in two minutes. After 4h-rolling z-normalization, |z| crossed 3.0. z < 0 (pool below HL) → bet on convergence → short HL.
Exit, 100 minutes later
ts hl_close pbtc_price basis_bps 1775047440 68677.00 67937.61 -107.66 1775047500 68341.00 68127.28 -31.27 ← HL gapped down $336, TP fired
HL crashed $336 in one bar; basis snapped from -107 → -31 bps. The convergence happened by HL dropping — exactly the thesis.
Gross PnL
HL move: (68341 - 68576) / 68576 = -0.003427 = -34.27 bps Short sign: -1 Gross PnL: -1 × -0.003427 × $1,000 = +$3.4269
Costs, itemized
Fees ($0.420)
Effective fee = 0.8 × 1.5bps + 0.2 × 4.5bps = 2.1 bps per leg Total fees = 2 × $1,000 × 2.1 / 10,000 = $0.420
Slippage ($0.120)
Effective slip = 0.8 × 0.5bps (maker adverse) + 0.2 × 1.0bps (taker half-spread)
= 0.6 bps per leg
Total slip = 2 × $1,000 × 0.6 / 10,000 = $0.120Funding ($0.005474, paid)
Position spans two hourly buckets; HL pays/charges at each top-of-hour. Bucket 21:00 (rate = -4.3524e-06 /hr) overlap = 1775044800 - 1775041500 = 3300s = 0.9167 hr cost = $1,000 × (-4.3524e-06) × 0.9167 × (-1) = +$0.003989 Bucket 22:00 (rate = -1.9792e-06 /hr) overlap = 1775047500 - 1775044800 = 2700s = 0.7500 hr cost = $1,000 × (-1.9792e-06) × 0.7500 × (-1) = +$0.001484 Total funding = +$0.005474 (rates were negative → short pays)
Net
Net PnL = +$3.4269 − $0.420 − $0.120 − $0.005474
= +$2.881
= +28.8 bps on $1,000 notionalCosts ate 15.9% of the gross edge. Scale this trade to $10k notional → +$28.81; to $100k → +$288 (assuming HL maker fills stay reliable at that size, which on BTC perp they do comfortably).
Multiply by ~3 trades/day × 70% win rate over 52 days → the +$105 headline on the run page.
Cost-by-cost contribution across the full 52d backtest
Σ gross PnL ≈ +$195 (all trades, signed by side) Σ fees = -$65 (2.1 bps × 2 sides × ~156 trades × $1k) Σ slippage = -$24 (0.6 bps × 2 sides × ~156 trades × $1k) Σ funding = +$0.65 (small net inflow — rates were marginally negative) Σ gas = $0 (no on-chain leg) ──────────────────────────────────────────── Σ net PnL ≈ +$105 on $1k notional
Sources of every constant
- HL fees:
HL_TAKER_FEE_BPS = 4.5,HL_MAKER_FEE_BPS = 1.5inconfig.py. Source: HL fee schedule. - Funding accrual: hourly buckets, prorated to held-seconds. Code in
exec_sim/funding.py. - Slippage model: bps per leg, blended by
p_fill_maker. Code inexec_sim/fill.py:simulate_hl_only. - Pool decimals + token0 ordering: resolved at ingest via on-chain
decimals()+token0(); stored inpbtc_pool. - Block timestamps:
eth_getBlock(n).timestamp— only fetched for blocks with Sync/Swap events (cheap).