Files
qlib/backend/app/domain/repositories/market.py
T
Simon 23972e7063 feat: 股息率案例口径 + 策略库与图表统一 + 回测存档完整化
汇总三轮未提交的开发(每轮均在本机 MariaDB + 真实浏览器上验证):

1) 股息率案例(全市场股息率最高 n 只,默认 20,每 m 月择股)
   - 新增日频估值表 daily_basic + 迁移;股息率因子(dv_ratio / dividend_yield / TTM)
   - 名称历史表 stock_name_history:剔除 ST 按**择股日当时名称**判定,消除
     「曾高股息后 ST」的股息陷阱(实测 3.70pp 偏差)
   - 区间择股/调仓双周期(m 择股 / y 调仓)、指数成分与白名单、停牌近似剔除
   - 复权因子口径核对(4,164,742 行、缺失 0.0%)、收盘价成交与涨跌停拦单
   - 案例实测:2020-01-01~2026-09-04 总收益 +24.86%(年化 3.52%、回撤 -28.58%)

2) 策略库与前端统一
   - strategy 表 + CRUD/PUT 原地更新 + `describe_strategy` 按 spec 真实推导
     「一句话说明 + 计算公式 + 执行步骤 + 注意事项」(与引擎实执行规则同源)
   - 任何出现股票代码处都成对显示名称且可点击进个股页
   - 全站图表基座统一 TradingView Lightweight Charts(ECharts 依赖、
     锁文件、组件与文档标注一并清除),买卖点标记只落在真实交易日上

3) 回测存档完整化(可往复查看)
   - 同步端点(POST /api/backtests、/api/factor-tests)此前完全不落库 → 现在同样归档,
     归档 id 经响应头 X-Experiment-Id 返回(不破坏 response_model)
   - data_version 首次真实写入(数据快照指纹:最新交易日 + 各表规模)
   - 个股收益曲线默认**全量保存**(此前硬截断 60 只);超出体积预算才裁剪,
     并写 archive_meta(机器可读)+ unimplemented(人可读)如实标注
   - 列表 kind/q 过滤 + X-Total-Count(此前 limit=50 静默截断)、DELETE 归档
   - 只读归档页 /experiments/{id}(Server Component,SSR 直出**选股条件**与
     **交易执行依据**);结果视图按 kind 分发(backtest/factor_test/selection),
     非回测归档不套用回测口径
   - 新增 CLI:prune_experiments(保留策略,默认 dry-run)、
     restore_experiment_from_job(从 Job 副本按原 id 重建被删的历史归档,默认 dry-run)

门禁:pytest 388 passed、ruff All checks passed、tsc 0 错误、图表单测 7 passed、
next build 成功、契约脚本 verify_strategy_workspace 59/59(含按 kind 逐类验证归档页)。
2026-09-20 07:31:04 +08:00

177 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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]: ...