"""Repository Protocol(Phase 1 数据层)。 业务层只依赖这些 Protocol;具体实现位于 infrastructure/persistence。 实体一律以 domain.entities 类型进出,禁止把 ORM Model 泄漏到上层。 """ from __future__ import annotations from collections.abc import Iterator, Sequence from datetime import date from typing import Protocol from app.domain.entities.market import ( AdjustFactor, DailyBar, DailyBasic, FinancialIndicator, Stock, StockNameHistory, SyncLog, TradingCalendar, ) class StockRepository(Protocol): def get_by_symbol(self, symbol: str) -> Stock | None: ... def list(self) -> list[Stock]: ... def upsert_many(self, stocks: Sequence[Stock]) -> int: """批量写入,以 symbol 为幂等键,返回写入/更新的行数。""" class TradingCalendarRepository(Protocol): def upsert_many(self, days: Sequence[TradingCalendar]) -> int: ... def list_range(self, start: date, end: date) -> list[TradingCalendar]: ... def is_open(self, day: date) -> bool: ... class DailyBarRepository(Protocol): def upsert_many(self, bars: Sequence[DailyBar]) -> int: ... def get_range(self, symbol: str, start: date, end: date) -> list[DailyBar]: ... def get_range_many( self, symbols: Sequence[str], start: date, end: date, adjust: str = "none", ) -> list[DailyBar]: """批量区间查询(研究装配面板用);adjust 指定行情口径(none 不复权 / qfq)。""" def stream_range_many_columns( self, symbols: Sequence[str], start: date, end: date, columns: Sequence[str], adjust: str = "none", ) -> Iterator[tuple]: """流式(分批 yield)返回 symbol, trade_date(iso str), 数值列(float) 元组。 研究装配大数据面板专用:只 SELECT 所需列并在 SQL 侧转 REAL, 避免 ORM 对象 / Decimal 全量物化(内存大头,见内存优化专项)。 实现可选——ResearchService 会对缺失该方法的老实现回退到 get_range_many。 """ def latest_date(self, symbol: str) -> date | None: """断点续传用:该股票本地已有数据的最新交易日。""" class AdjustFactorRepository(Protocol): def upsert_many(self, factors: Sequence[AdjustFactor]) -> int: ... def get_range(self, symbol: str, start: date, end: date) -> list[AdjustFactor]: ... class DailyBasicRepository(Protocol): """每日指标(估值 / 股息率 / 市值)仓储。 幂等键 (symbol, trade_date)。研究侧一律按 trade_date <= as_of 取用, 禁止用未来时点的指标回填历史(AGENT.md §9)。 """ def upsert_many(self, rows: Sequence[DailyBasic]) -> int: ... def get_range(self, symbol: str, start: date, end: date) -> list[DailyBasic]: ... def get_range_many( self, symbols: Sequence[str], start: date, end: date, ) -> list[DailyBasic]: """批量区间查询(接口对齐 DailyBarRepository)。""" def stream_range_many_columns( self, symbols: Sequence[str], start: date, end: date, columns: Sequence[str], ) -> Iterator[tuple]: """流式返回 symbol, trade_date(iso str), *数值列(float)。 研究装配面板专用(避免 Decimal / ORM 对象全量物化)。 """ def latest_date(self) -> date | None: """本表全局最新交易日(增量同步断点)。""" def missing_dates(self, start: date, end: date) -> list[date]: """区间内「交易日历为开市、但本表无任何行」的交易日(补齐用)。""" class StockNameHistoryRepository(Protocol): """股票名称变更历史仓储(时点 ST / 风险警示判定)。 用于把 `universe.exclude_st` 从「最新名称快照」升级为**时点名称**: 研究侧必须按 as_of 取当时生效的名称,否则「曾为高股息、后来才 ST/退市」的 股息陷阱样本会被整段排除(实测影响约 3.70pp 收益)。 """ def upsert_many(self, rows: Sequence[StockNameHistory]) -> int: ... def names_as_of( self, symbols: Sequence[str], as_of: date ) -> dict[str, str]: """返回 as_of 时点生效的名称(缺该股记录则不返回该键,由调用方回退最新名称)。""" def name_spans(self, symbols: Sequence[str]) -> dict[str, list[tuple[date, date | None, str]]]: """返回各股票的名称生效区间列表 [(start, end, name)](批量时点查询复用)。""" def count_rows(self) -> int: """本表总行数(未同步时用于降级回退最新名称)。""" def namechange_dates(self) -> tuple[date | None, date | None]: """已同步的 (最早 start_date, 最晚 start_date)(增量断点)。""" class FinancialRepository(Protocol): def upsert_many(self, rows: Sequence[FinancialIndicator]) -> int: ... def list_symbol(self, symbol: str) -> list[FinancialIndicator]: """该股票本地全部财务行(增量判断 / 新浪校验重叠用,量级小)。""" def has_report_period(self, symbol: str, report_date: date) -> bool: """本地是否已含该报告期(最新应披露报告期是否已入库)。""" def list_announced( self, symbol: str, as_of_date: date, report_start: date | None = None, ) -> list[FinancialIndicator]: """只返回 announce_date <= as_of_date 的记录 —— 未来函数红线。""" def list_announced_many( self, symbols: Sequence[str], as_of_date: date, ) -> list[FinancialIndicator]: """批量版:返回这些股票 announce_date <= as_of_date 的全部记录。 供选股/截面研究一次性取财务字段(调用方按需取每 symbol 最新一版)。 实现可选 —— 未提供时 SelectionService 回退逐只 list_announced。 """ class SyncLogRepository(Protocol): def add(self, log: SyncLog) -> SyncLog: ... def recent(self, source: str | None = None, limit: int = 20) -> list[SyncLog]: ...