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
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
"""市场数据领域实体(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
|
||||
Reference in New Issue
Block a user