DESIGN.md@strategies/shadow · git:20261012.c620775 · 2026-10-12 · sha256 a99c80b44c5a0a34
DESIGN.md@strategies/shadow git:20261012.c620775A
Immutable. This exact content never changes and is served at /api/v1/blob/a99c80b44c5a0a34.
# Shadow — blocked, not deployable (2026-10-12) Shadow is **tracked and tested in this repo but removed from the catalog**. It cannot work against the data source it reads. This file records the measurement so nobody re-derives it, and specifies what would unblock it. ## The premise, and why it can't hold Shadow mirrors a vetted trader's **newly opened** position, and only while that position is still young — `maxEntryAgeSeconds: 240` — and still near the trader's fill (`maxEntrySlippagePct: 1.5`). That premise requires the trader's position to be *visible to us* within four minutes of them opening it. It is not. Position state reaches us roughly **two hours late**. Measured on `hyperliquid.hyperliquid_position_states`, `event_type = 'start'`, as `updated_at - trade_time`: | day | position opens | median lag | available inside 240s | |---|---:|---:|---:| | 2026-09-26 | 9,963 | 133.0 min | **0.0%** | | 2026-09-28 | 28,607 | 138.7 min | **0.0%** | | 2026-09-30 | 21,870 | 144.6 min | **0.0%** | | 2026-10-02 | 22,936 | 131.9 min | **0.0%** | | 2026-10-05 | 17,334 | 134.9 min | **0.0%** | | 2026-10-07 | 23,774 | 130.6 min | **0.0%** | | 2026-10-08 | 34,877 | 141.1 min | **0.0%** | | 2026-10-09 | 21,192 | 134.4 min | **0.2%** | Fourteen complete days, ~280,000 position opens, **essentially none** of them visible inside shadow's window. This is chronic, not a regression — the lag is flat across the whole period. A worked case from one live install (2026-10-09): watched trader `0xcb5c2f378860437fad512c3aec09efce1c470283` opened ZK LONG from flat at **18:20:27.714Z** (5x, $4,003 margin, taker, no TWAP, position held and grew). That row was not written to position state until **20:00:04.012Z** — a lag of **5,976 s (1 h 40 m)**. Scan ticks ran at +3.5 s and +124 s, both inside the 240 s window, and both correctly reported `fresh opens 0`, because the position did not yet exist in the data we read. **So the scanner is behaving exactly as written.** `opened_within` rejects positions whose reported age exceeds 240 s, and by the time one is visible its age is ~2 h. Nothing is mis-tuned. ## This also explains the pre-2.0.0 behaviour Shadow 1.3.0 logged **1,153 chase rejections** (2026-09-25 → 10-08) with a median distance of **14.9%** past the trader's fill, minimum 3.04%, and **nothing at all in the 1.5–3.0% band**. Those were never fresh opens. They were ~2-hour-old positions becoming visible, and the ±1.5% slippage cap was the only thing preventing users from mirroring a move that had already happened. The cap was not a nuisance; it was the only functioning safety in the design. ## What it cost Ten installs, 2026-09-25 → 10-12. Five ever filled — all on the 1.3.0 build, where stale positions reached the order path. Net realized **−$31.44** (worst: one wallet −$27.97 over 96 fills). As of 2026-10-12 **every one of the ten wallets holds $0.00**; all capital was withdrawn. Two installs still arrived after the diagnosis was known, which is why this is delisted rather than left pending a fix. ## What would unblock it The cohort *selection* is fine and should not change — `top_perps_traders_7d` is current, and `_resolve_cohort` was verified against it (funnel reproduced to 32 survivors, MM and quality counts within 1). The problem is only the **per-tick book read**. 1. Point `_fetch_states` at Hyperliquid's public `clearinghouseState`, which is live, unauthenticated, and has no indexing lag. It returns coin, `szi` (size + side), `entryPx` and leverage — everything the diff and the slippage check need. 2. It does **not** return a position open timestamp, so freshness can no longer come from a reported age. Use the transition test (`diff_fresh`: present this tick, absent last tick) as the sole freshness signal — at a 120 s cadence that bounds detection to ≤120 s by construction, which is tighter than the 240 s the age gate was asking for. Keep `opened_within` only where an age is actually available. 3. Read the cohort **sequentially** — the HL info endpoint rate-limits parallel reads. Ten wallets per 120 s tick is comfortably inside the limit. 4. The `#664` guard must stay: on a failed read, carry the already-seeded book forward rather than emptying the seen-set, or the next tick reads the trader's whole standing book as fresh opens. ### Before relisting, prove it fires The economics are already verified and they are good — the opportunity is real, it was only ever invisible. Across the live cohort's **71** new positions in the 7 days to 2026-10-12, all 69 that could be priced were **within ±1.5% of the trader's entry one poll later** (median 0.101%, p90 0.44%, worst 1.19%). Every one would have passed the slippage cap, and the cohort opens roughly **10 new positions a day**. So the relist gate is a live one: run the fixed scanner on a single funded box and show `fresh opens` going above zero and at least one `EMIT` against a chain-confirmed open, with the entry inside the cap. Until that is on the board, this package stays blocked.