From c4e92b580e443a5caae1ddb7ab212fde54a3402b Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Sun, 24 May 2026 00:50:00 +0000 Subject: [PATCH] feat(kevin): alembic migration for v2 trading tables 3 new tables + seeds the 'kevin' row in strategies with a pinned UUID constant so Trade.strategy_id can be joined back to the strategy across live + backtest paths. --- .../d4e5f6a7b8c9_kevin_v2_trading_tables.py | 235 ++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 alembic/versions/d4e5f6a7b8c9_kevin_v2_trading_tables.py diff --git a/alembic/versions/d4e5f6a7b8c9_kevin_v2_trading_tables.py b/alembic/versions/d4e5f6a7b8c9_kevin_v2_trading_tables.py new file mode 100644 index 0000000..a402035 --- /dev/null +++ b/alembic/versions/d4e5f6a7b8c9_kevin_v2_trading_tables.py @@ -0,0 +1,235 @@ +"""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}'")