Merge branch 'develop' into codex/point
This commit is contained in:
@@ -0,0 +1,234 @@
|
||||
"""Create replayable independent sector radar tables."""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
revision: str = "0004_sector_radar"
|
||||
down_revision: str | None = "0003_market_integrity_checks"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Create source, point-in-time fact, publication, and ranking tables."""
|
||||
|
||||
op.create_table(
|
||||
"sector_radar_source_snapshot",
|
||||
sa.Column("id", sa.String(64), primary_key=True),
|
||||
sa.Column("api_name", sa.String(32), nullable=False),
|
||||
sa.Column("normalized_params", JSONB, nullable=False),
|
||||
sa.Column("target_trade_date", sa.Date()),
|
||||
sa.Column("partition_key", sa.String(64)),
|
||||
sa.Column("observed_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("payload", JSONB, nullable=False),
|
||||
sa.Column("row_count", sa.Integer(), nullable=False),
|
||||
sa.Column("returned_fields", JSONB, nullable=False),
|
||||
sa.Column("content_sha256", sa.String(64), nullable=False),
|
||||
sa.Column("row_limit", sa.Integer()),
|
||||
sa.Column("limit_reached", sa.Boolean(), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("now()"),
|
||||
),
|
||||
sa.CheckConstraint("row_count >= 0", name="ck_sector_radar_source_row_count"),
|
||||
sa.CheckConstraint(
|
||||
"row_limit IS NULL OR row_limit > 0", name="ck_sector_radar_source_limit"
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_sector_radar_source_api_date",
|
||||
"sector_radar_source_snapshot",
|
||||
["api_name", "target_trade_date", "observed_at"],
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"sector_radar_membership",
|
||||
sa.Column(
|
||||
"source_snapshot_id",
|
||||
sa.String(64),
|
||||
sa.ForeignKey("sector_radar_source_snapshot.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("trade_date", sa.Date(), nullable=False),
|
||||
sa.Column("sector_type", sa.String(16), nullable=False),
|
||||
sa.Column("sector_code", sa.String(16), nullable=False),
|
||||
sa.Column("sector_name", sa.String(128), nullable=False),
|
||||
sa.Column("stock_code", sa.String(12), nullable=False),
|
||||
sa.Column("stock_name", sa.String(128), nullable=False),
|
||||
sa.Column("membership_status", sa.String(32), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("now()"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("source_snapshot_id", "sector_code", "stock_code"),
|
||||
sa.CheckConstraint(
|
||||
"sector_type IN ('concept', 'industry')",
|
||||
name="ck_sector_radar_membership_type",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"membership_status = 'available'",
|
||||
name="ck_sector_radar_membership_status",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_sector_radar_membership_date_sector",
|
||||
"sector_radar_membership",
|
||||
["trade_date", "sector_type", "sector_code"],
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"sector_radar_stock_fact",
|
||||
sa.Column("fact_revision", sa.String(64), nullable=False),
|
||||
sa.Column("trade_date", sa.Date(), nullable=False),
|
||||
sa.Column("ts_code", sa.String(12), nullable=False),
|
||||
sa.Column("source_snapshot_ids", JSONB, nullable=False),
|
||||
sa.Column("status", sa.String(32), nullable=False),
|
||||
sa.Column("turnover_yuan", sa.Numeric(28, 6)),
|
||||
sa.Column("net_amount_yuan", sa.Numeric(28, 6)),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("now()"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("fact_revision", "ts_code"),
|
||||
sa.CheckConstraint(
|
||||
"turnover_yuan IS NULL OR turnover_yuan >= 0",
|
||||
name="ck_sector_radar_stock_turnover",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_sector_radar_stock_fact_date",
|
||||
"sector_radar_stock_fact",
|
||||
["trade_date", "ts_code"],
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"sector_radar_publication",
|
||||
sa.Column("id", sa.String(64), primary_key=True),
|
||||
sa.Column("target_trade_date", sa.Date(), nullable=False),
|
||||
sa.Column("status", sa.String(16), nullable=False),
|
||||
sa.Column("source_version", sa.String(128), nullable=False),
|
||||
sa.Column("universe_version", sa.String(128), nullable=False),
|
||||
sa.Column("metric_versions", JSONB, nullable=False),
|
||||
sa.Column("input_hash", sa.String(64)),
|
||||
sa.Column("coverage", sa.Numeric(8, 6), nullable=False),
|
||||
sa.Column("started_at", sa.DateTime(timezone=True), nullable=False),
|
||||
sa.Column("finished_at", sa.DateTime(timezone=True)),
|
||||
sa.Column("error_summary", sa.String(500)),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("now()"),
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"status IN ('running', 'success', 'partial', 'failed')",
|
||||
name="ck_sector_radar_publication_status",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"coverage >= 0 AND coverage <= 1",
|
||||
name="ck_sector_radar_publication_coverage",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"(status = 'running' AND finished_at IS NULL) OR "
|
||||
"(status <> 'running' AND finished_at IS NOT NULL)",
|
||||
name="ck_sector_radar_publication_finished",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_sector_radar_publication_status_date",
|
||||
"sector_radar_publication",
|
||||
["status", "target_trade_date", "finished_at"],
|
||||
)
|
||||
op.create_index(
|
||||
"uq_sector_radar_publication_running_date",
|
||||
"sector_radar_publication",
|
||||
["target_trade_date"],
|
||||
unique=True,
|
||||
postgresql_where=sa.text("status = 'running'"),
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"sector_radar_ranking",
|
||||
sa.Column(
|
||||
"publication_id",
|
||||
sa.String(64),
|
||||
sa.ForeignKey("sector_radar_publication.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("trade_date", sa.Date(), nullable=False),
|
||||
sa.Column("sector_type", sa.String(16), nullable=False),
|
||||
sa.Column("sector_code", sa.String(16), nullable=False),
|
||||
sa.Column("sector_name", sa.String(128), nullable=False),
|
||||
sa.Column("metric_kind", sa.String(16), nullable=False),
|
||||
sa.Column("metric_version", sa.String(128), nullable=False),
|
||||
sa.Column("implementation_kind", sa.String(16), nullable=False),
|
||||
sa.Column("unit", sa.String(16), nullable=False),
|
||||
sa.Column("metric_value", sa.Numeric(28, 12)),
|
||||
sa.Column("quality", sa.String(32), nullable=False),
|
||||
sa.Column("member_count", sa.Integer(), nullable=False),
|
||||
sa.Column("valid_sample_count", sa.Integer(), nullable=False),
|
||||
sa.Column("membership_coverage", sa.Numeric(8, 6), nullable=False),
|
||||
sa.Column("moneyflow_coverage", sa.Numeric(8, 6), nullable=False),
|
||||
sa.Column("rank_position", sa.Integer()),
|
||||
sa.Column("rank_percentile", sa.Numeric(18, 12)),
|
||||
sa.Column("rank_changes", JSONB, nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("now()"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint(
|
||||
"publication_id",
|
||||
"sector_type",
|
||||
"sector_code",
|
||||
"metric_version",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"sector_type IN ('concept', 'industry')",
|
||||
name="ck_sector_radar_ranking_type",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"implementation_kind = 'independent'",
|
||||
name="ck_sector_radar_ranking_implementation",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_sector_radar_ranking_query",
|
||||
"sector_radar_ranking",
|
||||
["publication_id", "sector_type", "metric_version", "rank_position"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Drop only sector radar tables in dependency-safe order."""
|
||||
|
||||
op.drop_index("ix_sector_radar_ranking_query", table_name="sector_radar_ranking")
|
||||
op.drop_table("sector_radar_ranking")
|
||||
op.drop_index(
|
||||
"uq_sector_radar_publication_running_date",
|
||||
table_name="sector_radar_publication",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_sector_radar_publication_status_date",
|
||||
table_name="sector_radar_publication",
|
||||
)
|
||||
op.drop_table("sector_radar_publication")
|
||||
op.drop_index("ix_sector_radar_stock_fact_date", table_name="sector_radar_stock_fact")
|
||||
op.drop_table("sector_radar_stock_fact")
|
||||
op.drop_index(
|
||||
"ix_sector_radar_membership_date_sector",
|
||||
table_name="sector_radar_membership",
|
||||
)
|
||||
op.drop_table("sector_radar_membership")
|
||||
op.drop_index("ix_sector_radar_source_api_date", table_name="sector_radar_source_snapshot")
|
||||
op.drop_table("sector_radar_source_snapshot")
|
||||
@@ -0,0 +1,155 @@
|
||||
"""Persist publication-owned sector daily aggregates for exact metric replay."""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0005_radar_daily_aggregate"
|
||||
down_revision: str | None = "0004_sector_radar"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Create source recovery links and exact multi-day metric inputs."""
|
||||
|
||||
op.create_table(
|
||||
"sector_radar_publication_source",
|
||||
sa.Column(
|
||||
"publication_id",
|
||||
sa.String(64),
|
||||
sa.ForeignKey("sector_radar_publication.id", ondelete="CASCADE"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.Column("source_group", sa.String(32), nullable=False),
|
||||
sa.Column("source_order", sa.Integer(), nullable=False),
|
||||
sa.Column(
|
||||
"refresh_on_retry",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.false(),
|
||||
),
|
||||
sa.Column(
|
||||
"source_snapshot_id",
|
||||
sa.String(64),
|
||||
sa.ForeignKey("sector_radar_source_snapshot.id", ondelete="RESTRICT"),
|
||||
nullable=False,
|
||||
),
|
||||
sa.PrimaryKeyConstraint("publication_id", "source_group", "source_order"),
|
||||
sa.UniqueConstraint(
|
||||
"publication_id",
|
||||
"source_group",
|
||||
"source_snapshot_id",
|
||||
name="uq_sector_radar_publication_source_snapshot",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"source_group IN ('calendar', 'concept_indices', 'industry_indices', "
|
||||
"'members', 'stock_basics', 'suspensions', 'daily', 'moneyflow_dc')",
|
||||
name="ck_sector_radar_publication_source_group",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"source_order >= 0",
|
||||
name="ck_sector_radar_publication_source_order",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_sector_radar_publication_source_snapshot",
|
||||
"sector_radar_publication_source",
|
||||
["source_snapshot_id"],
|
||||
)
|
||||
|
||||
op.create_unique_constraint(
|
||||
"uq_sector_radar_publication_id_date",
|
||||
"sector_radar_publication",
|
||||
["id", "target_trade_date"],
|
||||
)
|
||||
op.create_foreign_key(
|
||||
"fk_sector_radar_ranking_publication_date",
|
||||
"sector_radar_ranking",
|
||||
"sector_radar_publication",
|
||||
["publication_id", "trade_date"],
|
||||
["id", "target_trade_date"],
|
||||
ondelete="CASCADE",
|
||||
)
|
||||
|
||||
op.create_table(
|
||||
"sector_radar_daily_aggregate",
|
||||
sa.Column("publication_id", sa.String(64), nullable=False),
|
||||
sa.Column("trade_date", sa.Date(), nullable=False),
|
||||
sa.Column("sector_type", sa.String(16), nullable=False),
|
||||
sa.Column("sector_code", sa.String(16), nullable=False),
|
||||
sa.Column("sector_name", sa.String(128), nullable=False),
|
||||
sa.Column("member_count", sa.Integer(), nullable=False),
|
||||
sa.Column("valid_sample_count", sa.Integer(), nullable=False),
|
||||
sa.Column("net_amount_yuan", sa.Numeric(28, 6)),
|
||||
sa.Column("turnover_yuan", sa.Numeric(28, 6)),
|
||||
sa.Column("membership_coverage", sa.Numeric(8, 6), nullable=False),
|
||||
sa.Column("moneyflow_coverage", sa.Numeric(8, 6), nullable=False),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.text("now()"),
|
||||
),
|
||||
sa.PrimaryKeyConstraint("publication_id", "sector_type", "sector_code"),
|
||||
sa.ForeignKeyConstraint(
|
||||
["publication_id", "trade_date"],
|
||||
["sector_radar_publication.id", "sector_radar_publication.target_trade_date"],
|
||||
name="fk_sector_radar_daily_aggregate_publication_date",
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"sector_type IN ('concept', 'industry')",
|
||||
name="ck_sector_radar_daily_aggregate_type",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"member_count >= 0 AND valid_sample_count >= 0 AND valid_sample_count <= member_count",
|
||||
name="ck_sector_radar_daily_aggregate_counts",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"membership_coverage >= 0 AND membership_coverage <= 1 "
|
||||
"AND moneyflow_coverage >= 0 AND moneyflow_coverage <= 1",
|
||||
name="ck_sector_radar_daily_aggregate_coverage",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"turnover_yuan IS NULL OR (turnover_yuan >= 0 AND "
|
||||
"turnover_yuan NOT IN ('NaN'::numeric, 'Infinity'::numeric))",
|
||||
name="ck_sector_radar_daily_aggregate_turnover",
|
||||
),
|
||||
sa.CheckConstraint(
|
||||
"net_amount_yuan IS NULL OR net_amount_yuan NOT IN "
|
||||
"('NaN'::numeric, 'Infinity'::numeric, '-Infinity'::numeric)",
|
||||
name="ck_sector_radar_daily_aggregate_net_amount",
|
||||
),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_sector_radar_daily_aggregate_history",
|
||||
"sector_radar_daily_aggregate",
|
||||
["trade_date", "sector_type", "sector_code"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Drop only the replay aggregate extension."""
|
||||
|
||||
op.drop_index(
|
||||
"ix_sector_radar_daily_aggregate_history",
|
||||
table_name="sector_radar_daily_aggregate",
|
||||
)
|
||||
op.drop_table("sector_radar_daily_aggregate")
|
||||
op.drop_constraint(
|
||||
"fk_sector_radar_ranking_publication_date",
|
||||
"sector_radar_ranking",
|
||||
type_="foreignkey",
|
||||
)
|
||||
op.drop_constraint(
|
||||
"uq_sector_radar_publication_id_date",
|
||||
"sector_radar_publication",
|
||||
type_="unique",
|
||||
)
|
||||
op.drop_index(
|
||||
"ix_sector_radar_publication_source_snapshot",
|
||||
table_name="sector_radar_publication_source",
|
||||
)
|
||||
op.drop_table("sector_radar_publication_source")
|
||||
@@ -0,0 +1,103 @@
|
||||
"""Persist explicit unknown point-in-time sector membership snapshots."""
|
||||
|
||||
from collections.abc import Sequence
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0006_membership_unknown"
|
||||
down_revision: str | None = "0005_radar_daily_aggregate"
|
||||
branch_labels: str | Sequence[str] | None = None
|
||||
depends_on: str | Sequence[str] | None = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Allow one null-stock marker for an explicitly empty sector partition."""
|
||||
|
||||
op.add_column(
|
||||
"sector_radar_membership",
|
||||
sa.Column("membership_key", sa.String(32), nullable=True),
|
||||
)
|
||||
op.execute("UPDATE sector_radar_membership SET membership_key = stock_code")
|
||||
op.drop_constraint(
|
||||
"sector_radar_membership_pkey",
|
||||
"sector_radar_membership",
|
||||
type_="primary",
|
||||
)
|
||||
op.drop_constraint(
|
||||
"ck_sector_radar_membership_status",
|
||||
"sector_radar_membership",
|
||||
type_="check",
|
||||
)
|
||||
op.alter_column(
|
||||
"sector_radar_membership",
|
||||
"membership_key",
|
||||
existing_type=sa.String(32),
|
||||
nullable=False,
|
||||
)
|
||||
op.alter_column(
|
||||
"sector_radar_membership",
|
||||
"stock_code",
|
||||
existing_type=sa.String(12),
|
||||
nullable=True,
|
||||
)
|
||||
op.alter_column(
|
||||
"sector_radar_membership",
|
||||
"stock_name",
|
||||
existing_type=sa.String(128),
|
||||
nullable=True,
|
||||
)
|
||||
op.create_primary_key(
|
||||
"sector_radar_membership_pkey",
|
||||
"sector_radar_membership",
|
||||
["source_snapshot_id", "sector_code", "membership_key"],
|
||||
)
|
||||
op.create_check_constraint(
|
||||
"ck_sector_radar_membership_status",
|
||||
"sector_radar_membership",
|
||||
"(membership_status = 'available' "
|
||||
"AND stock_code IS NOT NULL AND stock_name IS NOT NULL "
|
||||
"AND membership_key = stock_code) OR "
|
||||
"(membership_status = 'membership_unknown' "
|
||||
"AND stock_code IS NULL AND stock_name IS NULL "
|
||||
"AND membership_key = '__membership_unknown__')",
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Discard unknown markers and restore the available-member-only schema."""
|
||||
|
||||
op.execute("DELETE FROM sector_radar_membership WHERE membership_status = 'membership_unknown'")
|
||||
op.drop_constraint(
|
||||
"ck_sector_radar_membership_status",
|
||||
"sector_radar_membership",
|
||||
type_="check",
|
||||
)
|
||||
op.drop_constraint(
|
||||
"sector_radar_membership_pkey",
|
||||
"sector_radar_membership",
|
||||
type_="primary",
|
||||
)
|
||||
op.alter_column(
|
||||
"sector_radar_membership",
|
||||
"stock_code",
|
||||
existing_type=sa.String(12),
|
||||
nullable=False,
|
||||
)
|
||||
op.alter_column(
|
||||
"sector_radar_membership",
|
||||
"stock_name",
|
||||
existing_type=sa.String(128),
|
||||
nullable=False,
|
||||
)
|
||||
op.drop_column("sector_radar_membership", "membership_key")
|
||||
op.create_primary_key(
|
||||
"sector_radar_membership_pkey",
|
||||
"sector_radar_membership",
|
||||
["source_snapshot_id", "sector_code", "stock_code"],
|
||||
)
|
||||
op.create_check_constraint(
|
||||
"ck_sector_radar_membership_status",
|
||||
"sector_radar_membership",
|
||||
"membership_status = 'available'",
|
||||
)
|
||||
Reference in New Issue
Block a user