feat(kevin): alembic migration for v2 trading tables
Some checks failed
ci/woodpecker/push/woodpecker Pipeline was canceled

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.
This commit is contained in:
Viktor Barzin 2026-05-24 00:50:00 +00:00
parent 4d40536da7
commit c4e92b580e

View file

@ -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}'")