trader.discovery.scan module

Turning configured themes — and, since issue #27, analyst-scan candidates — into tradable symbols.

Never raises. A search outage, a rate limit, a malformed response, an analyst lookup timing out — each is caught, logged, and yields fewer candidates rather than an exception. The fixed tickers must keep trading through a discovery failure, which is the same discipline that makes every LLM failure resolve to HOLD.

Failures are isolated per theme and per candidate: one broken theme does not discard another theme’s results, and one symbol whose analyst call fails does not discard its four siblings. The analyst-scan source (issue #27) follows the identical discipline and is isolated from theme search in both directions: a broken scan never loses a theme hit, and vice versa.

trader.discovery.scan.ANALYST_SCAN_INDEX = 'sp500'

trader scan-universe supports only this index today (issue #27’s Open Question #4 defers which others); discover_symbols reads the same one.

trader.discovery.scan.discover_symbols(*, settings, search, analysts, broker, bars_for, fixed_symbols, now, event_repository=None, index_repository=None, consensus_repository=None)[source]

Search every theme, plus (issue #27) whatever the analyst scan found.

index_repository/consensus_repository are optional and, like event_repository, duck-typed rather than imported as concrete persistence classes — discovery/ stays free of a persistence/ dependency. Left None (every existing caller), the analyst-scan source simply contributes nothing, the same degrade themes: [] already gives theme search — neither source blocks the other.

Parameters:
  • settings (DiscoverySettings)

  • bars_for (Callable[[str], Sequence[Bar]])

  • fixed_symbols (Collection[str])

  • now (datetime)

Return type:

list[Candidate]

trader.discovery.scan.screen_for_entry(symbols, *, settings, analysts, broker, bars_for)[source]

Which of these symbols must not be bought, and why.

The operator’s own fixed_tickers run through the same gates a discovered candidate does — “I don’t know that these are good purchases, I only know I am interested in watching them”. A symbol that fails is still evaluated and still records a decision; only the entry is blocked, which is what keeps a watchlist a watchlist.

Returns {SYMBOL: FilterOutcome} for failures only, the whole outcome rather than just its reason text: run_once writes outcome.reason into decisions.reasoning (for a human) and outcome.code into decisions.rejection_reason (for a GROUP BY), and a single object is what keeps those two in sync — two parallel dicts keyed by the same symbols would be the “two fields that can contradict each other” shape this codebase’s own architecture review flags.

Never raises: a screening outage must not block trading, so a symbol whose lookups fail is simply not blocked — the same fail-open direction discover_symbols takes, and the opposite of the exposure seed, which fails closed because guessing low there would spend money.

Return type:

dict[str, FilterOutcome]