Files
qlib/backend/app/domain/entities/market.py
T
Simon 2da234220a feat(backend): Phase 1 数据层 — Domain / Provider / Failover 审计 + 持久化 + 同步 CLI
- domain:市场数据实体(Stock / 交易日历 / 日线 / 复权 / 财务含 announce_date)+ Repository 与 MarketDataProvider Protocol
- 数据源:TushareProvider(归一化、重试、鉴权错误归类)、SinaProvider(备用,明确前复权口径与能力边界)、FailoverProvider + SyncLog 审计(禁止静默切换)
- 持久化:SQLAlchemy 2.x Models + Repository 实现(按业务键幂等 upsert、as_of_date 防未来函数过滤)+ Alembic 迁移
- CLI:uv run python -m app.cli.sync {basic|calendar|daily|financial|verify},支持 --resume 断点续传
- 真实 Tushare 验证:stock 5556 / 交易日历 366 / daily+factor 242 / 财务 55;sync_log 审计完整
- 测试:38 passed(domain / provider / failover / repository / 未来函数 / 迁移),ruff clean
2026-09-06 16:59:28 +08:00

105 lines
3.3 KiB
Python

"""市场数据领域实体(Phase 1)。
约定(AGENT.md §8/§9):
- 行情时间用 trade_date;财务数据同时区分 report_date(报告期)与 announce_date(公告日)
- 禁止以 report_date 作可见性依据 —— 只允许 announce_date 已过的数据进入研究
- 复权一律通过独立 AdjustFactor 表达,不在此层偷偷改前/后复权口径
"""
from __future__ import annotations
from datetime import date, datetime
from decimal import Decimal
from pydantic import BaseModel, ConfigDict, Field
# 常见精度:价格 4 位小数;成交量(股) 2 位;金额(元) 2 位
PRICE_PLACES = Decimal("0.0001")
AMOUNT_PLACES = Decimal("0.01")
class Stock(BaseModel):
"""A 股基础信息。symbol 统一为 Tushare 风格,如 600519.SH。"""
model_config = ConfigDict(str_strip_whitespace=True)
symbol: str = Field(pattern=r"^\d{6}\.(SH|SZ|BJ)$", description="如 600519.SH")
name: str
industry: str | None = None
area: str | None = None
market: str | None = Field(default=None, description="主板/创业板/科创板/北交所")
exchange: str | None = None
list_date: date
delist_date: date | None = None
status: str = Field(default="L", description="L 上市 / D 退市 / P 暂停")
class TradingCalendar(BaseModel):
"""交易日历。"""
calendar_date: date
is_open: bool = True
class DailyBar(BaseModel):
"""不复权日线。复权请使用 AdjustFactor 在消费侧显式计算。"""
symbol: str
trade_date: date
open: Decimal | None = None
high: Decimal | None = None
low: Decimal | None = None
close: Decimal | None = None
volume: Decimal | None = Field(default=None, description="成交量(股)")
amount: Decimal | None = Field(default=None, description="成交额(元)")
@property
def is_complete(self) -> bool:
"""基础行情字段是否齐全(供校验器使用)。"""
return all(
v is not None
for v in (self.open, self.high, self.low, self.close, self.volume, self.amount)
)
class AdjustFactor(BaseModel):
"""复权因子。因子原始口径由数据源决定,必须与数据源文档一致地存取。"""
symbol: str
trade_date: date
factor: Decimal
class FinancialIndicator(BaseModel):
"""核心财务指标(快照)。
可见性红线:研究侧查询一律按 announce_date <= as_of_date 过滤,
report_date 只表示报告所属期间,不代表公开时间。
"""
symbol: str
report_date: date
announce_date: date
eps: Decimal | None = None
roe: Decimal | None = None
total_revenue: Decimal | None = None
net_profit: Decimal | None = None
gross_margin: Decimal | None = None
def announced_by(self, as_of_date: date) -> bool:
"""as_of_date(含当日)是否已可见。防未来函数的核心判断。"""
return self.announce_date <= as_of_date
class SyncLog(BaseModel):
"""数据拉取审计记录(AGENT.md §7:来源必须可追踪,禁止静默切换)。"""
source: str
api: str
request_time: datetime = Field(default_factory=datetime.utcnow)
success: bool
failure_reason: str | None = None
row_count: int = 0
data_start: date | None = None
data_end: date | None = None