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:
Simon
2026-09-06 16:59:28 +08:00
parent 7a89d97c0b
commit 2da234220a
23 changed files with 2666 additions and 1 deletions
@@ -0,0 +1,178 @@
"""phase1 market data tables
Revision ID: e4d188250fb2
Revises:
Create Date: 2026-09-06 16:58:13.904265
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "e4d188250fb2"
down_revision: str | None = None
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table(
"adjust_factor",
sa.Column(
"id",
sa.BigInteger().with_variant(sa.Integer(), "sqlite"),
autoincrement=True,
nullable=False,
),
sa.Column("symbol", sa.String(length=12), nullable=False),
sa.Column("trade_date", sa.Date(), nullable=False),
sa.Column("factor", sa.Numeric(precision=20, scale=6), nullable=False),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("symbol", "trade_date", name="uq_adj_symbol_date"),
)
with op.batch_alter_table("adjust_factor", schema=None) as batch_op:
batch_op.create_index(batch_op.f("ix_adjust_factor_symbol"), ["symbol"], unique=False)
batch_op.create_index(
batch_op.f("ix_adjust_factor_trade_date"), ["trade_date"], unique=False
)
op.create_table(
"financial_indicator",
sa.Column(
"id",
sa.BigInteger().with_variant(sa.Integer(), "sqlite"),
autoincrement=True,
nullable=False,
),
sa.Column("symbol", sa.String(length=12), nullable=False),
sa.Column("report_date", sa.Date(), nullable=False),
sa.Column("announce_date", sa.Date(), nullable=False),
sa.Column("eps", sa.Numeric(precision=12, scale=4), nullable=True),
sa.Column("roe", sa.Numeric(precision=10, scale=4), nullable=True),
sa.Column("total_revenue", sa.Numeric(precision=24, scale=2), nullable=True),
sa.Column("net_profit", sa.Numeric(precision=24, scale=2), nullable=True),
sa.Column("gross_margin", sa.Numeric(precision=10, scale=4), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("symbol", "report_date", "announce_date", name="uq_fin_sym_rep_ann"),
)
with op.batch_alter_table("financial_indicator", schema=None) as batch_op:
batch_op.create_index(
batch_op.f("ix_financial_indicator_announce_date"), ["announce_date"], unique=False
)
batch_op.create_index(
batch_op.f("ix_financial_indicator_report_date"), ["report_date"], unique=False
)
batch_op.create_index(batch_op.f("ix_financial_indicator_symbol"), ["symbol"], unique=False)
op.create_table(
"stock",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("symbol", sa.String(length=12), nullable=False),
sa.Column("name", sa.String(length=64), nullable=False),
sa.Column("industry", sa.String(length=64), nullable=True),
sa.Column("area", sa.String(length=32), nullable=True),
sa.Column("market", sa.String(length=16), nullable=True),
sa.Column("exchange", sa.String(length=8), nullable=True),
sa.Column("list_date", sa.Date(), nullable=False),
sa.Column("delist_date", sa.Date(), nullable=True),
sa.Column("status", sa.String(length=8), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
with op.batch_alter_table("stock", schema=None) as batch_op:
batch_op.create_index(batch_op.f("ix_stock_symbol"), ["symbol"], unique=True)
op.create_table(
"stock_daily",
sa.Column(
"id",
sa.BigInteger().with_variant(sa.Integer(), "sqlite"),
autoincrement=True,
nullable=False,
),
sa.Column("symbol", sa.String(length=12), nullable=False),
sa.Column("trade_date", sa.Date(), nullable=False),
sa.Column("open", sa.Numeric(precision=12, scale=4), nullable=True),
sa.Column("high", sa.Numeric(precision=12, scale=4), nullable=True),
sa.Column("low", sa.Numeric(precision=12, scale=4), nullable=True),
sa.Column("close", sa.Numeric(precision=12, scale=4), nullable=True),
sa.Column("volume", sa.Numeric(precision=24, scale=2), nullable=True),
sa.Column("amount", sa.Numeric(precision=24, scale=2), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("symbol", "trade_date", name="uq_daily_symbol_date"),
)
with op.batch_alter_table("stock_daily", schema=None) as batch_op:
batch_op.create_index(batch_op.f("ix_stock_daily_symbol"), ["symbol"], unique=False)
batch_op.create_index(batch_op.f("ix_stock_daily_trade_date"), ["trade_date"], unique=False)
op.create_table(
"sync_log",
sa.Column(
"id",
sa.BigInteger().with_variant(sa.Integer(), "sqlite"),
autoincrement=True,
nullable=False,
),
sa.Column("source", sa.String(length=16), nullable=False),
sa.Column("api", sa.String(length=32), nullable=False),
sa.Column("request_time", sa.DateTime(), nullable=False),
sa.Column("success", sa.Boolean(), nullable=False),
sa.Column("failure_reason", sa.String(length=500), nullable=True),
sa.Column("row_count", sa.Integer(), nullable=False),
sa.Column("data_start", sa.Date(), nullable=True),
sa.Column("data_end", sa.Date(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
with op.batch_alter_table("sync_log", schema=None) as batch_op:
batch_op.create_index(batch_op.f("ix_sync_log_source"), ["source"], unique=False)
op.create_table(
"trading_calendar",
sa.Column("id", sa.Integer(), nullable=False),
sa.Column("calendar_date", sa.Date(), nullable=False),
sa.Column("is_open", sa.Boolean(), nullable=False),
sa.PrimaryKeyConstraint("id"),
)
with op.batch_alter_table("trading_calendar", schema=None) as batch_op:
batch_op.create_index(
batch_op.f("ix_trading_calendar_calendar_date"), ["calendar_date"], unique=True
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("trading_calendar", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_trading_calendar_calendar_date"))
op.drop_table("trading_calendar")
with op.batch_alter_table("sync_log", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_sync_log_source"))
op.drop_table("sync_log")
with op.batch_alter_table("stock_daily", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_stock_daily_trade_date"))
batch_op.drop_index(batch_op.f("ix_stock_daily_symbol"))
op.drop_table("stock_daily")
with op.batch_alter_table("stock", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_stock_symbol"))
op.drop_table("stock")
with op.batch_alter_table("financial_indicator", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_financial_indicator_symbol"))
batch_op.drop_index(batch_op.f("ix_financial_indicator_report_date"))
batch_op.drop_index(batch_op.f("ix_financial_indicator_announce_date"))
op.drop_table("financial_indicator")
with op.batch_alter_table("adjust_factor", schema=None) as batch_op:
batch_op.drop_index(batch_op.f("ix_adjust_factor_trade_date"))
batch_op.drop_index(batch_op.f("ix_adjust_factor_symbol"))
op.drop_table("adjust_factor")
# ### end Alembic commands ###