MCP tool: screener_board
Last verified
screener_board is the single tool that answers “what should I long/short right now” and “has the board’s picks actually worked.” One MCP tool, three views, each routing to a distinct public /api/v1/screener/* route — the same surface the dashboard’s screener page reads.
The tool is a thin dispatcher (mcp/mcp_server/tools/public.py:screener_board). view selects the backend route; the candidates view additionally forwards a curated set of the route’s filter params. No mutation, no auth — all three underlying routes are public.
Signature
screener_board(
view: str = "action_board", # "action_board" | "candidates" | "track_record"
# candidates-only filters, all optional:
archetype: str | None = None, # archetype ID (e.g. "A1") or descriptive name
direction: str | None = None, # "long" | "short" | "neutral"
status: str | None = None, # new | persisting | decayed | exited | promoted
sector: str | None = None, # GICS sector, case-insensitive
min_confluence: int | None = None, # >= 1
sort: str = "confluence", # confluence | conviction | score_norm | days_on_list | ticker | status
trade_date: str | None = None, # YYYY-MM-DD ET; omit for today (+ stale-board fallback)
limit: int | None = None, # 1-2000; omit for the trimmed default of 25
active_only: bool = False, # only status in (new, persisting)
s2_only: bool = False, # restrict to the gated S2 winner set
max_tickers: int | None = None, # >= 1; cap to top-N distinct tickers
) -> str # JSON-serialised dict
view outside the three valid values, or a candidates filter outside its bound, raises ValueError before the round-trip — same pattern as analyze_signals’s unknown-aspect guard and compare_to_history’s bound checks. The bounds mirror the backend route’s own Query(ge=..., le=...) constraints (app/screener/routes.py:get_screener_candidates): min_confluence >= 1, 1 <= limit <= 2000, max_tickers >= 1.
Views
| View | Backend route | What it returns |
|---|---|---|
action_board (default) |
GET /api/v1/screener/company-action-board |
The position book — locked long/short pick contracts, plus today’s competing candidates |
candidates |
GET /api/v1/screener/candidates |
The full ranked candidate pipeline, filterable |
track_record |
GET /api/v1/screener/board-track-record |
Forward-graded accuracy of the board’s actually-surfaced picks |
Returns
view="action_board"
{
"schema_version": "2026.08",
"as_of": "2026-07-14",
"regime_band": "elevated",
"regime_note": "Drawdown-risk band elevated; shorts amplified.",
"longs": [
{
"ticker": "AAPL", "archetype": "oversold_sympathy", "archetype_label": "Oversold Sympathy",
"direction": "long", "horizon": 5, "horizon_label": "5 trading days",
"conviction": 71.2, "conviction_tier": "high",
"board_score": 62.1, "edge_weight_tier": "proven",
"trust": { "reasons": [] }, "why": ["confluence 4", "conviction high"], "maturity": "mature",
"contract": {
"locked_trade_date": "2026-07-14", "entry_ref_price": 214.5,
"invalidation_price": 208.0, "horizon_days": 5, "day_k": 1,
"countdown": "day 1/5", "status": "open"
},
"bettability": { "score": 68.0, "chip": "BETTABLE", "gated": false },
"live_signal": null
}
],
"shorts": [],
"competing": [],
"counts": {
"long": 1, "short": 0, "suppressed_long": 0, "suppressed_short": 0,
"suppressed_stale": 0, "open_slots_long": 2, "open_slots_short": 3, "competing": 0
},
"empty_reason": null
}
view="candidates"
{
"trade_date": "2026-07-14",
"count": 1,
"filters": { "archetype": null, "status": null },
"sort": "confluence",
"candidates": [
{
"ticker": "MSFT", "archetype": "oversold_sympathy", "archetype_code": "A2",
"archetype_label": "Oversold Sympathy", "direction": "long_recovery",
"score_norm": 0.71, "confluence_count": 3, "blended_score_norm": 0.68,
"regime_tag": "neutral", "conviction": 55.0, "conviction_tier": "medium",
"sizing_tier": "watching", "rotation_tag": "tailwind",
"first_seen": "2026-07-10", "days_on_list": 4, "status": "persisting",
"rank": 1, "rank_prior": 2, "rank_delta": 1,
"sector": "Technology", "name": "Microsoft Corp"
}
],
"is_stale_fallback": false,
"is_intraday_snapshot": false,
"what_changed": { "prior_trade_date": "2026-07-13", "new_today": [], "requalified": [], "dropped": [], "promoted_today": [] }
}
view="track_record"
{
"schema_version": "2026.07",
"as_of_date": "2026-07-14",
"warming_up": false,
"closed": { "n": 42, "hit_rate": 0.57, "mean_excess": 0.31 },
"open": { "n": 5, "mean_mtm_excess": 0.12 },
"counts": { "runs": 130, "picks_recorded": 210 },
"caveats": { "warming_up": false },
"contracts": { "schema_version": "2026.07", "credibility": "early", "open": {}, "closed": {} }
}
hit_rate and every accuracy fraction in closed / open / contracts is a 0-1 fraction, never 0-100. contracts is the stricter lens — graded on the LOCKED pick contracts held to their actual exit, market-adjusted vs SPY — riding alongside the looser board-picks lens in closed/open. Per DOCTRINE P0, a cold-start substrate returns HTTP 200 with warming_up: true and zero counts, never a fabricated number.
Behaviour
- Cache TTL.
CACHE_TTL_SCREENER = 30s(mcp/mcp_server/config.py). Cache key includes the view and every forwarded filter, socandidatescalls with different filters cache independently. - Trimmed candidates default. The backend route defaults
limitto 200 (its dashboard-page size, ceiling 2000);screener_boarddefaults to 25 whenlimitis omitted — an agent asking “what should I long/short” rarely wants 200 rows in its context. Passlimitexplicitly to see more, up to 2000. - Omit, don’t null, unset filters.
httpxserializes an explicitNonequery param as an empty string rather than dropping it — passingtrade_date=Noneliterally would sendtrade_date=and defeat the backend’s stale-board fallback (it only fires whentrade_dateis genuinely absent). The tool omits unset filters from the request entirely rather than sending them asNone. - Stale-board fallback (candidates, default trade_date only). When
trade_dateis omitted,active_onlyis false, and today’s board is empty (the pipeline runs at 10:30 + 17:30 ET), the route automatically returns the most recent populated board and setsis_stale_fallback: true+requested_trade_dateto today’s date. - Read-only, no auth. All three views are public — no
TRADING_TOKENor internal token required.
Examples
What should I long/short right now?
Operator: "What's the board running today?"
Agent: screener_board() # view="action_board" default
-> read longs[] / shorts[] for the locked picks, competing[] for what's next in line
Browse short candidates in a specific sector
Agent: screener_board(
view="candidates",
direction="short",
sector="Technology",
sort="conviction",
limit=10,
)
Has the board’s picks actually worked?
Operator: "Is this screener actually any good?"
Agent: screener_board(view="track_record")
-> read closed.hit_rate (0-1 fraction) and contracts.closed for the stricter locked-and-held lens
When to use
- An agent or operator asking “what should I long/short” — start with the default
action_boardview. - Filtering or browsing the full candidate pipeline beyond what’s currently locked (
candidates). - Checking whether the board’s historical picks have actually beaten the market (
track_record).
When NOT to use
- You want AI-2’s risk/magnitude read on SPY — use
ai_brief(non-directional; structurally carries no direction/buy-sell field). - You want a specific symbol’s live price — use
get_quote. - You want deep signal-level analysis (regime, alignment, cascade) rather than screener picks — use
analyze_signals.
See also
ai_brief— AI-2’s non-directional risk/magnitude/regime read + its historical grade record, a different surface from the screener’s mechanical picks.market_pulse— market-wide regime context before reading the board.compare_to_history— analogue forward-return distributions, a signals-level (not screener-level) historical read.- Implementation:
mcp/mcp_server/tools/public.py:screener_board, backend routesGET /api/v1/screener/company-action-board,/candidates,/board-track-record(app/screener/routes.py).