The one untested selection axis (order-book depth = the owner's 'size stacked above the break' instinct). Staged so $0 is spent until a free sniff test + recorder prove the signal exists and covers the universe; paid feed is an explicit owner gate; the signal faces the same pre-registered bar + random-gate null as every refuted lever before it touches the live entry. Honest ceiling stated: a PASS nudges odds, is not an autonomous oracle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
14 KiB
Plan 017: Level-2 order-book depth recorder — the one untested selection axis, recorded free-first, spend only after the data earns it
Executor / operator instructions: This plan is deliberately staged so that NO paid data is bought until a free evaluation proves the signal is real. Phases 0–1 cost $0 and are the gate. Do NOT skip to a paid feed. Follow each phase's verification before the next. When a phase lands, update the status row in
plans/README.md.Drift check (run first):
git diff --stat 053c927..HEAD -- run_live_paper.py research/experiments/replay.py storage/On a mismatch with the "Current state" excerpts, STOP and reconcile.
Status
- Priority: P1 (this is the project's founding question — can premarket data give autonomous selection confidence — made into a testable experiment; every other lever is refuted)
- Effort: L (multi-week, mostly passive data accrual)
- Risk: LOW to live trading (recording-only; zero entry/exit/sizing change until a separate, owner-gated promotion plan). MED to wallet (Phase 2 is a paid feed — gated behind Phase 1 proof).
- Depends on: 006 (Experiment OS harness), the
quote_snapshotsrecorder pattern (already in prod) - Category: research-infrastructure / data-acquisition
- Planned at: commit
053c927, 2026-07-18
Why this matters (read fully — this is the strategic core)
The bot enters and exits well; it cannot SELECT. Twelve entry-conversion levers computable from 1-min bars + top-of-book quotes have been refuted. The measured reason (2026-07-18, owner's own visual review confirmed it): at the trigger, ~80% of clean gap/RVOL/VWAP setups fail, and the winners and losers are indistinguishable in the available data. A model can only find signal that exists in its inputs; the free inputs are exhausted.
The one axis never tested is the one the owner's own instinct names: the
order book — resting limit-order size stacked above/below the break. Top-of-
book (NBBO) is already recorded (quote_snapshots); full depth (all price
levels, "Level 2" / Nasdaq TotalView) is not, because it needs a paid feed the
current broker (Alpaca) does not sell. Depth is intent (what is waiting) vs.
the tape's history (what happened) — genuinely new information, not a
re-derivation of a refuted feature.
Honest priors, stated so nobody oversells this:
- L2 on thin small-caps is the noisiest, most-gamed data in the market (spoofing, hidden/iceberg size, HFT layering). The visible book ≠ the book that fills. This is a real reason it might show nothing.
- Even if it works, best case nudges the hit rate (e.g. 20%→30% winnable), not to "autonomous certainty." Pros with full depth AND borrow data still run modest hit rates and win on discipline/sizing — which the bot already does.
- It therefore MUST clear the same pre-registered bar as every other lever
(
expR_delta ≥ +0.05,n ≥ 300, adverse-fill fidelity, and the random-gate null added 2026-07-17), scoped to the livev1population, before one dollar of it touches the entry decision.
The value of this plan is not "add L2 and win." It is: spend $0 to find out whether the order book carries the selection signal, and only pay for the feed if the free evaluation says the signal is there. That is the disciplined answer to "get to high confidence" — record the candidate, test it cold, believe only what clears the bar.
Current state (what to mirror)
-
run_live_paper.py:~218-230— the DDL +research_conrecorder pattern:research_con.execute( """CREATE TABLE IF NOT EXISTS quote_snapshots ( symbol TEXT NOT NULL, ts TIMESTAMPTZ NOT NULL DEFAULT now(), bid_price DOUBLE PRECISION, bid_size DOUBLE PRECISION, ask_price DOUBLE PRECISION, ask_size DOUBLE PRECISION)""") -
run_live_paper.py:~972step_quotes()— the exact recorder idiom to copy: gather tracked-trigger + held symbols, fetch, insert per symbol, never raise, return a one-line status string. Registered on the scheduler with an interval (see thescheduler.add("quotes", ...)line, ~15 s cadence). The L2 recorder is a sibling step with the same shape and a wider row. -
research/experiments/replay.py— the harness that consumes recorded features. The order-flow gate interventions fromorderflow_notes.md(iv_spread_expansion_gate,iv_quote_imbalance_gate) already readquote_snapshotswith a skip-on-no-coverage design (a fire without data →variant_r = None, excluded fromn, never silently kept). The L2 hypothesis reuses this exact coverage discipline (the dilution_veto_v1 false-FAIL lesson). -
storage/db_pg.pyPgConnection— the facade auto-reconnects on a dropped connection (_run/_reconnect); the recorder inherits that for free. -
Config convention: every recorder/feature is env-flagged (
_flag("X_ENABLED", "1")), default-on, fail-open. The L2 recorder ships default-OFF until Phase 2 (no free depth source exists to record from in Phases 0–1).
Phased plan (each phase gates the next)
Phase 0 — FREE evaluation: does any usable depth exist, and does it look predictive at all? ($0)
Goal: decide whether L2 is worth a paid feed WITHOUT buying one, using free trials and free tiers only.
- 0a. Vendor scan (no purchase). Identify 2–3 sources that expose full-depth (Nasdaq TotalView / MBO or aggregated depth) for the $2–20 small-cap universe, and their FREE trial / free-tier terms. Candidates to evaluate (verify current terms, do not assume): Polygon.io (has an aggregated quotes tier; confirm whether true depth is included and at what plan), Databento (pay-as-you-go with a free credit allotment — TotalView/MBO available; good for a bounded historical pull), Alpaca (confirm it still does NOT sell depth, only NBBO — the assumption this plan rests on), a DAS/ broker-terminal trial. Deliverable: a one-page comparison — provider × (depth available? y/n) × (free trial size) × ($/mo after) × (symbols covered) × (latency). No card entered that auto-charges.
- 0b. Free historical probe. Using a free-credit/trial pull (e.g.
Databento's free allotment or a Polygon trial), fetch full-depth book
snapshots for ~20 of the bot's actual recent gapper fires (pull the
symbols+session_dates from
setup_events WHERE setup_version='v1'on days with known outcomes). For each fire, compute one candidate feature at the trigger minute: depth imbalance = (Σ bid size top N levels − Σ ask size top N levels) / (Σ both), plus ask-stack-above = resting ask size within X% above the break / recent traded volume. Join to the knownreached_1r_before_minus_1routcome. - Verify / GO-NO-GO: a crude eyeball on ~20 fires (winners vs losers) shows
a visible separation in depth imbalance or ask-stack — even noisy. This is
NOT the real test (n far too small); it is the cheap sniff test that decides
whether the paid recording phase is worth starting. If 20 fires show zero
hint of separation AND the vendor scan shows depth is expensive → STOP here,
write the negative result in
plans/README.md, recommend the human-selector fork instead. This is a legitimate, cheap, correct place to end.
Phase 1 — Build the recorder against the free/trial feed, log-only ($0 or trial credit)
If Phase 0 passed the sniff test, build the pipe — still on free/trial data where possible, recording only.
- 1a. Schema. New table
depth_snapshots(research DB), mirroring thequote_snapshotsDDL idiom but wider:symbol TEXT, ts TIMESTAMPTZ DEFAULT now(), provider TEXT, bids JSONB, asks JSONB, mid DOUBLE PRECISION, spread_bps DOUBLE PRECISIONwherebids/asksare the top-N[price, size]ladder (N≈10). JSONB keeps it one row per snapshot without a level table; the harness reads the ladder back and computes imbalance features. Index(symbol, ts). - 1b. Recorder step. New
research/ingestion/depth_recorder.pywith asnapshot_depth(con, client, symbols) -> strfunction shaped exactly likestep_quotes()(fetch top-N depth per symbol, insert, never raise, return a status line). Wire ascheduler.add("depth", ...)step inrun_live_paper.pybehind_flag("DEPTH_RECORD_ENABLED", "0")(default OFF), symbols = same tracked-trigger + held set asstep_quotes. Cadence: ~5 s around the open (depth is fast), configurable viaDEPTH_RECORD_INTERVAL_SECONDS. A provider adapter module isolates the vendor API so Phase 2 swaps the feed without touching the step. - 1c. Feature extractor.
research/depth_features.py:depth_imbalance(ladder_bids, ladder_asks, levels),ask_overhang(asks, break_price, pct_window),book_thinness(bids, asks)— all pure, unit-tested on synthetic ladders. - Verify: recorder run against the trial feed writes well-formed
depth_snapshotsrows for live/replayed gappers;depth_featuresunit tests pass;python3 -m pytest tests/unit/ -qgreen; recording is provably zero-impact on the trade loop (grep: no import of depth code in any gate/exit/sizing path). STOP condition: if the trial feed's coverage of $2–20 small-caps is poor (the names the bot trades aren't covered), report — a feed that doesn't cover the universe is worthless regardless of price.
Phase 2 — Paid feed + accrual (THE SPEND — owner-gated, explicit)
Only after Phases 0–1 prove the data exists, covers the universe, and sniff- tests positive. This is the single go/no-go where money is committed.
- 2a. Owner decision point. Present: the chosen vendor, exact $/mo, the
Phase-0 sniff result, and the expected accrual timeline to
n ≥ 300covered fires (at ~5–15 qualifying v1 fires/session with depth coverage, ~4–8 weeks). The executor does not buy anything; the owner does, or declines. - 2b. Flip
DEPTH_RECORD_ENABLED=1in the deployed compose (both files), pointed at the paid feed via the Phase-1 adapter. Recording only. Let it run. - Verify:
depth_snapshotsaccrues daily for the live gapper set; a weekly coverage line reports covered-fires / total-fires (the accrual clock).
Phase 3 — Pre-registered experiment (the real verdict)
When covered fires reach the bar, test it like every other lever — no exceptions, no special pleading.
- Register
experiments/depth_imbalance_v1.json: keep-or-zero gate that zeros fires whose trigger-minute depth imbalance is ask-heavy (supply overhang above the break), population{gap≥0.10, vol≥300k, $2–20}, bar{expR_delta_min: 0.05, min_n: 300}with the random-gate null applied, fidelity{atr_frac:0.25, spread_mult:3, adverse_ticks:1}. Coverage design: fires without a depth snapshot are SKIPPED (variant None), never kept. - Add
iv_depth_imbalance_gatetoreplay.py(additive, mirrorsiv_quote_imbalance_gate), readingdepth_snapshots. Record the raw imbalance indetailper fire for tercile/stratification autopsy. - Verify / VERDICT: nightly
experiments run+adjudicateproduces a verdict. PASS (clears the bar AND the null AND survives gap%/RVOL stratification) → a SEPARATE owner-gated promotion plan wires it into the live entry gate. FAIL/INCONCLUSIVE → recorded honestly; the founding question is answered "no even with depth," and the human-selector fork is the remaining path. Either way, the project gets a real answer instead of a hope.
Scope
In scope: storage/ (new depth_snapshots DDL), research/ingestion/ depth_recorder.py + provider adapter (new), research/depth_features.py
(new), run_live_paper.py (one flag-gated scheduler step), research/ experiments/replay.py (one additive intervention, Phase 3), experiments/ depth_imbalance_v1.json (Phase 3), tests/.
Out of scope: ANY change to the live entry/exit/sizing path (promotion is a separate plan after a PASS); buying data before the Phase-2 owner gate; trading on depth in any form during Phases 0–3.
Done criteria (per phase, not all at once)
- Phase 0: vendor comparison one-pager + 20-fire sniff result recorded in
plans/README.md; GO or STOP decision explicit - Phase 1:
depth_snapshotstable + flag-OFF recorder + pure feature extractors, all tested, zero live-path imports - Phase 2: owner-gated spend decision documented; if GO, recording accrues with a weekly coverage line
- Phase 3: pre-registered experiment adjudicated; verdict recorded; promotion is a NEW plan only on PASS
git statusclean per phase; suite green;ruff check --select Fclean
STOP conditions
- Phase 0 sniff shows no separation hint AND depth is costly → STOP, record the negative, recommend the human-selector fork. (Cheapest correct ending.)
- Any trial/paid feed fails to cover the $2–20 small-cap universe the bot actually trades → STOP; a feed that misses the names is worthless.
- Any temptation to trade on depth before the Phase-3 verdict → STOP (violates the positive-evidence contract; this is exactly the discipline that killed 12 false levers cheaply).
- Phase 3 experiment cannot reach
n ≥ 300covered fires within ~3 months → park as accrual-blocked, do not lower the bar.
Maintenance notes
- Depth data is large.
depth_snapshotsJSONB at 5 s cadence over the watched set will grow fast — add it to the nightly retention purge (mirror theentry_gate_evaluated14-day purge inresearch/premarket_eval.py) keeping only fires' trigger-window snapshots long-term, spam-purge the rest. - The provider adapter is the seam: keep vendor specifics behind it so a feed swap (or drop) is one module, never a loop change.
- If Phase 3 PASSES: the promotion plan must re-verify latency — a signal that works in recorded snapshots must survive being read and acted on inside the live trigger thread's timing budget (the same fast-path discipline the regime gate needed).
- Honest ceiling reminder for whoever reads this later: a PASS nudges the odds; it is not the autonomous oracle. Size and discipline still carry the P&L.