trading/alembic/versions/a1b2c3d4e5f6_initial_schema.py

291 lines
12 KiB
Python
Raw Normal View History

"""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.Numeric(16, 6), nullable=False),
sa.Column("price", sa.Numeric(16, 6), 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.Numeric(16, 6), nullable=True),
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), index=True),
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.Numeric(16, 6), nullable=False),
sa.Column("avg_entry", sa.Numeric(16, 6), nullable=False),
sa.Column("unrealized_pnl", sa.Numeric(16, 6), nullable=True),
sa.Column("stop_loss", sa.Numeric(16, 6), nullable=True),
sa.Column("take_profit", sa.Numeric(16, 6), 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 if the extension is available.
op.execute("""
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') THEN
PERFORM create_hypertable('market_data', 'timestamp', if_not_exists => TRUE);
PERFORM create_hypertable('portfolio_snapshots', 'timestamp', if_not_exists => TRUE);
PERFORM create_hypertable('strategy_metrics', 'timestamp', if_not_exists => TRUE);
END IF;
END $$;
""")
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)