"""consolidate ledger trades into portfolio ledger Revision ID: 20260605_0025 Revises: 20260601_0024 Create Date: 2026-06-05 00:25:00.000000 """ from collections.abc import Sequence from alembic import op import sqlalchemy as sa revision: str = "20260605_0025" down_revision: str | None = "20260601_0024" branch_labels: str | Sequence[str] | None = None depends_on: str | Sequence[str] | None = None def upgrade() -> None: # PostgreSQL identity columns are implicitly NOT NULL. ``nullable=True`` # renders a conflicting trailing ``NULL`` clause on a real PostgreSQL DB. op.add_column("portfolio_ledger", sa.Column("ledger_entry_id", sa.BigInteger(), sa.Identity(always=False), nullable=False)) op.add_column("portfolio_ledger", sa.Column("order_id", sa.Text(), nullable=True)) op.add_column("portfolio_ledger", sa.Column("execution_id", sa.Text(), nullable=True)) op.add_column("portfolio_ledger", sa.Column("side", sa.Text(), nullable=True)) op.add_column("portfolio_ledger", sa.Column("executed_at", sa.DateTime(timezone=True), nullable=True)) op.execute( """ DO $$ BEGIN IF EXISTS ( SELECT 1 FROM information_schema.columns WHERE table_name = 'portfolio_ledger' AND column_name = 'action' ) THEN UPDATE portfolio_ledger SET side = lower(action) WHERE side IS NULL; END IF; END $$; """ ) op.execute("UPDATE portfolio_ledger SET side = 'manual' WHERE side IS NULL") op.execute("UPDATE portfolio_ledger SET executed_at = recorded_at WHERE executed_at IS NULL") op.alter_column("portfolio_ledger", "side", nullable=False) op.alter_column("portfolio_ledger", "executed_at", nullable=False) for column in ( "price", "quantity", "fee_amount", "tax_amount", "cash_balance_after", "asset_quantity_after", "total_value_after", "profit_buffer_after", ): op.alter_column( "portfolio_ledger", column, existing_type=sa.Float(), type_=sa.Numeric(20, 8), postgresql_using=f"{column}::numeric(20,8)", ) op.drop_column("portfolio_ledger", "action") op.create_index("idx_portfolio_ledger_portfolio_executed", "portfolio_ledger", ["portfolio_id", sa.text("executed_at DESC")]) op.drop_table("ledger_trades") def downgrade() -> None: op.create_table( "ledger_trades", sa.Column("ledger_trade_id", sa.BigInteger(), primary_key=True), sa.Column("portfolio_id", sa.Text(), nullable=False), sa.Column("order_id", sa.Text(), nullable=False), sa.Column("execution_id", sa.Text(), nullable=False), sa.Column("side", sa.Text(), nullable=False), sa.Column("quantity", sa.Float(), nullable=False), sa.Column("price", sa.Float(), nullable=False), sa.Column("fee_amount", sa.Float(), nullable=False, server_default="0"), sa.Column("tax_amount", sa.Float(), nullable=False, server_default="0"), sa.Column("executed_at", sa.DateTime(timezone=True), nullable=False), sa.Column("recorded_at", sa.DateTime(timezone=True), nullable=False), sa.UniqueConstraint("portfolio_id", "order_id", "execution_id", name="uq_ledger_trades_idempotency"), ) op.add_column("portfolio_ledger", sa.Column("action", sa.Text(), nullable=True)) op.execute("UPDATE portfolio_ledger SET action = upper(side) WHERE action IS NULL") op.alter_column("portfolio_ledger", "action", nullable=False) op.drop_index("idx_portfolio_ledger_portfolio_executed", table_name="portfolio_ledger") op.drop_column("portfolio_ledger", "executed_at") op.drop_column("portfolio_ledger", "side") op.drop_column("portfolio_ledger", "execution_id") op.drop_column("portfolio_ledger", "order_id") op.drop_column("portfolio_ledger", "ledger_entry_id")