Skip to content

KB/operations

Screener Pipeline & Lifecycle (Operations)

Last verified

This is the how. For the vision behind it — why a floating funnel, the two-axis model, the predictive payoff — see The Screener System (vision). For what each pattern looks for, see the archetype pages.

Status convention. Sections tagged LIVE describe shipped behavior; BUILDING describes the target this doc is the spec for, with code converging to it. The distinction is kept honest on purpose — the doc is the contract, not a press release.

The funnel, stage by stage

A screener pass (app/screener/pipeline.py:run_screener) walks five steps:

  1. Universe selection LIVE — resolved from screener_config.json → universe.tier (sp500 | sp900, default sp900 = ~903 names) by app/screener/companies.py:universe_tickers(), sourced from app/data/sp900_companies.json (membership-tagged sp500 / sp400). S1 is OHLCV-only — the S&P 900 is index-curated liquid, so gate_pass=True by membership; no per-name .info quality fetch in the sweep.
  2. Feature gathering LIVEbuild_features_for_universe() downloads OHLCV for every universe member and persists to screener_features_daily (so historical replay isn’t starved).
  3. Archetype scoring LIVEscore_universe() runs the registered evaluators, emitting one row per (ticker, archetype) that reaches partial or primed.
  4. Ranking & confluence LIVErank_candidates() dedupes by ticker (consolidating multi-archetype fires into one row with confluence_count) and sorts by confluence_count desc, then blended_score_norm desc.
  5. Persistence LIVE — ranked rows land in screener_candidates.

The gate dial (target: ~20 survivors, no hard cap) BUILDING

The shortlist size is governed by the gates, not a truncation. The S1→S2 funnel config lives in screener_config.json → funnel.s1_to_s2 (max_promote_count, min_confluence_score, include_watchlist). The target is to tune these so a normal pass naturally leaves ~20 names — intense enough that only genuine setups clear, loose enough that the board is rarely empty.

Known bug being fixed: the universe API route currently hardcodes sp500_only=True, so the board shows 503 “S&P 500 members” instead of the configured ~903 S&P 900. The DB already holds all 903 rows; only the read path and its UI label are wrong. Fix resolves the tier from config.

Archetypes

Each S1 name is run through every registered archetype evaluator (app/screener/archetypes.py). Archetype IDs are the canonical descriptive strings (oversold_sympathy, positioning_extreme, …); the short codes A1–A6 / N1–N3 are display metadata only (ARCHETYPE_META).

Code ID Direction Status
A1 post_earnings_overreaction long LIVE (wired)
A2 oversold_sympathy long LIVE (wired)
A3 positioning_extreme long LIVE (wired)
A4 precatalyst_coiling long BUILDING (coded, not yet registered)
A5 mechanical_reversion long LIVE (wired)
A6 vol_mispricing long BUILDING (coded, not yet registered)
A7 earnings_dealer_unwind long LIVE — paper-tracking only (async deep-path)
N1 distribution_top short BUILDING (design)
N2 overextended_blowoff short BUILDING (design)
N3 failed_breakout short BUILDING (design)

A4 and A6 evaluators exist in code but are absent from _SYNC_ARCHETYPE_EVALUATORS, so they don’t fire yet — wiring them is part of the build-out. The N* short archetypes are designed (see their pages) and not yet implemented. Full per-pattern detail: /kb/archetype.

A7 is an async deep-path candidate-GENERATOR — deliberately absent from _SYNC_ARCHETYPE_EVALUATORS (it needs DB reads for its dealer-net-delta history

Options legs are sparse by design. The A3 (skew), A4 (IV-percentile), and A6 (implied-move) legs read the latest co_options_daily row, which is watchlist-only (populated by scaffold_company.py, never for the full universe). For the vast majority of S1 names these legs abstain — contribute nothing, no error. Only scaffolded / promoted names carry an options row.

Both feature paths must agree. Every new screener feature is wired into BOTH the universe sweep (build_features_for_universe) and the per-ticker deep path (score_a*_for_tickerget_features_as_of). Drift between them shipped a 0-candidate bug once; tests/test_feature_path_parity.py enforces parity.

Promotion & demotion lifecycle

Candidate status machine LIVE

screener_candidates status values: newpersistingdecayedexited (terminal) or promoted (terminal). Transitions are always UPDATEs, never DELETEs. Per-archetype relevance windows (trading days) are the SSOT in screener_config.json § lifecycle.relevance_windows. Access module: app/signals/screener_candidates.py.

Manual promotion to the watchlist LIVE

POST /api/v1/screener/promote transitions a candidacy to promoted, INSERT OR IGNOREs a minimal co_entity row so the name appears on /company/watchlist immediately, then launches scripts/scaffold_company.py as a detached subprocess (start_new_session=True) so the scaffold’s known teardown hang can’t wedge the API worker.

The two-axis lifecycle (target) BUILDING

This is where the vision’s two axes — automatic data tier × manual visibility — become concrete:

Refresh cadences by tier

Tier What refreshes Cadence Status
S1 OHLCV for archetype scoring per screener pass (daily) LIVE
S2-active or pinned deep: options chain, estimates, sentiment, gamma daily (scaffold / screener run) LIVE
S2-active or pinned hot quote: last / OHLC / volume / bid-ask every 5 min, RTH-gated BUILDING
auto-demoted, not pinned cheap daily price bar only daily BUILDING

The 5-min watchlist quote refresh mirrors SPY’s hot_looper but runs as a separate looper (watchlist_hot_looper, target) — a watchlist-quote failure must never stall SPY’s macro signals. One brokerage batch quote covers the whole union (pinned ∪ S2-active), so the rate cost is one request per tick. A refreshed spot price additionally lets the gamma rail recompute its zone + distance-to-walls intraday from the stored walls — no options chain re-fetch.

Data tables & retention

Retention is sacred — every screener + company table is keep-forever by default. A DELETE against any of them without a RETENTION_*_DAYS env-var gate is a P0 revert (tests/test_retention_guard.py enforces). The append-only archetype-fire history across screener_candidates / screener_predictions is the substrate the predictive layer reads — see the vision page.

Core tables: screener_universe, screener_features_daily, screener_candidates, screener_predictions, plus the company-side co_entity, co_price_daily, co_options_daily, co_option_strikes_daily, gamma_profile_daily, co_estimates / co_estimates_periods, co_sentiment_daily / co_sentiment_snapshots, co_ownership, universe_price_daily. Full retention table + env-var overrides live in the repo’s CLAUDE.md.

Where pitfalls arise