"""Pure cross-venue funding-arb math — faithful 1:1 port of foxhunt
scripts/surfer/cross_venue_funding.py (spreads + cmd_run booking). No I/O.

Price-neutral: for a coin liquid on >=2 venues, SHORT the highest-funding venue perp + LONG the
lowest-funding venue perp and collect the funding DIFFERENCE (max-min). Each venue's funding is already
normalized to a DAILY rate by the adapter (Binance ×24/interval, Bybit ×3, Hyperliquid ×24).

Constants match the original exactly (lines 25-31):
  * LIQ     = 10e6    require > $10M/day on BOTH legs (only venues passing this enter the spread)
  * MAXF    = 0.005   exclude any leg with |daily funding| > 50bp/day (distress/artifact, un-tradeable)
  * TOPK    = 10      book at most the top-K spreads
  * COST_RT = 0.0010  ~10bp round-trip; net books realized − turnover * (COST_RT/2)
  * HURDLE  = 0.0005  (display-only in the original scan; not used in select/book)
  * ENTRY   = 0.0010  hysteresis: only OPEN a fresh pair whose spread > 10bp/day
  * EXIT    = 0.0005  hysteresis: HOLD a held pair until its spread decays below 5bp/day

Original spread (spreads(), lines 90-96): per coin, keep only venues whose 24h volume > LIQ AND whose
|daily funding| <= MAXF; if >=2 such venues remain, spread = max(f) - min(f), short = argmax, long = argmin.

Original select (cmd_run, lines 124-135):
  cur  = {coin: spread}
  held = [c for c in prev if cur.get(c, 0.0) > EXIT]          # keep decaying winners
  for r in sorted(rows with spread > ENTRY, by -spread):       # open strong fresh
      if len(held) >= TOPK: break
      if r.coin not in held: held.append(r.coin)
  newpos = {c: 1/len(held)}

Original booking (cmd_run, lines 126, 136-137):
  realized = sum(w * cur.get(c, 0.0) for c, w in prev.items())  # carry on prior book at TODAY's spread
  turn     = sum(|newpos[c] - prev[c]| over union)
  net      = realized - turn * (COST_RT / 2)
"""
from __future__ import annotations

from typing import Any

LIQ = 10e6
MAXF = 0.005
TOPK = 10
COST_RT = 0.0010
HURDLE = 0.0005
ENTRY = 0.0010
EXIT = 0.0005


def compute_spreads(
    funding_by_venue: dict[str, dict[str, float]],
    volume_by_venue: dict[str, dict[str, float]],
    liq: float = LIQ,
    maxf: float = MAXF,
) -> list[dict[str, Any]]:
    """Per-coin cross-venue spread rows, sorted by spread descending (faithful to spreads(), lines 88-98).

    For each coin, keep only venues where 24h volume > `liq` and |daily funding| <= `maxf`; require >=2
    such venues; emit {coin, spread = max-min funding, short = argmax venue, long = argmin venue, f}."""
    rows: list[dict[str, Any]] = []
    for c, venue_f in funding_by_venue.items():
        venue_v = volume_by_venue.get(c, {})
        pts = {
            nm: f
            for nm, f in venue_f.items()
            if venue_v.get(nm, 0.0) > liq and abs(f) <= maxf
        }
        if len(pts) < 2:
            continue
        hi = max(pts, key=lambda k: pts[k])
        lo = min(pts, key=lambda k: pts[k])
        rows.append({"coin": c, "spread": pts[hi] - pts[lo], "short": hi, "long": lo, "f": pts})
    rows.sort(key=lambda r: -r["spread"])
    return rows


def select(
    cur: dict[str, float],
    prev: dict[str, float],
    entry: float = ENTRY,
    exit_: float = EXIT,
    topk: int = TOPK,
) -> list[str]:
    """Coins to hold today via ENTRY/EXIT hysteresis + TOPK cap (faithful to cmd_run lines 128-134).

    `cur` is {coin: today's spread}. Keep held coins whose spread is still > `exit_`; then open fresh
    coins whose spread > `entry` (sorted by spread desc) until `topk` total are held."""
    held = [c for c in prev if cur.get(c, 0.0) > exit_]
    fresh = sorted(
        (c for c, s in cur.items() if s > entry),
        key=lambda c: -cur[c],
    )
    for c in fresh:
        if len(held) >= topk:
            break
        if c not in held:
            held.append(c)
    return held


def book_return(
    prev: dict[str, float],
    cur: dict[str, float],
    newpos: dict[str, float],
    cost_rt: float = COST_RT,
) -> float:
    """Net realized carry: equal-weight realized spread on the PRIOR book at TODAY's spread `cur`,
    minus the round-trip cost on turnover between the prior and new books. Faithful to cmd_run
    (lines 126, 136-137): `net = realized - turn * (COST_RT / 2)`."""
    realized = sum(w * cur.get(c, 0.0) for c, w in prev.items())
    turnover = sum(
        abs(newpos.get(c, 0.0) - prev.get(c, 0.0))
        for c in set(newpos) | set(prev)
    )
    return realized - turnover * (cost_rt / 2)
