Files
qlib/backend/app/domain/entities/market.py
T
Simon 56254172b3 feat(data): Tushare 限速退避 + 新浪兜底(财务 getFinanceReport2022 / 日K 前复权),source+adjust 口径标记
- TushareProvider:频率超限按指数退避重试(不再一次 200/min 即中断),最长等待 30s
- SinaProvider 重构(参考 cc-cursor 公开接口实现):
  · 新增财务通道 CompanyFinanceService.getFinanceReport2022(source=gjzb) → FinancialIndicator
    (report_date / announce_date=publish_date),与 Tushare fina_indicator schema 一致
  · 日 K 保留 jsonp(前复权),统一 UA + 重试
  · 不支持方法仍抛 DataSourceNotSupported(复权因子/交易日历/基础信息)
- FailoverProvider 现在可对 daily 与 financial 兜底(CLI _failover_provider 接 SinaProvider)
- DailyBar + stock_daily 表新增 source/adjust 列:新浪兜底行标记 sina/qfq,
  Tushare 恢复后 --resume 按同键覆盖回不复权 → 两源格式一致且可追溯
- 迁移 91c4e27a03fb 已生成;执行需在全市场同步结束后:uv run alembic upgrade head
- 测试 34+ 项(新浪财务解析/格式一致/限速退避等)通过
2026-09-06 20:52:13 +08:00

113 lines
3.8 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):
"""日线。默认不复权(source=tushare, adjust=none)。
备用源兜底行会标记 source=sina、adjust=qfq(新浪返回前复权价)。
字段统一、可区分、可追溯(AGENT §5.2/§8):研究侧应优先消费
source=tushare 且 adjust=none 的行;新浪行仅在 Tushare 不可用期间作为兜底,
Tushare 恢复后重跑 --resume 会按日覆盖回不复权口径。
"""
symbol: str
trade_date: date
source: str = Field(default="tushare", description="tushare | sina")
adjust: str = Field(default="none", description="none 不复权 | qfq 前复权")
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