Source code for trader.persistence.index_membership

"""Repository for `index_membership` (issue #27).

A set, not a history: `trader scan-universe` upserts on every scrape, and
`discover_symbols`'s analyst-scan candidate source reads the current members
of one named index.
"""

from collections.abc import Iterable
from datetime import datetime

from sqlalchemy import select
from sqlalchemy.orm import Session, sessionmaker

from trader.persistence.models import IndexMembership

__all__ = ["IndexMembershipRepository"]


[docs] class IndexMembershipRepository: """Writes and reads `index_membership`.""" def __init__(self, session_factory: sessionmaker[Session]) -> None: self._session_factory = session_factory
[docs] def upsert(self, index_name: str, symbols: Iterable[str], *, now: datetime) -> None: """Record `symbols` as the current membership of `index_name`. Existing members not in `symbols` are left as they were — this only ever adds. A departed constituent lingering in `index_membership` after the next scrape costs nothing: it still has to clear `is_strong_buy_consensus` and the full `_evaluate` chain like any other candidate, and `asset_gate` rejects it if it is no longer tradable. """ normalized = {s.strip().upper() for s in symbols if s.strip()} if not normalized: return with self._session_factory() as session: existing = set( session.scalars( select(IndexMembership.symbol).where( IndexMembership.index_name == index_name, IndexMembership.symbol.in_(normalized), ) ).all() ) new_rows = [ IndexMembership(index_name=index_name, symbol=symbol, discovered_at=now) for symbol in normalized - existing ] session.add_all(new_rows) session.commit()
[docs] def members(self, index_name: str) -> list[str]: """Every symbol currently recorded under `index_name`.""" statement = select(IndexMembership.symbol).where( IndexMembership.index_name == index_name ) with self._session_factory() as session: return list(session.scalars(statement).all())