Long-only equity swing desk. 21-day hold horizon, ATR-based stops, EOD-driven signals. Options positioning (walls, gamma flip, skew, P/C, EOD flow tilt) is read as a signal for the underlying equity — this desk does not trade options and is not intended for day-trading.
> Daily desk view: top-3 focus ideas with confluence strip, ranked scan, and sector rotation. EOD options positioning & GEX ladders on the OPTIONS FLOW tab (equity signal only — this desk does not trade options).
Read REVIEW.md before trading from this. It documents the pre-production adversarial review, what was fixed, and the limitations you must trade around (option-flow side-inference is inferred not observed; feeds can disagree by a full session). The walk-forward gap flagged in the original review is now closed — see
backtest_wf.pyand REVIEW.md §4c.
v1.2.3 — scope clarification: long-only equity swing (2026-08-16)
- Header, footer, and options blocks now explicitly labelled as equity-only. OPTIONS INTEL → OPTIONS POSITIONING (equity signal); FLOW & GEX tab → OPTIONS FLOW (signal); footer states "does not trade options, not intended for day-trading."
- Zero logic change. All options data (walls, gamma flip, skew, P/C, flow tilt, EOD flow rows) still fetched and displayed — the trader reads them as positioning indicators for the underlying stock, same as before.
- Motivated by a client scope clarification: the app uses options data as a signal, never as a trading target. Relabels make that explicit so a new user cannot misread the OPTIONS INTEL card as encouraging options trading.
v1.2.2 — positions tracker rolled back (2026-08-16)
- v1.3 and v1.3.1 shipped a positions tracker (
posCLI verbs,data/positions.json, BOOK HEAT panel on DESK, per-card TRAIL/T1/CLOSE buttons, 4 POST endpoints under/api/pos/*). It duplicated what the watchlist and single-position risk budget already surfaced, so it was removed. Watchlist + FILE-watchlist entries with an optional entry price remain the way to track names. - Everything else from v1.2.1 stands: verdict-shadow fix, atomic cache writes, SPY TTL, ET-anchored date keys, VIX>35 -> NO_ADDS, regime staleness gate, wall-basis note, 4 HIGH silent-except sites converted to surfaced warnings. Test suite: 35 cases (was 62 with positions), all green.
v1.2.1 — safety hotfix (2026-08-16)
- BLOCKER fixed: verdict shadow in
thesis_commentdefeated the trend gate for any name with a recent earnings print. The function accidentally reassigned its localverdictfrom BROKEN/WEAKENING/INTACT/NEUTRAL to beat/missed/reported, and downstream the confluence strip's trend gate silently degraded to CAUTION on affected names — meaning a BROKEN name with supportive options/FA could be presented as MAJORITY SUPPORT. Live smoke run the day of the fix caught 2 BROKEN names (ASTS, CRWV) that would have bypassed the gate. Regression covered bytests/test_thesis_verdict.py. - Cache writes atomized. All disk caches now use tmp+os.replace
(
_atomic_write_json) — a crash mid-write no longer leaves a truncated JSON that poisons the next run. Previously onlyreport.jsonwas safe. - SPY history TTL'd in serve.py (12h). Long-running lookup server no longer computes RS math against boot-day SPY closes.
_et_today()— every "today" is now America/New_York. A UTC+7 trader's local date used to lead ET by a full day for portions of the session, making the TR06 earnings blackout misfire and cache keys invalidate on the wrong boundary. All 13 uses ofdatetime.date.today()in engine.py are now routed through the ET helper; a guardrail test blocks reintroductions.- VIX > 35 → NO_ADDS (REVIEW.md §4f M2). A pure equity-vol shock like Aug 2024's yen-carry unwind now zeroes sizing regardless of HY OAS.
- Regime lookup staleness disclosed.
phaseC_regime_builder.pyoutput older than 60d shows an amber "STALE Nd" chip; older than 180d, the regime chip refuses to display and prints a rebuild-required message. - Wall basis carried into TRADE PLAN labels. VOL-basis walls (Yahoo drops OI on some chains) are now flagged as "may evaporate overnight" so a volume tell isn't read as durable positioning.
- Silent-except audit — 4 HIGH sites converted to surfaced warnings.
A subagent audited every
except Exception:in the live path for silent data-quality holes. Fixed:fetch_earnings_mapper-day Nasdaq failures now roll into ahealth.reliableflag exposed onreport["feed_health"]. If the TR06 blackout gate has data holes, a yellow banner tops the DESK.fetch_hy_oasreturns{"unavailable": True, "reason": ...}on FRED failure instead ofNone, so the dashboard renders "HY CREDIT VETO OFFLINE" instead of silently degrading to VIX-only.serve._bootearnings warm records fetch errors into_state["earnings_health"]and exposes via/api/health.fetch_earnings_previewimplied-move sub-block setspv["implied"] = {"error": ...}on failure, sostop_testrenders "cannot verify — reason" rather than silently disappearing.
- Unit tests added (
tests/— 35 cases,python -m unittest discover -s tests). Covers verdict shadow, risk-off idempotency, tier progression including VIX_SHOCK, MAX_POSITION_PCT enforcement, atomic-write contract, ET helper, regime staleness, feed-health signatures + HY-unavailable propagation. Guardrail test blocks any futuredatetime.date.today()in engine.py.
v1.2 — hardening pass after adversarial review
- Event-bar detection. Any gap ≥3% or move ≥5% in the last 10 sessions is
detected, tagged
POST_EVENT_DRIFT, shown as an orange EVENT BAR banner, and named in the thesis with its gap-fill level. Previously a name that gapped +7% on earnings could be presented as a quiet base. - Trend is a gate, not a vote. When the technical thesis is BROKEN the confluence strip reads TREND GATE FAILED regardless of options/FA.
- R:R integrity. Sub-1R tickets are demoted out of the top ranks; cards flag
POOR(red) andBELOW_SPEC(amber) against the spec's 2:1 floor. - Earnings clarity (
none 21dinstead of an ambiguous—), ETF/index cards state that fundamentals don't apply, server robustness (bad ticker → 404,BRK.B→BRK-B, overlong → 400), and GEX "pin" language suppressed when the king strike is >7% from spot.
v1.1 — live lookup server (every ticker gets the full treatment)
serve.pyserves the dashboard and exposes/api/lookup?t=TICKER. Searching any ticker now fetches price history, options chains and fundamentals on demand (~5s) and renders a card identical in every section to a top pick — checklist, confluence strip, thesis, trade plan, options intel (walls/GEX/skew/flow), fundamentals. This fixes the old behaviour where a searched name showed "not fetched today".- Auto-enrichment also covers: ranked-table names outside the top 8, chart-tab tickers, compare-mode adds, and watchlist adds.
- Results merge into
data/report.json, so they survive reloads and rebuilds. - HTML is served
no-storeand chart links are build-stamped, so a stale browser cache can't show you yesterday's page.
v1.0 — FA layer + three-way confluence
- FUNDAMENTALS block on every card — deliberately thin, only what argues for or against a long: revenue growth YoY with acceleration/deceleration (latest quarter vs the full-year trend), EPS growth, gross/operating/net margins with the YoY operating-margin change, forward P/E · PEG · P/S, net cash · FCF · ROE, and analyst consensus with target upside.
- Each block renders SUPPORTS LONG / NEUTRAL / REJECTS LONG with the ✓ pro and ✗ con reasons that drove it — same grammar as the options verdict.
- CONFLUENCE strip at the top of every card: TECHNICAL · OPTIONS · FUNDAMENTAL verdicts side by side, plus a one-line read ("ALL ALIGNED — highest conviction", "CONFLICTED — size down or wait", "MULTIPLE REJECTS — stand aside"). Three independent votes on one trade.
- FA also appears in the watchlist cards, the chart tab's side panel, and the compare table (rev growth, fwd P/E · PEG, FA verdict) so side-by-side candidate selection includes fundamentals.
- Coverage: top 8 ranked + watchlist each scan, cached daily in
data/fa_cache.json; any other name viapython engine.py lookup T.
v0.4.1 — OPTIONS INTEL on every card
- The thin "options map" grew into a full OPTIONS INTEL block shown on focus cards, search cards, and lookups: call/put walls, gamma flip, net gamma, P/C volume & OI, IV skew (90%-moneyness put IV minus 110% call IV — steep = downside hedging bid, flat = no fear), ATM IV with IBKR 52w IV percentile, option momentum (chain vol/OI plus IBKR volume-vs-average), EOD premium flow tilt, and the top 3 flow prints inline.
- Each block renders a SUPPORTS LONG / NEUTRAL / REJECTS LONG verdict with its reasons — options as an explicit validation/rejection input to the thesis, not a side stat.
- Options coverage widened: top 8 ranked + watchlist + USO every scan; any
other name gets the same block via
python engine.py lookup T. - IBKR enrichment fields (volume vs avg, P/C, IV percentile, HV30) merge directly into the intel block and its verdict.
New in v0.4
- chart.html — the chart is its own browser tab (
chart.html#TICKER), so you can keep the desk and charts side by side. Interactive: mouse-wheel zoom, drag to pan, double-click reset, DAILY/WEEKLY toggle (2y of data), levels overlay on/off, ✎ NOTE (click the chart to drop a note — auto-saved in the browser, survives re-runs; click a note dot to delete), 💾 SAVE PNG. - Compare mode — add up to 3 tickers in the chart tab (⇄ COMPARE): rebased performance overlay (3M/6M/1Y window) + a side-by-side stats table (setup, checklist, composite, RS, ATR, R:R, thesis, sector rotation, earnings) to pick the best of the candidates.
- Search = full signal card — Enter on any ticker shows the complete intelligence card (thesis, 12-pt checklist, scores, trade plan, options map), identical to the focus cards. The chart is an optional 📈 CHART ↗ button.
- 👁 WATCHLIST tab — add/remove tickers in the UI; saves automatically in
the browser (localStorage) and never changes unless you change it. Every
re-run refreshes each name's signals (setup, checklist, composite, RVOL,
accumulation/distribution, thesis verdict). Names from
watchlist.txt(managed viapython engine.py watch add/remove T) additionally get the daily health verdict (HOLDING/WARNING/BROKEN) and options/GEX coverage — promote a browser-added name to the file when you want full coverage.
New in v0.3 — three-tab dashboard:
- DESK — everything from v0.2 (top 3, signal scans, ranked table, watchlist, earnings).
- MARKET ENV — VIX basket (level/9D/3M term structure, zone, 1y percentile, auto-read), oil complex (WTI trend + energy rotation quadrants + USO flow pointer), short/inverse-ETF basket (SH/SDS/SPXU/PSQ/SQQQ/SOXS/UVXY/VXX dollar-volume hedging gauge with LOW/NORMAL/ELEVATED/PANIC zones).
- FLOW & GEX — per focus name + USO: notable option flow rows (strike,
C/P, expiry/DTE, size @ price, premium, Vol/OI flag, BOUGHT@ASK / SOLD@BID
side inference) with a bull/bear premium tilt, and a strike-by-strike GEX
ladder (teal = absorbs, purple = amplifies, gold ★ = king/pin strike, white
row = spot) with day-over-day OI change chips (appear after 2+ daily scans;
snapshots persist in
data/gex_snapshots/) and auto-commentary about pin structure, floors, and amplifier zones. - Interactive chart screen — click any ticker anywhere (or search + Enter):
candlestick chart (260d) with SMA 20/50/200, volume, and every critical
level drawn & labeled (entry, stop, T1/T2, shelves, fibs, OI walls, gamma
flip, 52w high, 200d-break), plus the thesis comment and trade plan beside
it. Esc to close. Names outside the report →
python engine.py lookup XYZ.
Flow/GEX honesty note: rows are EOD chain approximations (volume vs OI, last-trade vs closing bid/ask), not live tick tape — a real-time flow feed (e.g. via IBKR subscriptions) is the upgrade path if we want intraday.
Long-only equity swing-idea desk tool for a 2-person shop. Scans the broad market daily for the top 3 actionable swing setups, tracks whether existing ideas are holding or breaking down, maps sector rotation, options positioning (walls/GEX), and flags the week's earnings + macro events.
Not investment advice — mechanical screens + rule-based commentary for internal research only.
python engine.py all # ~20s: screener universe + scan + dashboard/chart html
python serve.py # start the desk (leave it running all session)
Then open http://localhost:8741/dashboard.html.
serve.py is what makes lookups automatic: every ticker you search — in the
scan or not — is analyzed live (technicals, options intel, fundamentals) in
about 5 seconds, and merged into data/report.json so it persists. No manual
commands. Same for the chart tab and watchlist adds.
python engine.py lookup XYZ still exists as a CLI equivalent if the server
isn't running.
Then ask Claude:
- "Enrich today's top 3 from IBKR" — pulls live option volume vs average,
P/C, IV + 52w percentile via IBKR MCP →
data/ibkr_enrich.json→ rerunpython engine.py html. Volume ≥2× average gets flagged UNUSUAL. - "Refresh macro.txt" weekly — CPI/FOMC/NFP dates via web search.
- Weekly:
python engine.py export-holdingsthen runpython ../../Portfolio-risk/portfolio_stress_test.py --holdings holdings.csvfor the portfolio-level VaR / crisis-replay view (kept standalone on purpose — it measures the book, not the ideas).
Files you edit: universe.txt (manual adds on top of the auto screener),
watchlist.txt (open ideas, optional entry price), macro.txt (event dates).
Config knobs at the top of engine.py.
yfinance screener (mcap>$10B, 11 sector queries) ─┐ auto universe (150) + universe.txt
Yahoo v8 chart API (2y adjusted OHLCV, threaded) ─┤
Nasdaq earnings calendar (21d, cached daily) ─┼─> engine.py scan ─> data/report.json
yfinance option chains (top3+watchlist) ─┤ │
rotation: RRG math (JdK approx) on 22 ETFs ─┘ v
data/ibkr_enrich.json (Claude/IBKR)
│
engine.py html ───┴──> dashboard.html (static)
- Gates: TR01 $20M dollar-vol · TR03 $5 floor · TR11 220 bars · TR06 earnings blackout (no entry ≤3d before print) · RG01/RG02 200d regime · RG11 ADX · VO03 extension ≤4 ATR
- 12-pt checklist (screenshot-style): MA FAN, 200d RISING, HH/HL, 52W HI, CLOSE HI, U/D VOL, OBV+, VDU, ATR SQZ, NR7, SPRING, W.EMA (+TOP RS swap-in) → tag: ≥8 MOMENTUM BUILDING · 6-7 WATCH · 4-5 TRANSITION · <4 WEAK
- Structure: pivot-clustered support/resistance shelves, base geometry, fib retracements of the 52w swing, NR7, spring (undercut & reclaim)
- Risk: stop = structure (base low − ½ATR) or 2.2 ATR; targets = shelf → 52w high → measured move (base height projected); T1/T2; R:R; shares at risk budget; ±1/2 ATR day bands
- Rotation confluence: ticker's sector ETF quadrant (Leading / Improving / Weakening / Lagging, daily + weekly) — prefer longs in Leading/Improving
- Options map (top ideas + watchlist): call wall, put wall, naive GEX gamma-flip, P/C volume & OI, flow-switch flag (volume tilt fighting OI tilt)
- Thesis comment (rule-based): INTACT / WEAKENING / BROKEN + the exact levels — hold level, invalidation level, trend-break level (200d)
- Trade plan: "if entered today" — stop/T1/T2, expected day range, levels above/below (shelves, fibs, walls, gamma flip), events to watch
- Signal events across the universe with recency: golden/death cross, 8/21 & 13/50 EMA crosses, RSI extremes, new 52w highs
Search bar (any scanned ticker → full card; outside universe → lookup hint), regime banner, macro chips, rotation quadrant boxes (daily/weekly toggle), top-3 focus cards, signal-scan browser with recency chips, ranked table (click row → detail card), watchlist health with thesis text, earnings week.
Two harnesses ship. Prefer backtest_wf.py for anything that matters — it
calls the real scorer.
3y × 147 names, weekly PIT slicing, 5bps round-trip cost, forward 21d excess vs SPY:
| Variant | Q5-Q1 | Sharpe | DSR | PBO |
|---|---|---|---|---|
| base | +1.30% | 0.124 | 0.642 | 0.080 |
| sector-neutral | +1.44% | 0.229 | 0.817 | 0.170 |
| earnings-filtered | +1.29% | 0.125 | 0.650 | 0.182 |
| both | +1.42% | 0.221 | 0.794 | 0.104 |
Q1→Q5 monotonic across all horizons (1/3/5/10/21d), decay curve monotonic
through 21d. PBO < 0.2 across variants means the composite isn't noise-fit.
The sector-neutral lift is the real actionable finding — see REVIEW.md §4c.
Rerun: python backtest_wf.py (~90s after earnings-history cache warm-up).
Kept for historical comparison; it recomputes a vectorized approximation of the scorer (missing structure/participation/gates) and previously reported Q5 +3.94%/21d gross. The walk-forward above shows the real scorer nets roughly half that after costs, which is the whole reason the walk-forward harness now exists.
- FA layer not wired yet (next milestone): EPS/sales growth, margins, guidance revisions — IBKR fundamentals or earnings-call summaries via Claude.
- GEX is a naive approximation (dealers long calls/short puts, one expiry). Real dealer positioning needs flow attribution we don't have.
- Base-pattern classifier is still geometric (no cup-handle/double-bottom recognition).
- Credit spreads (MK07) not wired — add FRED HY-OAS pull.
- Earnings dates come from Nasdaq's calendar — occasionally missing/late for unconfirmed dates; IBKR enrichment should double-check before entries.
- Yahoo endpoints are unofficial; if they break, swap
fetch_historyto IBKRget_price_historyvia Claude — the engine is source-agnostic.