feat(selection): 集成 B1 FastDTW 图形评分
This commit is contained in:
@@ -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)
|
||||
Reference in New Issue
Block a user