"""initial schema Revision ID: a1b2c3d4e5f6 Revises: Create Date: 2026-02-22 15:15:15.661206 """ from typing import Sequence, Union import sqlalchemy as sa from alembic import op from sqlalchemy.dialects import postgresql # revision identifiers, used by Alembic. revision: str = "a1b2c3d4e5f6" down_revision: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: """Create all tables for the trading bot.""" # --- Core trading tables --- op.create_table( "strategies", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("name", sa.String(255), unique=True, nullable=False), sa.Column("description", sa.Text, nullable=True), sa.Column("current_weight", sa.Float, nullable=False, server_default="0.333"), sa.Column("active", sa.Boolean, nullable=False, server_default=sa.text("true")), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) op.create_table( "signals", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("ticker", sa.String(20), nullable=False, index=True), sa.Column( "direction", sa.Enum("LONG", "SHORT", "NEUTRAL", name="signaldirection"), nullable=False, ), sa.Column("strength", sa.Float, nullable=False), sa.Column("strategy_sources", postgresql.JSON, nullable=True), sa.Column("sentiment_score", sa.Float, nullable=True), sa.Column("acted_on", sa.Boolean, nullable=False, server_default=sa.text("false")), sa.Column( "strategy_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("strategies.id"), nullable=True, ), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) op.create_table( "trades", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("ticker", sa.String(20), nullable=False, index=True), sa.Column( "side", sa.Enum("BUY", "SELL", name="tradeside"), nullable=False, ), sa.Column("qty", sa.Float, nullable=False), sa.Column("price", sa.Float, nullable=False), sa.Column("timestamp", sa.String, nullable=True), sa.Column( "strategy_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("strategies.id"), nullable=True, ), sa.Column( "signal_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("signals.id"), nullable=True, ), sa.Column( "status", sa.Enum("PENDING", "FILLED", "CANCELLED", "REJECTED", name="tradestatus"), nullable=False, server_default="PENDING", ), sa.Column("pnl", sa.Float, nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) op.create_table( "positions", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("ticker", sa.String(20), unique=True, nullable=False), sa.Column("qty", sa.Float, nullable=False), sa.Column("avg_entry", sa.Float, nullable=False), sa.Column("unrealized_pnl", sa.Float, nullable=True), sa.Column("stop_loss", sa.Float, nullable=True), sa.Column("take_profit", sa.Float, nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) op.create_table( "strategy_weight_history", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column( "strategy_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("strategies.id"), nullable=False, ), sa.Column("old_weight", sa.Float, nullable=False), sa.Column("new_weight", sa.Float, nullable=False), sa.Column("reason", sa.String(500), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) # --- News & sentiment --- op.create_table( "articles", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("source", sa.String(100), nullable=False), sa.Column("url", sa.Text, nullable=False), sa.Column("title", sa.Text, nullable=False), sa.Column("published_at", sa.DateTime(timezone=True), nullable=True), sa.Column("fetched_at", sa.DateTime(timezone=True), nullable=False), sa.Column("content_hash", sa.String(64), unique=True, nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) op.create_table( "article_sentiments", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column( "article_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("articles.id"), nullable=False, ), sa.Column("ticker", sa.String(20), nullable=False, index=True), sa.Column("score", sa.Float, nullable=False), sa.Column("confidence", sa.Float, nullable=False), sa.Column("model_used", sa.String(50), nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) # --- Learning --- op.create_table( "trade_outcomes", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column( "trade_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("trades.id"), unique=True, nullable=False, ), sa.Column("hold_duration", sa.Interval, nullable=True), sa.Column("realized_pnl", sa.Float, nullable=False), sa.Column("roi_pct", sa.Float, nullable=False), sa.Column("was_profitable", sa.Boolean, nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) op.create_table( "learning_adjustments", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column( "strategy_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("strategies.id"), nullable=False, ), sa.Column("old_weight", sa.Float, nullable=False), sa.Column("new_weight", sa.Float, nullable=False), sa.Column("reason", sa.Text, nullable=True), sa.Column("reward_signal", sa.Float, nullable=False), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) # --- Auth --- op.create_table( "users", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column("username", sa.String(100), unique=True, nullable=False), sa.Column("display_name", sa.String(255), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) op.create_table( "user_credentials", sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True), sa.Column( "user_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("users.id"), nullable=False, ), sa.Column("credential_id", sa.String(512), unique=True, nullable=False), sa.Column("public_key", sa.LargeBinary, nullable=False), sa.Column("sign_count", sa.Integer, nullable=False, server_default="0"), sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()), sa.Column("updated_at", sa.DateTime(timezone=True), server_default=sa.func.now()), ) # --- Timeseries (TimescaleDB hypertables) --- op.create_table( "market_data", sa.Column("timestamp", sa.DateTime(timezone=True), primary_key=True), sa.Column("ticker", sa.String(20), primary_key=True), sa.Column("open", sa.Float, nullable=False), sa.Column("high", sa.Float, nullable=False), sa.Column("low", sa.Float, nullable=False), sa.Column("close", sa.Float, nullable=False), sa.Column("volume", sa.Float, nullable=False), ) op.create_table( "portfolio_snapshots", sa.Column("timestamp", sa.DateTime(timezone=True), primary_key=True), sa.Column("total_value", sa.Float, nullable=False), sa.Column("cash", sa.Float, nullable=False), sa.Column("positions_value", sa.Float, nullable=False), sa.Column("daily_pnl", sa.Float, nullable=False), ) op.create_table( "strategy_metrics", sa.Column("timestamp", sa.DateTime(timezone=True), primary_key=True), sa.Column( "strategy_id", postgresql.UUID(as_uuid=True), sa.ForeignKey("strategies.id"), primary_key=True, ), sa.Column("win_rate", sa.Float, nullable=False), sa.Column("total_pnl", sa.Float, nullable=False), sa.Column("trade_count", sa.Integer, nullable=False), sa.Column("sharpe_ratio", sa.Float, nullable=True), ) # Convert timeseries tables to TimescaleDB hypertables. # These calls are idempotent-safe when the extension is loaded. op.execute("SELECT create_hypertable('market_data', 'timestamp', if_not_exists => TRUE)") op.execute("SELECT create_hypertable('portfolio_snapshots', 'timestamp', if_not_exists => TRUE)") op.execute("SELECT create_hypertable('strategy_metrics', 'timestamp', if_not_exists => TRUE)") def downgrade() -> None: """Drop all tables in reverse dependency order.""" op.drop_table("strategy_metrics") op.drop_table("portfolio_snapshots") op.drop_table("market_data") op.drop_table("user_credentials") op.drop_table("users") op.drop_table("learning_adjustments") op.drop_table("trade_outcomes") op.drop_table("article_sentiments") op.drop_table("articles") op.drop_table("strategy_weight_history") op.drop_table("positions") op.drop_table("trades") op.drop_table("signals") op.drop_table("strategies") # Drop enums sa.Enum(name="signaldirection").drop(op.get_bind(), checkfirst=True) sa.Enum(name="tradeside").drop(op.get_bind(), checkfirst=True) sa.Enum(name="tradestatus").drop(op.get_bind(), checkfirst=True)