feat(selection): 集成 B1 FastDTW 图形评分

This commit is contained in:
yuxuanhui
2026-08-31 16:14:16 +08:00
parent 86762c0d9a
commit 6ce291e242
51 changed files with 2917 additions and 41 deletions
@@ -0,0 +1,142 @@
"""Add stock-level versioned B1 pattern scoring results."""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
revision: str = "0004_selection_pattern_scoring"
down_revision: str | None = "0003_market_integrity_checks"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
_PATTERN_BREAKDOWN_CHECK = """
match_breakdown IS NULL OR (
jsonb_typeof(match_breakdown) = 'object'
AND CASE WHEN jsonb_typeof(match_breakdown -> 'trend_structure') = 'number'
THEN (match_breakdown ->> 'trend_structure')::numeric BETWEEN 0 AND 100
ELSE false END
AND CASE WHEN jsonb_typeof(match_breakdown -> 'kdj_state') = 'number'
THEN (match_breakdown ->> 'kdj_state')::numeric BETWEEN 0 AND 100
ELSE false END
AND CASE WHEN jsonb_typeof(match_breakdown -> 'volume_pattern') = 'number'
THEN (match_breakdown ->> 'volume_pattern')::numeric BETWEEN 0 AND 100
ELSE false END
AND CASE WHEN jsonb_typeof(match_breakdown -> 'price_shape') = 'number'
THEN (match_breakdown ->> 'price_shape')::numeric BETWEEN 0 AND 100
ELSE false END
)
"""
def upgrade() -> None:
"""Add nullable scoring data while keeping existing runs readable."""
op.add_column(
"selection_run_item",
sa.Column(
"score_status",
sa.String(32),
nullable=False,
server_default="not_executed",
),
)
op.add_column("selection_run_item", sa.Column("score_value", sa.Numeric(5, 2)))
op.add_column("selection_run_item", sa.Column("score_threshold", sa.Numeric(5, 2)))
op.add_column("selection_run_item", sa.Column("score_version", sa.String(64)))
op.add_column("selection_run_item", sa.Column("match_case_id", sa.String(32)))
op.add_column("selection_run_item", sa.Column("match_case_name", sa.String(128)))
op.add_column("selection_run_item", sa.Column("match_case_breakout_date", sa.Date()))
op.add_column(
"selection_run_item",
sa.Column("match_breakdown", postgresql.JSONB(astext_type=sa.Text())),
)
op.add_column("selection_run_item", sa.Column("score_reason", sa.Text()))
op.create_check_constraint(
"ck_selection_run_item_score_status",
"selection_run_item",
"score_status IN ('not_executed', 'matched', 'below_threshold', 'failed')",
)
op.create_check_constraint(
"ck_selection_run_item_score_value_range",
"selection_run_item",
"score_value IS NULL OR score_value BETWEEN 0 AND 100",
)
op.create_check_constraint(
"ck_selection_run_item_score_threshold_range",
"selection_run_item",
"score_threshold IS NULL OR score_threshold BETWEEN 0 AND 100",
)
op.create_check_constraint(
"ck_selection_run_item_breakdown_range",
"selection_run_item",
_PATTERN_BREAKDOWN_CHECK,
)
op.create_check_constraint(
"ck_selection_run_item_score_shape",
"selection_run_item",
"(score_status = 'not_executed' AND score_value IS NULL AND score_threshold IS NULL "
"AND score_version IS NULL AND match_case_id IS NULL AND match_case_name IS NULL "
"AND match_case_breakout_date IS NULL AND match_breakdown IS NULL "
"AND score_reason IS NULL) "
"OR (score_status = 'failed' AND score_value IS NULL AND score_threshold IS NULL "
"AND score_version IS NULL AND match_case_id IS NULL AND match_case_name IS NULL "
"AND match_case_breakout_date IS NULL AND match_breakdown IS NULL "
"AND score_reason IS NOT NULL) "
"OR (score_status IN ('matched', 'below_threshold') AND score_value IS NOT NULL "
"AND score_threshold IS NOT NULL AND score_version IS NOT NULL "
"AND match_case_id IS NOT NULL AND match_case_name IS NOT NULL "
"AND match_case_breakout_date IS NOT NULL AND match_breakdown IS NOT NULL "
"AND score_reason IS NULL "
"AND ((score_status = 'matched' AND score_value >= score_threshold) "
"OR (score_status = 'below_threshold' AND score_value < score_threshold)))",
)
op.create_index(
"ix_selection_run_item_score",
"selection_run_item",
["run_id", sa.text("score_value DESC"), "ts_code"],
)
def downgrade() -> None:
"""Remove only the additive stock-level scoring contract."""
op.drop_index("ix_selection_run_item_score", table_name="selection_run_item")
op.drop_constraint(
"ck_selection_run_item_score_shape",
"selection_run_item",
type_="check",
)
op.drop_constraint(
"ck_selection_run_item_score_threshold_range",
"selection_run_item",
type_="check",
)
op.drop_constraint(
"ck_selection_run_item_breakdown_range",
"selection_run_item",
type_="check",
)
op.drop_constraint(
"ck_selection_run_item_score_value_range",
"selection_run_item",
type_="check",
)
op.drop_constraint(
"ck_selection_run_item_score_status",
"selection_run_item",
type_="check",
)
for column in (
"score_reason",
"match_breakdown",
"match_case_breakout_date",
"match_case_name",
"match_case_id",
"score_version",
"score_threshold",
"score_value",
"score_status",
):
op.drop_column("selection_run_item", column)