"""Tables added in v2 for paper-trading + backtest persistence. - KevinSignalBridgeState audit trail: one row per processed mention - KevinBacktestRun one row per backtest invocation - KevinBacktestTrade per-trade detail within a run """ from __future__ import annotations import enum import uuid from datetime import datetime from decimal import Decimal from typing import Any from sqlalchemy import ( BigInteger, DateTime, Enum as SAEnum, ForeignKey, Index, Integer, Numeric, String, Text, func, ) from sqlalchemy.dialects.postgresql import JSONB, UUID from sqlalchemy.orm import Mapped, mapped_column, relationship from shared.models.base import Base, TimestampMixin class BridgeStatus(str, enum.Enum): EMITTED = "emitted" SKIPPED_NON_TRADABLE = "skipped_non_tradable" SKIPPED_BLOCKLIST = "skipped_blocklist" SKIPPED_CAPS = "skipped_caps" DEFERRED = "deferred" BROKER_REJECTED = "broker_rejected" DRY_RUN = "dry_run" # kill-switch off class KevinBacktestRunStatus(str, enum.Enum): RUNNING = "running" COMPLETED = "completed" FAILED = "failed" class TriggerSource(str, enum.Enum): MANUAL = "manual" SCHEDULED = "scheduled" class KevinSignalBridgeState(TimestampMixin, Base): __tablename__ = "kevin_signal_bridge_state" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) mention_id: Mapped[int] = mapped_column( BigInteger, ForeignKey("kevin_stock_mentions.id"), nullable=False, unique=True, ) bridge_status: Mapped[BridgeStatus] = mapped_column( SAEnum( BridgeStatus, name="kevin_bridge_status", values_callable=lambda x: [e.value for e in x], ), nullable=False, ) signal_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("signals.id"), nullable=True ) trade_id: Mapped[uuid.UUID | None] = mapped_column( UUID(as_uuid=True), ForeignKey("trades.id"), nullable=True ) effective_conviction: Mapped[Decimal | None] = mapped_column( Numeric(4, 3), nullable=True ) decided_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) notes: Mapped[str | None] = mapped_column(Text, nullable=True) __table_args__ = ( Index("ix_bridge_state_status_decided", "bridge_status", "decided_at"), ) class KevinBacktestRun(TimestampMixin, Base): __tablename__ = "kevin_backtest_runs" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) run_uuid: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), unique=True, nullable=False, default=uuid.uuid4 ) started_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, server_default=func.now() ) finished_at: Mapped[datetime | None] = mapped_column( DateTime(timezone=True), nullable=True ) status: Mapped[KevinBacktestRunStatus] = mapped_column( SAEnum( KevinBacktestRunStatus, name="kevin_backtest_run_status", values_callable=lambda x: [e.value for e in x], ), nullable=False, ) trigger_source: Mapped[TriggerSource] = mapped_column( SAEnum( TriggerSource, name="kevin_backtest_trigger_source", values_callable=lambda x: [e.value for e in x], ), nullable=False, default=TriggerSource.MANUAL, ) params_json: Mapped[dict[str, Any]] = mapped_column(JSONB, nullable=False) metrics_json: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True) equity_curve_json: Mapped[list[Any] | None] = mapped_column(JSONB, nullable=True) benchmark_curve_json: Mapped[list[Any] | None] = mapped_column(JSONB, nullable=True) error_message: Mapped[str | None] = mapped_column(Text, nullable=True) trades: Mapped[list["KevinBacktestTrade"]] = relationship( back_populates="run", cascade="all, delete-orphan" ) __table_args__ = ( Index("ix_backtest_runs_started", "started_at"), Index("ix_backtest_runs_status_started", "status", "started_at"), ) class KevinBacktestTrade(TimestampMixin, Base): __tablename__ = "kevin_backtest_trades" id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), primary_key=True, default=uuid.uuid4 ) run_id: Mapped[uuid.UUID] = mapped_column( UUID(as_uuid=True), ForeignKey("kevin_backtest_runs.id", ondelete="CASCADE"), nullable=False, ) symbol: Mapped[str] = mapped_column(String(16), nullable=False) source_mention_id: Mapped[int | None] = mapped_column( BigInteger, ForeignKey("kevin_stock_mentions.id"), nullable=True ) entry_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) entry_price: Mapped[Decimal] = mapped_column(Numeric(12, 4), nullable=False) exit_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) exit_price: Mapped[Decimal | None] = mapped_column(Numeric(12, 4)) qty: Mapped[Decimal] = mapped_column(Numeric(14, 4), nullable=False) pnl_usd: Mapped[Decimal | None] = mapped_column(Numeric(14, 4)) pnl_pct: Mapped[Decimal | None] = mapped_column(Numeric(8, 4)) holding_days_actual: Mapped[int | None] = mapped_column(Integer) run: Mapped["KevinBacktestRun"] = relationship(back_populates="trades") __table_args__ = ( Index("ix_backtest_trades_run_symbol", "run_id", "symbol"), Index("ix_backtest_trades_run_entry", "run_id", "entry_at"), )