Delta-neutral funding carry across HL + Extended + Pacifica + Nado. Per coin: LONG the
lowest-funding venue, SHORT the highest → collect the funding SPREAD, price-neutral, all perps.
Validated strategy: project_combo_deep_test_2026_06_25 (cross-sectional + multi-venue-routed,
edge ~3.5% conserv / ~8% observed ON NOTIONAL, cluster-t 5.16–7.77, 1 regime — not multi-cycle OOS).
carry.py— COORDINATOR (runs locally). scan → rank cross-section → route legs around caps → size by basis-tail-budgetL = risk_budget / |basis_shock|(default 33%/3.3% = 10×). LOGS the target book. Places NO orders.rpc_read.py— READ-ONLY RPC piped over SSH into each venue's venv; builds that repo's tested client viamain._build_client(Settings.from_env()), forcesDRY_RUN=1, returns funding/positions/equity/mark as JSON. No order/cancel path is importable here.funding_allis tradeability-gated: a(coin,venue)cell is emitted only if that venue has a live mark > 0 for it (kills theSKIP_MARK-at-hedge-time class);markresolves the venue-correct symbol (Nado'-PERP'key).hedger.py/rpc_order.py— GATED delta-neutral executor + the only write path (double gateARMED+DRY_RUN/intent; default OFF → returns plan dicts, places nothing).risk.py— basis-tail monitor, ACTIVE every loop (wired intocarry.run_onceafter reconcile, viarun_risk_monitor):assess()deleverages a held pair on liq-distance < 8% or basis-MtM ≥ 75% of budget, flattens-ALL on total adverse MtM ≥ 100% (GLOBAL_KILL), and tops up HL isolated margin when a leg's liq-distance is thin;execute_actions()dispatches every write through the SAME gatedOrderRPC(egress-assert +ARMED/DRYgate + RateGovernor) → under DRY/unarmed it PLANS only and places nothing.state.py— persistent BOOK-STATE store (the bot's OWN carry book). Crash-safe atomic JSON atstate/book.json(write tmp +os.replace, so a mid-write crash never corrupts it). Carry-managed coins ONLY. This is what lets hysteresis (HOLD / REBALANCE / CLOSE) survive restarts — without it the coordinator runsheld={}and re-OPENs the whole cross-section every loop.
The flaw this guards against. Carry's RPC runs each venue's client on that venue's PROD-bot host (SSH-in to reuse the tested SDK client). So carry's exchange API calls egress from the SAME public IP as the production bot → they share the venue's per-IP rate-limit bucket. A burst of carry traffic can therefore throttle the prod bot's money-critical writes (stop-loss placement, naked-heal). Two forcing mechanisms make that impossible:
The ORDER/WRITE path (hedger.OrderRPC → rpc_order.py) REFUSES to run when its SSH egress
host ∈ PROD_HOSTS, unless an explicit dedicated carry host is configured.
carry.PROD_HOSTS= the set of prod-bot hosts, derived fromVENUES(never hand-listed, so it can't drift):{hl-bot, extended-bot, pacifica-bot, nado-bot}.CARRY_DEDICATED_HOST(env / config) = a box that is NOT inPROD_HOSTS— a dedicated carry machine with the subaccount keys + all 4 venue SDKs installed. When set, every WRITE egresses from that host's IP (its own rate-limit bucket), never a prod IP.- Refusal = raise/return a clear error, place nothing. Default (no dedicated host) → WRITES are
refused; the
open_pair/close_pairresult isstatus=REFUSED_EGRESS, placed=Falseand no SSH is even attempted. ACARRY_DEDICATED_HOSTthat is a prod host is also refused (it would still share that prod IP's bucket). - READS still work:
rpc_read.pymay SSH-in to a prod host for low-freq reads (funding/positions/ equity) — tolerable, but subject to the governor below.
This makes "carry orders egress from a prod IP" structurally impossible — it is enforced in
carry.assert_write_egress_isolated() / OrderRPC._egress_host() before any SSH or SDK
construction, so an unsafe deploy can't place a single order.
carry.RateGovernor is a per-(egress-host) token-bucket + min-interval limiter wired into the SSH-
dispatch layer (carry._ssh_read and OrderRPC._call):
- Carry caps its own request rate to a small ceiling
CARRY_MAX_REQ_PER_MIN(default 20/min), well under any venue's per-IP budget, and enforces a min gap between calls (CARRY_MIN_GAP_SEC, default60/CARRY_MAX_REQ_PER_MIN= 3 s). Even a tight loop physically cannot hammer a venue. - On any HTTP 429 / rate-limit signal from a venue (
carry.is_rate_limit_signal), carry backs off exponentially and yields (CARRY_BACKOFF_BASE_SEC2 s → ×2 each hit, capped atCARRY_BACKOFF_MAX_SEC120 s) — it defers to prod, never retry-storms. A clean call resets the streak. - The funding scan is hourly-cadence anyway:
CARRY_POLL_INTERVAL_SEC(default 3600) is the sane loop default, and--loopis floored to it so a misconfigured--loop 1can't hammer.
| knob (env) | default | meaning |
|---|---|---|
CARRY_DEDICATED_HOST |
(unset → writes refused) | dedicated non-prod carry box (subaccount keys + venue SDKs) the WRITE path egresses from |
CARRY_MAX_REQ_PER_MIN |
20 |
per-egress-host request ceiling (≪ venue budgets) |
CARRY_MIN_GAP_SEC |
60 / max_req_per_min (=3) |
min seconds between calls to the same host |
CARRY_BACKOFF_BASE_SEC |
2.0 |
first 429 backoff (doubles per consecutive hit) |
CARRY_BACKOFF_MAX_SEC |
120.0 |
backoff ceiling (never unbounded) |
CARRY_POLL_INTERVAL_SEC |
3600 |
sane funding-loop interval (--loop floored to it) |
Because WRITES refuse to egress from a prod IP, arming live requires a dedicated carry host — a separate box/IP that is NOT one of the prod-bot hosts. Provision it with:
- All 4 venue SDKs + each repo's
bot.*package installed (HL=hyperliquid-sdk, Extended=x10 Stark async, Pacifica=solders, Nado=nado-protocol) sorpc_order._build()can build each tested client there — the samecd <repo dir> && <venv>/bin/pythonlayout the prod hosts use, mirrored on the carry box. - The carry SUBACCOUNT
.envper venue (seeenv_templates/*.env.exampleand the "Subaccount isolation" section) — the carry leg trades the isolated subaccount, never the prod account. - SSH reachable as the hostname you put in
CARRY_DEDICATED_HOST(matching~/.ssh/config).
Until that host exists and CARRY_DEDICATED_HOST points at it, the assert keeps every WRITE refused —
reads keep working (governed) so the DRY pipeline is fully usable. Tests:
pytest -q test_egress_governor.py (20/20: egress refusal sends no SSH/places nothing; a dedicated
non-prod host is allowed-to-proceed but ARMED-off plans only; governor throttles bursts + backs off on
429; a DRY funding scan still reads under the governor).
load()— readbook.json(missing → empty cold start; corrupt → FAILS LOUD, never silently empties).add_pair(rec)— record a confirmed-open pair (atomic save). Record shape:{coin, long_venue, short_venue, leg_notional, entry_ts, entry_spread_pct, long_fill_px, short_fill_px, long_oid, short_oid, sl_oids, status}.remove_pair(coin)— drop a confirmed-closed (or FULLY_GONE) pair (atomic save).update_pair(coin, **fields)— patch fields (status, sl_oids, fill px) on an existing record.held()→{coin: rec}·held_coins()→set·held_view()→{coin:{long,short,net}}(what the diff hysteresis reads) ·save()after every mutation (atomic).reconcile(live_positions_by_venue, universe)→ READ-ONLY discrepancy report (logs only, NO auto-trade). Per booked pair, confirms BOTH legs still live with ~matching signed size and classifies:OK— both legs live, sizes match.LEG_MISSING— one leg gone → naked directional risk, surfaced LOUD inreport["naked"]. Now ACTIVELY HEALED every loop bycarry.run_risk_monitor: the survivor leg is flattened through the gated hedger (flatten_with_readback→ readback + retry +ALARM_NAKED_LEG), so a naked leg is no longer log-only — it is killed (DRY/unarmed → planned, places nothing). Thereconcilefunction itself remains read-only and NEVER touches a position; the heal is driven by the monitor, not reconcile.SIZE_DRIFT— both legs live but signed size diverged > tol (investigate, no auto-trade).FULLY_GONE— both legs closed externally → listed inreport["drop"](removed only on a real close).UNTRACKED— a live position on a carry coin/venue that is not in the book → flagged, not adopted.- Foreign / non-carry coins are NEVER touched or flagged (manual / other-bot rule;
book.jsonis carry-coins only). UNTRACKED is scoped tocarry.UNIVERSE∩ carry venues.
Wiring (carry.run_once): load book → pull live positions per venue → reconcile + log report (first
loop iteration) → pass book.held_view() into diff_book_vs_live (replaces held={}) so OPEN fires
only for un-held target coins, CLOSE only for held carry-pairs that left the target set (with the
RESELECT_BAND_ANN hysteresis), REBALANCE on drift. When --arm (OFF now), a confirmed open →
add_pair, a confirmed flat close → remove_pair. Smoke test: python3 smoke_state.py (25/25, zero orders).
Why distributed: the 4 clients use incompatible SDKs/hosts (HL=hyperliquid-sdk, Extended=x10 Stark async, Pacifica=solders, Nado=nado-protocol) — cannot co-import. Live creds verified present on each host.
DRY-RUN reads + scan + route + size + log + persistent self-managed book (state.py) + read-only
reconcile all work. The book now survives restarts, so hysteresis/CLOSE/REBALANCE persist (the
coordinator no longer runs held={}). Signal-parity bugs are FIXED (6h-trailing-mean · Extended
sentinel-parse · Nado venue-decode) → all 4 venues report valid funding and the DRY-RUN book is sane.
Self-impact OI-cap (5% of venue OI/leg) is applied in the sizer. Order layer (hedger/rpc_order/risk)
is written and GATED OFF (triple gate). Remaining before live = capital + ARM sign-off + paper-trade.
The 4 bots resolve DRY_RUN through different mechanisms, so a single shell DRY_RUN=1 is
not uniformly safe and a single cfg.dry_run read is wrong on 2 of 4 venues. carry.py
carries a per-venue dry_force descriptor; carry._dry_force_prefix() builds the venue-correct
remote force (used by BOTH the read path and the order path via hedger.OrderRPC); rpc_order._resolved_dry()
reads the value the order guard actually obeys, keyed by the CARRY_VENUE env the coordinator passes.
The gate in rpc_order._guarded() REFUSES to fire unless the venue-correct resolved-dry is provably
True under a dry intent.
🚨 The CARRY GATE is the SINGLE SOURCE OF TRUTH (Bug C → single-source-of-truth, money-critical,
upgraded 2026-06-30). resolved-dry==True proves the venue is configured dry — it does not prove
the adapter method mocks the write. The Extended and Nado adapters submit a REAL order
regardless of dry_run (prod gates dry at a HIGHER layer — Extended trader.py _dry_block, Nado
main.py — that carry's direct client.market_open/market_close call bypasses; proof: an armed+DRY
carry leg once placed REAL ENA-USD MARKET orders on the Extended carry vault, ~$2.88 real taker
round-trip). HL and Pacifica adapters do self-guard every signed write — but the carry gate no
longer relies on that. Under a dry intent rpc_order._guarded self-mocks the signed write ITSELF for
EVERY venue (HL/Extended/Pacifica/Nado alike) and the live adapter method is NEVER called, on any
venue (carry_self_mocked=True). The adapter's own dry-guard remains as a redundant second layer,
never the only one; the per-venue _ADAPTER_SELF_MOCKS_DRY flag is retained for telemetry only
(surfaced as adapter_would_self_mock), no longer load-bearing — so a future venue whose adapter we have
NOT audited is safe by construction (the carry gate intercepts first). A LIVE intent (resolved-dry
provably False) is the ONLY path that fires a real order.
Live-verified 2026-06-30 on the carry box via the real rpc_order.py: armed+DRY place_market +
market_close + cancel on all four venues (HL 0x100B03 · Extended 5290 · Pacifica carry sub ·
Nado default_1) → every op returned carry_self_mocked=True / ack.status=carry-dry-mock, ZERO live
SDK submits, and a before/after position+equity+fills read showed no new orders/fills/positions.
| venue | force mechanism (dry_force) |
resolved-dry detector | why |
|---|---|---|---|
| HL | env_file: cp .env.combo /tmp/carry_dry.$$.env; sed DRY_RUN=0→1; ENV_FILE=<copy> (cleaned up) |
Settings.from_env().dry_run |
config does load_dotenv(ENV_FILE or .env.combo, override=TRUE) → a shell DRY_RUN=1 is clobbered by the file. The ENV_FILE temp is the only safe force. |
| Extended | shell: prepend DRY_RUN=1 |
import bot.trader; bot.trader.DRY_RUN |
no Settings.dry_run. Order guard obeys module const bot.trader.DRY_RUN = os.getenv(...) (override=False) → shell wins. |
| Pacifica | shell: prepend DRY_RUN=1 |
bool(int(os.getenv("DRY_RUN","0") or "0")) (env RULE, no client built) |
no Settings.dry_run. Guard obeys instance PacificaClient.dry_run; reading the env rule is read-only-equivalent (client __init__ does a network _load_meta). |
| Nado | shell: prepend DRY_RUN=1 |
Settings.from_env().dry_run |
load_dotenv WITHOUT override → shell DRY_RUN=1 propagates into Settings.dry_run. |
dry_force is wired against /home/ubuntu/pacifica_bot_v2_a, but that dir
is RETIRED — the live pacifica-bot.service runs /home/ubuntu/pacifica_xnn_bot. The dry-force
code path is identical, but the account/dir for any live carry leg is a deploy decision to re-point
before arming.
Build-verified READ-ONLY 2026-06-25 (zero orders; no live file/source touched; HL /tmp temp
cleaned). OP=resolved_dry probe per venue:
| venue | resolved-dry baseline (live) | resolved-dry under force | detector | adapter ALSO self-guards? (adapter_would_self_mock, telemetry only) |
|---|---|---|---|---|
| HL | False (live) |
True |
Settings.from_env().dry_run (forced via ENV_FILE) |
YES — _dry_guard (redundant; carry gate intercepts first) |
| Extended | False (live) |
True |
import bot.trader; bot.trader.DRY_RUN |
NO — trader.create_and_place_order, no guard |
| Pacifica | False (live) |
True |
bool(int(os.getenv("DRY_RUN","0") or "0")) |
YES — _request POST-mock (redundant; carry gate intercepts first) |
| Nado | False (live) |
True |
Settings.from_env().dry_run |
NO — SDK place_market_order/close_position, no guard |
Under a dry intent the carry gate self-mocks the write for every venue regardless of the last column
(carry_self_mocked=True, the live adapter method is never called); the column only records whether the
adapter would also have mocked (verified 2026-06-30 against /root/carry_sdks/<venue>/bot/). It is a
backstop, not the authority.
The carry HL leg trades the standalone master account 0x100B03683F7A62f32fB6ab276eAe06529505f506
with the agent wallet 0x52C54373… as the signer and no vault_address (no subaccount). HL is
a UNIFIED account — the master's spot USDC is the perp cross-margin collateral, so there is NO
usdClassTransfer (re-adding one encodes a false split model AND is a DRY-bypass risk; see the NOTE in
rpc_order.py). The installed SDK Exchange resolves the acting address as
addr = wallet.address; if account_address: addr = account_address; if vault_address: addr = vault_address
— so an empty account_address silently acts as the empty AGENT wallet (the proven
"Must deposit before performing actions. User: 0x52c5…" rejection). rpc_order._build /
rpc_read._build therefore call _assert_hl_acts_for_master(cfg, client): it forces
exchange.account_address = <master> whenever it is empty or equals the agent, and raises if no
master is configured (fail-closed, never silently act as the agent). Live-verified READ-ONLY on the
carry box: a signed HL order ACTS_AS 0x100B03… (master), signer 0x52C54373… (agent), vault=None;
an HL equity read resolves the master's $1000 unified balance.
- Dedicated carry host — a separate box/IP (NOT a prod-bot host) with the 4 venue SDKs +
subaccount
.env, pointed to byCARRY_DEDICATED_HOST. Enforced: until it exists every WRITE isREFUSED_EGRESS(carry must never share a prod IP's rate-limit bucket). See "Egress isolation". - Capital — deposit per venue (HL / Extended / Pacifica / Nado); nothing can size without it.
- ARM sign-off —
--armis OFF andrpc_order's host gate (ARMED+DRY_RUN/intent, HL alsoHL_LIVE_SIGNOFF) defaults DRY. Live = explicit operator sign-off on each host, not a code change. - Paper-trade — run armed-but-DRY end-to-end to exercise the live order code path with zero
money: confirm fills read back,
add_pair/remove_pairrecord correctly, reconcile stays clean across a real open→close cycle. The dry-force is per-venue (see the table above): HL needs theENV_FILE→dry-copy force (a bare shellDRY_RUN=1is clobbered by itsload_dotenv(override=True)); Extended/Pacifica/Nado take a shellDRY_RUN=1.hedger.OrderRPC(dry) splices these automatically;rpc_order._guardedREFUSES to place unless the venue-correct resolved-dry is provablyTrue, AND (Bug C) self-mocks the write itself on any venue whose adapter does not provably self-guard (Extended/Nado), so a "dry" leg can never reach a realmarket_open/market_close. Then, and only then, flip to live (host-sideDRY_RUN=0/sign-off, NOT a coordinator change).
(Signal-parity item 1 is a correctness blocker for the NUMBERS; capital + ARM + paper-trade are the operational gates for going live once the numbers are trusted.)
The carry bot should NOT trade on the same account as the existing per-venue production bots —
it needs its own isolated margin book on each venue so its positions/risk never commingle
with the live strategy bots. Each venue exposes a native isolation primitive; the carry bot is
subaccount-ready WITHOUT any edit to the prod bot repos — config templates live in
env_templates/*.env.example and the only code change is a carry-side HL re-point (below).
Per-venue isolation map (verified live + docs 2026-06-25):
| venue | isolation primitive | what's NEW (user supplies) | what's REUSED | margin isolated? |
|---|---|---|---|---|
| HL | native subaccount = separate on-chain address, no own key (master/agent controls it; SDK targets it via vault_address) |
HYPERLIQUID_SUBACCOUNT_ADDRESS (create + fund in UI) |
same agent key + same master HYPERLIQUID_ACCOUNT_ADDRESS |
yes (sub vs master) |
| Extended | native subaccount (≤10 per ETH wallet) = own API key + own Stark keypair + own margin pool | EXTENDED_VAULT_ID, EXTENDED_ACCOUNT_ID, EXTENDED_STARK_PUBLIC/PRIVATE, EXTENDED_API_KEY |
same EXTENDED_ETH_ADDRESS (parent wallet) |
yes (independent pool) |
| Pacifica | native subaccount = separate Solana wallet, dual-signed create, isolated book | new subaccount wallet + PACIFICA_ACCOUNT_ADDRESS + an agent bound to the sub + the sub's PACIFICA_PRIVATE_KEY |
network/exchange only | yes ("Sub-accounts are isolated") |
| Nado | native subaccount = same wallet + a name tag (1CT); own cross-margin book | NADO_SUBACCOUNT=carry (new tag, create in 1CT UI) |
same signer key + same NADO_ACCOUNT_ADDRESS |
yes (per-subaccount book) |
Pacifica note (Part A finding): Pacifica's subaccount is BOTH "native" AND "a new wallet" — the
route POST /api/v1/account/subaccount/create exists live (unauth POST → HTTP 400 "missing field
main_account", not 404) and the official docs state "Sub-accounts are margined independently /
isolated". It is a fresh Solana keypair cross-signed (main_signature + sub_signature) into the
master, NOT an address-only swap like HL. So the Pacifica carry leg needs a new keypair + a bound
agent, whereas HL/Nado reuse the existing key and only change an address/tag.
All steps that touch money (create / bind / fund) are done by the operator, never by the carry
bot. Keep DRY_RUN=1 in every template until the paper-trade passes.
- HL — In the HL UI create a subaccount, transfer USDC to it from the master. Copy its address
into
HYPERLIQUID_SUBACCOUNT_ADDRESS(templateenv_templates/hl.env.example). The carry HL client (rpc_read/rpc_order_build) then auto-rebuildsExchange(... vault_address=<sub>)and points reads at the sub. If the var is unset, carry runs on the master unchanged (no-op). - Extended — In the Extended app create a new subaccount under the same ETH wallet, fund it,
then issue its API key and derive its Stark keypair (SNIP-12, from the parent seed for that
subaccount index). Fill
EXTENDED_VAULT_ID/EXTENDED_ACCOUNT_ID/EXTENDED_STARK_PUBLIC/EXTENDED_STARK_PRIVATE/EXTENDED_API_KEY; keepEXTENDED_ETH_ADDRESSas the parent. - Pacifica — Generate a fresh Solana keypair;
POST /account/subaccount/createdual-signed (main + sub);POST /agent/bind(signed by the sub key) to authorize an agent; fund via a subaccount transfer from the master. SetPACIFICA_ACCOUNT_ADDRESS= sub pubkey,PACIFICA_AGENT_PRIVATE_KEY= the bound agent,PACIFICA_PRIVATE_KEY= the sub's own key. - Nado — In app.nado.xyz (1CT) create a new named subaccount (e.g. tag
carry) under the same wallet and fund it. SetNADO_SUBACCOUNT=carry; reuseNADO_LINKED_SIGNER_PRIVATE_KEYandNADO_ACCOUNT_ADDRESS. (Use a tag DIFFERENT from the live bot'sdefault.)
rpc_read.py and rpc_order.py _build() call _apply_hl_subaccount(cfg, client) right after
building the HL client. When HYPERLIQUID_SUBACCOUNT_ADDRESS is set it (a) rebuilds client.exchange
to a hyperliquid Exchange(wallet, url, account_address=<master>, vault_address=<sub>, perp_dexs=…)
so every signed action stamps vaultAddress=<sub> (HL SDK uses vault_address for both vaults AND
subaccounts), and (b) re-points reads by swapping in dataclasses.replace(settings, account_address=<sub>) (Settings is a frozen dataclass). When the var is absent it returns
immediately — identical to the prod bot today. It is HL-only (guarded on the client being an
HLClient with a hyperliquid Exchange), so the other 3 venues are unaffected. The prod
exchange_hl.py is NOT edited.
Build-verified READ-ONLY on hl-bot (DRY, zero orders, only Exchange objects constructed):
| case | exchange.vault_address |
reads account_address |
|---|---|---|
HYPERLIQUID_SUBACCOUNT_ADDRESS = 0xDEAD…0001 |
0xDEAD…0001 ✓ |
0xDEAD…0001 (re-pointed to sub) ✓ |
| var absent | None (no-op) ✓ |
master 0x21d5… (unchanged) ✓ |
(exchange.account_address stays the master in the sub case — the agent that SIGNS is the master's
agent; vault_address is only the TARGET account.)
DONE this session:
- ✅ SIGNAL-PARITY —
rpc_readnow returns 6h-TRAILING-MEAN funding (HL fundingHistory · Extendedget_funding_rates_history· Pacificafunding_rate/history· Nado SDK daily×365) + ±cap guard. Spike×8760 killed (AVAX −32→−15, ETH −19→−8.7). Live signal == backtested signal. - ✅ Extended parse — sentinel
@@CARRYJSON@@, coordinator greps that line (SDK stdout chatter ignored). - ✅ Nado funding — root was
cfg.exchange=Noneon host →_venue()class-name fallback → SDK decode works. - ✅ Order execution —
rpc_order.py(per-host, double-gated) +hedger.py(atomic both-legs-or-unwind, readback, no-naked; the LEG-SKEW unwind + any timed-out/erred leg now do a POSITIVE position readback + retry, and ALARM_NAKED_LEG on a residual that won't flatten — never a silent success) +risk.py(basis-tail deleverage / global flatten / HL isolated top-up), now WIRED ACTIVE every loop viacarry.run_risk_monitor(after reconcile), which also HEALS LEG_MISSING (flattens a naked survivor) +state.py(persistent book + reconcile). Entry = marketablemarket_open(taker; no native post-only entry). All writes go through the SAME gate (egress-assert + ARMED/DRY + RateGovernor): Orders OFF (ARMED default 0) → the monitor/heal/deleverage PLAN only and place nothing. - ✅ Self-impact OI-cap — each leg ≤ 5% of min(venue OI); thin-alt legs auto-shrunk/skipped.
- ✅ SKIP_MARK gap FIXED at source —
rank_and_routepaired coins on funding PRESENCE, but a venue can publish funding-panel data for a coin it has no live tradeable MARK for (delisted/paused/symbol-map mismatch) → those legs detonatedSKIP_MARKat hedge time.- Tradeability gate in
rpc_read._funding_all: each funding cell is now gated on a venue-correct live mark > 0, so a(coin,venue)cell in the funding matrix ⇒ the coin is tradeable there. Cheap/reuse: HL oneall_mids()snapshot · Pacifica one_prices()snapshot (mark+funding same pass) · Extendedget_market_statistics(...).mark_price· Nadomark_price(<canonical key>). DEGRADED (mark source down) keeps the cell (never a silent whole-venue drop, logged to stderr); a fetchable mark==0/absent drops that one coin for that one venue. - Nado bare-coin mark bug (the actual
SKIP_MARKtrigger): Nadomark_price()/_pid()key strictly on the canonical symbol ('SOL-PERP') in_symbol_to_pid, somark_price('SOL')KeyErrored →0.0. The hedger'smarkop + post-fill readback used the bare coin →0.0→SKIP_MARK L=0.0on every Nado leg. Added a shared venue-correct_mark_for(cfg, client, coin)(resolves bare →'<COIN>-PERP'→asset()) in bothrpc_read.py(themarkop) andrpc_order.py(themarkop and_read_onereadback). Read-only — places nothing, no ARMED/dry-gate change. - VERIFY (DRY, zero orders):
--armDRYSKIP_MARKcount 2 → 0; the Nado-LONG legs (AAVE/SOL/ETH/ENA) now reachFAIL_LONG_UNCONFIRMED(expected no-fill-in-dry), notSKIP_MARK. Plain DRY book sane, all 4 venues report, every |funding| ≤ venue cap, gate logged 0 un-tradeable drops on the live snapshot (no coin currently has funding without a mark — gate is protective, not yet load-bearing today).smoke_state.py25/25.
- Tradeability gate in
REMAINING correctness items (not blockers for paper-trade, but for trusting the LIVE %):
- cap-collection 2.2↔8% — does a cap-pinned venue actually pay posted funding to our short? live-only (paper-trade).
- self-impact magnitude — OI-cap bounds it; realized compression measured by paper-trade.
- single regime ≤342d — not multi-cycle OOS; revisit as funding history grows.
cap_annmap vs live structural funding ceilings — reconcile exact clamp values before live.- cleanup: delete superseded
book_state.py(replaced bystate.py).
GATES TO LIVE (operational): dedicated carry host (CARRY_DEDICATED_HOST, non-prod IP — else WRITES refused) → capital per venue → ARM sign-off per host → paper-trade (ARMED=1 DRY_RUN=1) → flip DRY_RUN=0.
L = RISK_BUDGET_PCT / BASIS_SHOCK_PCT. Default 33% / 3.3% (Oct-10-2025 EW perp-spot decouple) = 10×.
ROE = edge × L; basis-cascade loss = shock × L. Alt-heavy book → assume −6.6% → L≈5×. NOT Kelly/vol-target
(they read flow-vol 0.78%, blind to basis-tail → ruinous leverage).
funding source per-venue · spike×8760 vs trailing+clamp (item 1 above) · funding_cum vs level · stale baseline · 1×/book≤eq (futures) · spot==mark basis-tail · fill instant-vs-limit · cap-collection 2.2↔8% (live-only) · Nado snapshot-history · single regime · depth/borrow/venue-pos-cap · per-venue liq.
python3 carry.py --equity 50000 # one pass, fixed equity
python3 carry.py # sum live equity across venues
python3 carry.py --loop 3600 # repeat hourly