pulse_btc_arbPulseX WBTC pool → HL BTC signal

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,372

For 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:

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.120

Funding ($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 notional

Costs 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