- 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
65 lines
2.0 KiB
Python
65 lines
2.0 KiB
Python
"""领域实体测试:字段约束与「防未来函数」可见性判断。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import date
|
|
from decimal import Decimal
|
|
|
|
import pytest
|
|
from app.domain.entities.market import (
|
|
DailyBar,
|
|
FinancialIndicator,
|
|
Stock,
|
|
SyncLog,
|
|
)
|
|
from pydantic import ValidationError
|
|
|
|
|
|
class TestStock:
|
|
def test_symbol_pattern_enforced(self) -> None:
|
|
Stock(symbol="600519.SH", name="贵州茅台", list_date=date(2001, 8, 27))
|
|
with pytest.raises(ValidationError):
|
|
Stock(symbol="600519", name="x", list_date=date(2001, 1, 1))
|
|
with pytest.raises(ValidationError):
|
|
Stock(symbol="sh600519", name="x", list_date=date(2001, 1, 1))
|
|
|
|
|
|
class TestDailyBar:
|
|
def test_is_complete(self) -> None:
|
|
bar = DailyBar(
|
|
symbol="600519.SH",
|
|
trade_date=date(2024, 1, 2),
|
|
open=Decimal("100"),
|
|
high=Decimal("101"),
|
|
low=Decimal("99"),
|
|
close=Decimal("100.5"),
|
|
volume=Decimal("10000"),
|
|
amount=Decimal("1000000"),
|
|
)
|
|
assert bar.is_complete
|
|
assert not DailyBar(symbol="600519.SH", trade_date=date(2024, 1, 2)).is_complete
|
|
|
|
|
|
class TestFinancialIndicator:
|
|
def _fin(self, announce: date) -> FinancialIndicator:
|
|
return FinancialIndicator(
|
|
symbol="600519.SH",
|
|
report_date=date(2024, 6, 30),
|
|
announce_date=announce,
|
|
eps=Decimal("1.2"),
|
|
)
|
|
|
|
def test_announced_by_after_announce(self) -> None:
|
|
fin = self._fin(date(2024, 8, 31))
|
|
# 公告日当天已可见;公告前不可见
|
|
assert fin.announced_by(date(2024, 8, 31))
|
|
assert not fin.announced_by(date(2024, 8, 30))
|
|
assert not fin.announced_by(date(2024, 6, 30)) # 报告期不代表公开
|
|
|
|
|
|
class TestSyncLog:
|
|
def test_defaults(self) -> None:
|
|
log = SyncLog(source="tushare", api="daily", success=True, row_count=5)
|
|
assert log.request_time is not None
|
|
assert log.data_start is None
|