90 lines
3.9 KiB
Python
90 lines
3.9 KiB
Python
"""initial portfolio schema
|
|
|
|
Revision ID: 20260403_0001
|
|
Revises:
|
|
Create Date: 2026-04-03 00:00:00
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
|
|
revision = "20260403_0001"
|
|
down_revision = None
|
|
branch_labels = None
|
|
depends_on = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"strategy_sets",
|
|
sa.Column("id", sa.Text(), primary_key=True),
|
|
sa.Column("strategy_name", sa.Text(), nullable=False),
|
|
sa.Column("strategy_config", sa.Text(), nullable=False, server_default=""),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("NOW()")),
|
|
sa.UniqueConstraint("strategy_name", "strategy_config", name="uq_strategy_sets_name_config"),
|
|
)
|
|
|
|
op.create_table(
|
|
"portfolios",
|
|
sa.Column("portfolio_id", sa.Text(), primary_key=True),
|
|
sa.Column("broker", sa.Text(), nullable=False, server_default="kis"),
|
|
sa.Column("status", sa.Text(), nullable=False, server_default="enabled"),
|
|
sa.Column("symbol", sa.Text(), nullable=False),
|
|
sa.Column("strategy_name", sa.Text(), nullable=False, server_default=""),
|
|
sa.Column("strategy_params", postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default=sa.text("'{}'::jsonb")),
|
|
sa.Column("cash_balance", sa.Float(), nullable=False),
|
|
sa.Column("asset_quantity", sa.Float(), nullable=False),
|
|
sa.Column("asset_price", sa.Float(), nullable=False, server_default="0"),
|
|
sa.Column("profit_buffer", sa.Float(), nullable=False, server_default="0"),
|
|
sa.Column("current_strategy_set_id", sa.Text(), nullable=True),
|
|
sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("NOW()")),
|
|
sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("NOW()")),
|
|
sa.Column("last_trade_at", sa.DateTime(timezone=True), nullable=True),
|
|
)
|
|
|
|
op.create_table(
|
|
"portfolio_ledger",
|
|
sa.Column("portfolio_id", sa.Text(), nullable=False),
|
|
sa.Column("recorded_at", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("action", sa.Text(), nullable=False),
|
|
sa.Column("price", sa.Float(), nullable=False),
|
|
sa.Column("quantity", 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("cash_balance_after", sa.Float(), nullable=False),
|
|
sa.Column("asset_quantity_after", sa.Float(), nullable=False),
|
|
sa.Column("total_value_after", sa.Float(), nullable=False),
|
|
sa.Column("profit_buffer_after", sa.Float(), nullable=False, server_default="0"),
|
|
sa.Column("strategy_set_id", sa.Text(), nullable=False, server_default="legacy"),
|
|
sa.Column("note", sa.Text(), nullable=False, server_default=""),
|
|
)
|
|
|
|
op.create_index(
|
|
"idx_portfolio_ledger_portfolio_time",
|
|
"portfolio_ledger",
|
|
["portfolio_id", sa.text("recorded_at DESC")],
|
|
unique=False,
|
|
)
|
|
op.create_index(
|
|
"idx_portfolios_broker_status_updated",
|
|
"portfolios",
|
|
["broker", "status", sa.text("updated_at DESC")],
|
|
unique=False,
|
|
)
|
|
|
|
bind = op.get_bind()
|
|
has_timescaledb = bind.execute(sa.text("SELECT EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb')")).scalar()
|
|
if has_timescaledb:
|
|
bind.execute(sa.text("SELECT create_hypertable('portfolio_ledger', 'recorded_at', if_not_exists => TRUE)"))
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_index("idx_portfolios_broker_status_updated", table_name="portfolios")
|
|
op.drop_index("idx_portfolio_ledger_portfolio_time", table_name="portfolio_ledger")
|
|
op.drop_table("portfolio_ledger")
|
|
op.drop_table("portfolios")
|
|
op.drop_table("strategy_sets") |