"""kevin v2 trading tables + seed kevin strategy row. Revision ID: d4e5f6a7b8c9 Revises: c3d4e5f6a7b8 Create Date: 2026-05-23 """ import uuid from typing import Sequence, Union import sqlalchemy as sa from alembic import op from sqlalchemy.dialects.postgresql import ENUM, JSONB, UUID # revision identifiers, used by Alembic. revision: str = "d4e5f6a7b8c9" down_revision: Union[str, Sequence[str], None] = "c3d4e5f6a7b8" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None KEVIN_STRATEGY_UUID = uuid.UUID("4b8d1c2a-5e7f-4d3b-9a1c-6f8b2e4d7a90") def upgrade() -> None: # 1) Seed the strategies.kevin row that Trade.strategy_id will reference op.execute( f""" INSERT INTO strategies (id, name, description, current_weight, active, created_at, updated_at) VALUES ( '{KEVIN_STRATEGY_UUID}', 'kevin', 'Meet Kevin signal-driven strategy (paper trading)', 1.0, true, now(), now() ) ON CONFLICT (id) DO NOTHING """ ) # 2) Enums bridge_status_enum = sa.Enum( "emitted", "skipped_non_tradable", "skipped_blocklist", "skipped_caps", "deferred", "broker_rejected", "dry_run", name="kevin_bridge_status", ) bridge_status_enum.create(op.get_bind(), checkfirst=True) run_status_enum = sa.Enum( "running", "completed", "failed", name="kevin_backtest_run_status", ) run_status_enum.create(op.get_bind(), checkfirst=True) trigger_source_enum = sa.Enum( "manual", "scheduled", name="kevin_backtest_trigger_source", ) trigger_source_enum.create(op.get_bind(), checkfirst=True) # 3) Bridge audit table op.create_table( "kevin_signal_bridge_state", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column( "mention_id", sa.BigInteger, sa.ForeignKey("kevin_stock_mentions.id"), nullable=False, unique=True, ), sa.Column( "bridge_status", ENUM(name="kevin_bridge_status", create_type=False), nullable=False, ), sa.Column( "signal_id", UUID(as_uuid=True), sa.ForeignKey("signals.id"), nullable=True, ), sa.Column( "trade_id", UUID(as_uuid=True), sa.ForeignKey("trades.id"), nullable=True, ), sa.Column("effective_conviction", sa.Numeric(4, 3), nullable=True), sa.Column( "decided_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), sa.Column("notes", sa.Text, nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), sa.Column( "updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), ) op.create_index( "ix_bridge_state_status_decided", "kevin_signal_bridge_state", ["bridge_status", "decided_at"], ) # 4) Backtest runs op.create_table( "kevin_backtest_runs", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column("run_uuid", UUID(as_uuid=True), unique=True, nullable=False), sa.Column( "started_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), sa.Column("finished_at", sa.DateTime(timezone=True), nullable=True), sa.Column( "status", ENUM(name="kevin_backtest_run_status", create_type=False), nullable=False, ), sa.Column( "trigger_source", ENUM(name="kevin_backtest_trigger_source", create_type=False), nullable=False, server_default="manual", ), sa.Column("params_json", JSONB, nullable=False), sa.Column("metrics_json", JSONB, nullable=True), sa.Column("equity_curve_json", JSONB, nullable=True), sa.Column("benchmark_curve_json", JSONB, nullable=True), sa.Column("error_message", sa.Text, nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), sa.Column( "updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), ) op.create_index( "ix_backtest_runs_started", "kevin_backtest_runs", ["started_at"], ) op.create_index( "ix_backtest_runs_status_started", "kevin_backtest_runs", ["status", "started_at"], ) # 5) Backtest trades op.create_table( "kevin_backtest_trades", sa.Column("id", UUID(as_uuid=True), primary_key=True), sa.Column( "run_id", UUID(as_uuid=True), sa.ForeignKey("kevin_backtest_runs.id", ondelete="CASCADE"), nullable=False, ), sa.Column("symbol", sa.String(16), nullable=False), sa.Column( "source_mention_id", sa.BigInteger, sa.ForeignKey("kevin_stock_mentions.id"), nullable=True, ), sa.Column("entry_at", sa.DateTime(timezone=True), nullable=False), sa.Column("entry_price", sa.Numeric(12, 4), nullable=False), sa.Column("exit_at", sa.DateTime(timezone=True), nullable=True), sa.Column("exit_price", sa.Numeric(12, 4), nullable=True), sa.Column("qty", sa.Numeric(14, 4), nullable=False), sa.Column("pnl_usd", sa.Numeric(14, 4), nullable=True), sa.Column("pnl_pct", sa.Numeric(8, 4), nullable=True), sa.Column("holding_days_actual", sa.Integer, nullable=True), sa.Column( "created_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), sa.Column( "updated_at", sa.DateTime(timezone=True), server_default=sa.text("now()"), nullable=False, ), ) op.create_index( "ix_backtest_trades_run_symbol", "kevin_backtest_trades", ["run_id", "symbol"], ) op.create_index( "ix_backtest_trades_run_entry", "kevin_backtest_trades", ["run_id", "entry_at"], ) def downgrade() -> None: op.drop_table("kevin_backtest_trades") op.drop_table("kevin_backtest_runs") op.drop_table("kevin_signal_bridge_state") bind = op.get_bind() ENUM(name="kevin_backtest_trigger_source").drop(bind, checkfirst=True) ENUM(name="kevin_backtest_run_status").drop(bind, checkfirst=True) ENUM(name="kevin_bridge_status").drop(bind, checkfirst=True) op.execute(f"DELETE FROM strategies WHERE id = '{KEVIN_STRATEGY_UUID}'")