feat(universe): B1-1 指数历史成分(index_weight)+ Universe 按 as_of 成分过滤

- index_weight 表(migration f5e0d1c2b3a4,MySQL 已应用;index_code+date+symbol 唯一)
  + IndexWeight 实体 + IndexConstituentRepository(members_at:取 <=as_of 最近一期快照,
  Survivorship-free / 无未来成分;latest_date)
- UniverseSpec.index_code + universe.filter_stocks members 交集 + resolve_members;
  Research/Selection/Signal/Replay 服务注入 index repo(历史成分过滤,选股/回测共用)
- tests/test_index_universe.py:快照历史成分(成分变更不入早期结果)、幂等、
  空快照期空集、index_code 过滤下 as_of 一致性;全量 pytest 通过
This commit is contained in:
Simon
2026-09-09 07:27:13 +08:00
parent 03fb463216
commit 9cc4bfccac
13 changed files with 379 additions and 16 deletions
@@ -0,0 +1,46 @@
"""index_weight 表(B1 指数历史成分)
Revision ID: f5e0d1c2b3a4
Revises: e1f2a3b4c5d6
Create Date: 2026-09-09
"""
from __future__ import annotations
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "f5e0d1c2b3a4"
down_revision: str | None = "e1f2a3b4c5d6"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def upgrade() -> None:
op.create_table(
"index_weight",
sa.Column(
"id",
sa.BigInteger().with_variant(sa.Integer(), "sqlite"),
autoincrement=True,
nullable=False,
),
sa.Column("index_code", sa.String(length=12), nullable=False),
sa.Column("index_name", sa.String(length=64), nullable=True),
sa.Column("trade_date", sa.Date(), nullable=False),
sa.Column("symbol", sa.String(length=12), nullable=False),
sa.Column("weight", sa.Numeric(precision=10, scale=6), nullable=True),
sa.PrimaryKeyConstraint("id"),
sa.UniqueConstraint("index_code", "trade_date", "symbol", name="uq_idxw_code_date_sym"),
)
op.create_index("ix_index_weight_index_code", "index_weight", ["index_code"])
op.create_index("ix_index_weight_trade_date", "index_weight", ["trade_date"])
def downgrade() -> None:
op.drop_index("ix_index_weight_trade_date", table_name="index_weight")
op.drop_index("ix_index_weight_index_code", table_name="index_weight")
op.drop_table("index_weight")