SIP_GoldBees_Backend/migrations/versions/52abc790351d_initial_schema.py
2026-02-01 13:06:44 +00:00

675 lines
33 KiB
Python

"""initial_schema
Revision ID: 52abc790351d
Revises:
Create Date: 2026-01-18 08:34:50.268181
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import postgresql
# revision identifiers, used by Alembic.
revision: str = '52abc790351d'
down_revision: Union[str, None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('admin_audit_log',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('ts', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('actor_user_hash', sa.Text(), nullable=False),
sa.Column('target_user_hash', sa.Text(), nullable=False),
sa.Column('target_username_hash', sa.Text(), nullable=True),
sa.Column('action', sa.Text(), nullable=False),
sa.Column('meta', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('admin_role_audit',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('actor_user_id', sa.String(), nullable=False),
sa.Column('target_user_id', sa.String(), nullable=False),
sa.Column('old_role', sa.String(), nullable=False),
sa.Column('new_role', sa.String(), nullable=False),
sa.Column('changed_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('app_user',
sa.Column('id', sa.String(), nullable=False),
sa.Column('username', sa.String(), nullable=False),
sa.Column('password_hash', sa.String(), nullable=False),
sa.Column('is_admin', sa.Boolean(), server_default=sa.text('false'), nullable=False),
sa.Column('is_super_admin', sa.Boolean(), server_default=sa.text('false'), nullable=False),
sa.Column('role', sa.String(), server_default=sa.text("'USER'"), nullable=False),
sa.CheckConstraint("role IN ('USER','ADMIN','SUPER_ADMIN')", name='chk_app_user_role'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username')
)
op.create_index('idx_app_user_is_admin', 'app_user', ['is_admin'], unique=False)
op.create_index('idx_app_user_is_super_admin', 'app_user', ['is_super_admin'], unique=False)
op.create_index('idx_app_user_role', 'app_user', ['role'], unique=False)
op.create_table('market_close',
sa.Column('symbol', sa.String(), nullable=False),
sa.Column('date', sa.Date(), nullable=False),
sa.Column('close', sa.Numeric(), nullable=False),
sa.PrimaryKeyConstraint('symbol', 'date')
)
op.create_index('idx_market_close_date', 'market_close', ['date'], unique=False)
op.create_index('idx_market_close_symbol', 'market_close', ['symbol'], unique=False)
op.create_table('app_session',
sa.Column('id', sa.String(), nullable=False),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('last_seen_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_app_session_expires_at', 'app_session', ['expires_at'], unique=False)
op.create_index('idx_app_session_user_id', 'app_session', ['user_id'], unique=False)
op.create_table('strategy_run',
sa.Column('run_id', sa.String(), nullable=False),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('started_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('stopped_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('status', sa.String(), nullable=False),
sa.Column('strategy', sa.String(), nullable=True),
sa.Column('mode', sa.String(), nullable=True),
sa.Column('broker', sa.String(), nullable=True),
sa.Column('meta', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.CheckConstraint("status IN ('RUNNING','STOPPED','ERROR')", name='chk_strategy_run_status'),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('run_id'),
sa.UniqueConstraint('user_id', 'run_id', name='uq_strategy_run_user_run')
)
op.create_index('idx_strategy_run_user_created', 'strategy_run', ['user_id', 'created_at'], unique=False)
op.create_index('idx_strategy_run_user_status', 'strategy_run', ['user_id', 'status'], unique=False)
op.create_index('uq_one_running_run_per_user', 'strategy_run', ['user_id'], unique=True, postgresql_where=sa.text("status = 'RUNNING'"))
op.create_table('user_broker',
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('broker', sa.String(), nullable=True),
sa.Column('connected', sa.Boolean(), server_default=sa.text('false'), nullable=False),
sa.Column('access_token', sa.Text(), nullable=True),
sa.Column('connected_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('api_key', sa.Text(), nullable=True),
sa.Column('user_name', sa.Text(), nullable=True),
sa.Column('broker_user_id', sa.Text(), nullable=True),
sa.Column('pending_broker', sa.Text(), nullable=True),
sa.Column('pending_api_key', sa.Text(), nullable=True),
sa.Column('pending_api_secret', sa.Text(), nullable=True),
sa.Column('pending_started_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('user_id')
)
op.create_index('idx_user_broker_broker', 'user_broker', ['broker'], unique=False)
op.create_index('idx_user_broker_connected', 'user_broker', ['connected'], unique=False)
op.create_table('zerodha_request_token',
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('request_token', sa.Text(), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('user_id')
)
op.create_table('zerodha_session',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('linked_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('api_key', sa.Text(), nullable=True),
sa.Column('access_token', sa.Text(), nullable=True),
sa.Column('request_token', sa.Text(), nullable=True),
sa.Column('user_name', sa.Text(), nullable=True),
sa.Column('broker_user_id', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_zerodha_session_linked_at', 'zerodha_session', ['linked_at'], unique=False)
op.create_index('idx_zerodha_session_user_id', 'zerodha_session', ['user_id'], unique=False)
op.create_table('engine_event',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('ts', sa.DateTime(timezone=True), nullable=False),
sa.Column('event', sa.String(), nullable=True),
sa.Column('data', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('message', sa.Text(), nullable=True),
sa.Column('meta', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.ForeignKeyConstraint(['run_id'], ['strategy_run.run_id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index('idx_engine_event_ts', 'engine_event', ['ts'], unique=False)
op.create_index('idx_engine_event_user_run_ts', 'engine_event', ['user_id', 'run_id', 'ts'], unique=False)
op.create_table('engine_state',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.Column('total_invested', sa.Numeric(), nullable=True),
sa.Column('nifty_units', sa.Numeric(), nullable=True),
sa.Column('gold_units', sa.Numeric(), nullable=True),
sa.Column('last_sip_ts', sa.DateTime(timezone=True), nullable=True),
sa.Column('last_run', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['user_id', 'run_id'], ['strategy_run.user_id', 'strategy_run.run_id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'run_id', name='uq_engine_state_user_run')
)
op.create_table('engine_state_paper',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.Column('initial_cash', sa.Numeric(), nullable=True),
sa.Column('cash', sa.Numeric(), nullable=True),
sa.Column('total_invested', sa.Numeric(), nullable=True),
sa.Column('nifty_units', sa.Numeric(), nullable=True),
sa.Column('gold_units', sa.Numeric(), nullable=True),
sa.Column('last_sip_ts', sa.DateTime(timezone=True), nullable=True),
sa.Column('last_run', sa.DateTime(timezone=True), nullable=True),
sa.Column('sip_frequency_value', sa.Integer(), nullable=True),
sa.Column('sip_frequency_unit', sa.String(), nullable=True),
sa.CheckConstraint('cash >= 0', name='chk_engine_state_paper_cash_non_negative'),
sa.ForeignKeyConstraint(['user_id', 'run_id'], ['strategy_run.user_id', 'strategy_run.run_id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'run_id', name='uq_engine_state_paper_user_run')
)
op.create_table('engine_status',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.Column('status', sa.String(), nullable=False),
sa.Column('last_updated', sa.DateTime(timezone=True), nullable=False),
sa.ForeignKeyConstraint(['user_id', 'run_id'], ['strategy_run.user_id', 'strategy_run.run_id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'run_id', name='uq_engine_status_user_run')
)
op.create_index('idx_engine_status_user_run', 'engine_status', ['user_id', 'run_id'], unique=False)
op.create_table('event_ledger',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.Column('timestamp', sa.DateTime(timezone=True), nullable=False),
sa.Column('logical_time', sa.DateTime(timezone=True), nullable=False),
sa.Column('event', sa.String(), nullable=False),
sa.Column('nifty_units', sa.Numeric(), nullable=True),
sa.Column('gold_units', sa.Numeric(), nullable=True),
sa.Column('nifty_price', sa.Numeric(), nullable=True),
sa.Column('gold_price', sa.Numeric(), nullable=True),
sa.Column('amount', sa.Numeric(), nullable=True),
sa.ForeignKeyConstraint(['user_id', 'run_id'], ['strategy_run.user_id', 'strategy_run.run_id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'run_id', 'event', 'logical_time', name='uq_event_ledger_event_time')
)
op.create_index('idx_event_ledger_ts', 'event_ledger', ['timestamp'], unique=False)
op.create_index('idx_event_ledger_user_run_ts', 'event_ledger', ['user_id', 'run_id', 'timestamp'], unique=False)
op.create_table('mtm_ledger',
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.Column('timestamp', sa.DateTime(timezone=True), nullable=False),
sa.Column('logical_time', sa.DateTime(timezone=True), nullable=False),
sa.Column('nifty_units', sa.Numeric(), nullable=True),
sa.Column('gold_units', sa.Numeric(), nullable=True),
sa.Column('nifty_price', sa.Numeric(), nullable=True),
sa.Column('gold_price', sa.Numeric(), nullable=True),
sa.Column('nifty_value', sa.Numeric(), nullable=True),
sa.Column('gold_value', sa.Numeric(), nullable=True),
sa.Column('portfolio_value', sa.Numeric(), nullable=True),
sa.Column('total_invested', sa.Numeric(), nullable=True),
sa.Column('pnl', sa.Numeric(), nullable=True),
sa.ForeignKeyConstraint(['user_id', 'run_id'], ['strategy_run.user_id', 'strategy_run.run_id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('user_id', 'run_id', 'logical_time')
)
op.create_index('idx_mtm_ledger_ts', 'mtm_ledger', ['timestamp'], unique=False)
op.create_index('idx_mtm_ledger_user_run_ts', 'mtm_ledger', ['user_id', 'run_id', 'timestamp'], unique=False)
op.create_table('paper_broker_account',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.Column('cash', sa.Numeric(), nullable=False),
sa.CheckConstraint('cash >= 0', name='chk_paper_broker_cash_non_negative'),
sa.ForeignKeyConstraint(['user_id', 'run_id'], ['strategy_run.user_id', 'strategy_run.run_id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'run_id', name='uq_paper_broker_account_user_run')
)
op.create_table('paper_equity_curve',
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.Column('timestamp', sa.DateTime(timezone=True), nullable=False),
sa.Column('logical_time', sa.DateTime(timezone=True), nullable=False),
sa.Column('equity', sa.Numeric(), nullable=False),
sa.Column('pnl', sa.Numeric(), nullable=True),
sa.ForeignKeyConstraint(['user_id', 'run_id'], ['strategy_run.user_id', 'strategy_run.run_id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('user_id', 'run_id', 'logical_time')
)
op.create_index('idx_paper_equity_curve_ts', 'paper_equity_curve', ['timestamp'], unique=False)
op.create_index('idx_paper_equity_curve_user_run_ts', 'paper_equity_curve', ['user_id', 'run_id', 'timestamp'], unique=False)
op.create_table('paper_order',
sa.Column('id', sa.String(), nullable=False),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.Column('symbol', sa.String(), nullable=False),
sa.Column('side', sa.String(), nullable=False),
sa.Column('qty', sa.Numeric(), nullable=False),
sa.Column('price', sa.Numeric(), nullable=True),
sa.Column('status', sa.String(), nullable=False),
sa.Column('timestamp', sa.DateTime(timezone=True), nullable=False),
sa.Column('logical_time', sa.DateTime(timezone=True), nullable=False),
sa.CheckConstraint('price >= 0', name='chk_paper_order_price_non_negative'),
sa.CheckConstraint('qty > 0', name='chk_paper_order_qty_positive'),
sa.ForeignKeyConstraint(['user_id', 'run_id'], ['strategy_run.user_id', 'strategy_run.run_id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'run_id', 'id', name='uq_paper_order_scope_id'),
sa.UniqueConstraint('user_id', 'run_id', 'logical_time', 'symbol', 'side', name='uq_paper_order_logical_key')
)
op.create_index('idx_paper_order_ts', 'paper_order', ['timestamp'], unique=False)
op.create_index('idx_paper_order_user_run_ts', 'paper_order', ['user_id', 'run_id', 'timestamp'], unique=False)
op.create_table('paper_position',
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.Column('symbol', sa.String(), nullable=False),
sa.Column('qty', sa.Numeric(), nullable=False),
sa.Column('avg_price', sa.Numeric(), nullable=True),
sa.Column('last_price', sa.Numeric(), nullable=True),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.CheckConstraint('qty > 0', name='chk_paper_position_qty_positive'),
sa.ForeignKeyConstraint(['user_id', 'run_id'], ['strategy_run.user_id', 'strategy_run.run_id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('user_id', 'run_id', 'symbol'),
sa.UniqueConstraint('user_id', 'run_id', 'symbol', name='uq_paper_position_scope')
)
op.create_index('idx_paper_position_user_run', 'paper_position', ['user_id', 'run_id'], unique=False)
op.create_table('strategy_config',
sa.Column('id', sa.BigInteger(), autoincrement=True, nullable=False),
sa.Column('strategy', sa.String(), nullable=True),
sa.Column('sip_amount', sa.Numeric(), nullable=True),
sa.Column('sip_frequency_value', sa.Integer(), nullable=True),
sa.Column('sip_frequency_unit', sa.String(), nullable=True),
sa.Column('mode', sa.String(), nullable=True),
sa.Column('broker', sa.String(), nullable=True),
sa.Column('active', sa.Boolean(), nullable=True),
sa.Column('frequency', sa.Text(), nullable=True),
sa.Column('frequency_days', sa.Integer(), nullable=True),
sa.Column('unit', sa.String(), nullable=True),
sa.Column('next_run', sa.DateTime(timezone=True), nullable=True),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.ForeignKeyConstraint(['run_id'], ['strategy_run.run_id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'run_id', name='uq_strategy_config_user_run')
)
op.create_table('strategy_log',
sa.Column('seq', sa.BigInteger(), nullable=False),
sa.Column('ts', sa.DateTime(timezone=True), nullable=False),
sa.Column('level', sa.String(), nullable=True),
sa.Column('category', sa.String(), nullable=True),
sa.Column('event', sa.String(), nullable=True),
sa.Column('message', sa.Text(), nullable=True),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.Column('meta', postgresql.JSONB(astext_type=sa.Text()), nullable=True),
sa.ForeignKeyConstraint(['run_id'], ['strategy_run.run_id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('seq')
)
op.create_index('idx_strategy_log_event', 'strategy_log', ['event'], unique=False)
op.create_index('idx_strategy_log_ts', 'strategy_log', ['ts'], unique=False)
op.create_index('idx_strategy_log_user_run_ts', 'strategy_log', ['user_id', 'run_id', 'ts'], unique=False)
op.create_table('paper_trade',
sa.Column('id', sa.String(), nullable=False),
sa.Column('order_id', sa.String(), nullable=True),
sa.Column('user_id', sa.String(), nullable=False),
sa.Column('run_id', sa.String(), nullable=False),
sa.Column('symbol', sa.String(), nullable=False),
sa.Column('side', sa.String(), nullable=False),
sa.Column('qty', sa.Numeric(), nullable=False),
sa.Column('price', sa.Numeric(), nullable=False),
sa.Column('timestamp', sa.DateTime(timezone=True), nullable=False),
sa.Column('logical_time', sa.DateTime(timezone=True), nullable=False),
sa.CheckConstraint('price >= 0', name='chk_paper_trade_price_non_negative'),
sa.CheckConstraint('qty > 0', name='chk_paper_trade_qty_positive'),
sa.ForeignKeyConstraint(['user_id', 'run_id', 'order_id'], ['paper_order.user_id', 'paper_order.run_id', 'paper_order.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id', 'run_id'], ['strategy_run.user_id', 'strategy_run.run_id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['user_id'], ['app_user.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('user_id', 'run_id', 'id', name='uq_paper_trade_scope_id'),
sa.UniqueConstraint('user_id', 'run_id', 'logical_time', 'symbol', 'side', name='uq_paper_trade_logical_key')
)
op.create_index('idx_paper_trade_ts', 'paper_trade', ['timestamp'], unique=False)
op.create_index('idx_paper_trade_user_run_ts', 'paper_trade', ['user_id', 'run_id', 'timestamp'], unique=False)
# admin views and protections
op.execute(
"""
CREATE OR REPLACE FUNCTION prevent_super_admin_delete()
RETURNS trigger AS $$
BEGIN
IF OLD.role = 'SUPER_ADMIN' OR OLD.is_super_admin THEN
RAISE EXCEPTION 'cannot delete super admin user';
END IF;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
"""
)
op.execute(
"""
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_trigger WHERE tgname = 'trg_prevent_super_admin_delete') THEN
CREATE TRIGGER trg_prevent_super_admin_delete
BEFORE DELETE ON app_user
FOR EACH ROW
EXECUTE FUNCTION prevent_super_admin_delete();
END IF;
END $$;
"""
)
op.create_index('idx_event_ledger_user_run_logical', 'event_ledger', ['user_id', 'run_id', 'logical_time'], unique=False)
op.execute(
"""
CREATE OR REPLACE VIEW admin_user_metrics AS
WITH session_stats AS (
SELECT
user_id,
MIN(created_at) AS first_session_at,
MAX(COALESCE(last_seen_at, created_at)) AS last_login_at
FROM app_session
GROUP BY user_id
),
run_stats AS (
SELECT
user_id,
COUNT(*) AS runs_count,
MAX(CASE WHEN status = 'RUNNING' THEN run_id END) AS active_run_id,
MAX(CASE WHEN status = 'RUNNING' THEN status END) AS active_run_status,
MIN(created_at) AS first_run_at
FROM strategy_run
GROUP BY user_id
),
broker_stats AS (
SELECT user_id, BOOL_OR(connected) AS broker_connected
FROM user_broker
GROUP BY user_id
)
SELECT
u.id AS user_id,
u.username,
u.role,
(u.role IN ('ADMIN','SUPER_ADMIN')) AS is_admin,
COALESCE(session_stats.first_session_at, run_stats.first_run_at) AS created_at,
session_stats.last_login_at,
COALESCE(run_stats.runs_count, 0) AS runs_count,
run_stats.active_run_id,
run_stats.active_run_status,
COALESCE(broker_stats.broker_connected, FALSE) AS broker_connected
FROM app_user u
LEFT JOIN session_stats ON session_stats.user_id = u.id
LEFT JOIN run_stats ON run_stats.user_id = u.id
LEFT JOIN broker_stats ON broker_stats.user_id = u.id;
"""
)
op.execute(
"""
CREATE OR REPLACE VIEW admin_run_metrics AS
WITH order_stats AS (
SELECT user_id, run_id, COUNT(*) AS order_count, MAX("timestamp") AS last_order_time
FROM paper_order
GROUP BY user_id, run_id
),
trade_stats AS (
SELECT user_id, run_id, COUNT(*) AS trade_count, MAX("timestamp") AS last_trade_time
FROM paper_trade
GROUP BY user_id, run_id
),
event_stats AS (
SELECT
user_id,
run_id,
MAX("timestamp") AS last_event_time,
MAX(CASE WHEN event = 'SIP_EXECUTED' THEN "timestamp" END) AS last_sip_time
FROM event_ledger
GROUP BY user_id, run_id
),
equity_latest AS (
SELECT DISTINCT ON (user_id, run_id)
user_id,
run_id,
equity AS equity_latest,
pnl AS pnl_latest,
"timestamp" AS equity_ts
FROM paper_equity_curve
ORDER BY user_id, run_id, "timestamp" DESC
),
mtm_latest AS (
SELECT DISTINCT ON (user_id, run_id)
user_id,
run_id,
"timestamp" AS mtm_ts
FROM mtm_ledger
ORDER BY user_id, run_id, "timestamp" DESC
),
log_latest AS (
SELECT user_id, run_id, MAX(ts) AS last_log_time
FROM strategy_log
GROUP BY user_id, run_id
),
engine_latest AS (
SELECT user_id, run_id, MAX(ts) AS last_engine_time
FROM engine_event
GROUP BY user_id, run_id
),
activity AS (
SELECT user_id, run_id, MAX(ts) AS last_event_time
FROM (
SELECT user_id, run_id, ts FROM engine_event
UNION ALL
SELECT user_id, run_id, ts FROM strategy_log
UNION ALL
SELECT user_id, run_id, "timestamp" AS ts FROM paper_order
UNION ALL
SELECT user_id, run_id, "timestamp" AS ts FROM paper_trade
UNION ALL
SELECT user_id, run_id, "timestamp" AS ts FROM mtm_ledger
UNION ALL
SELECT user_id, run_id, "timestamp" AS ts FROM paper_equity_curve
UNION ALL
SELECT user_id, run_id, "timestamp" AS ts FROM event_ledger
) t
GROUP BY user_id, run_id
)
SELECT
sr.run_id,
sr.user_id,
sr.status,
sr.created_at,
sr.started_at,
sr.stopped_at,
sr.strategy,
sr.mode,
sr.broker,
sc.sip_amount,
sc.sip_frequency_value,
sc.sip_frequency_unit,
sc.next_run AS next_sip_time,
activity.last_event_time,
event_stats.last_sip_time,
COALESCE(order_stats.order_count, 0) AS order_count,
COALESCE(trade_stats.trade_count, 0) AS trade_count,
equity_latest.equity_latest,
equity_latest.pnl_latest
FROM strategy_run sr
LEFT JOIN strategy_config sc
ON sc.user_id = sr.user_id AND sc.run_id = sr.run_id
LEFT JOIN order_stats
ON order_stats.user_id = sr.user_id AND order_stats.run_id = sr.run_id
LEFT JOIN trade_stats
ON trade_stats.user_id = sr.user_id AND trade_stats.run_id = sr.run_id
LEFT JOIN event_stats
ON event_stats.user_id = sr.user_id AND event_stats.run_id = sr.run_id
LEFT JOIN equity_latest
ON equity_latest.user_id = sr.user_id AND equity_latest.run_id = sr.run_id
LEFT JOIN mtm_latest
ON mtm_latest.user_id = sr.user_id AND mtm_latest.run_id = sr.run_id
LEFT JOIN log_latest
ON log_latest.user_id = sr.user_id AND log_latest.run_id = sr.run_id
LEFT JOIN engine_latest
ON engine_latest.user_id = sr.user_id AND engine_latest.run_id = sr.run_id
LEFT JOIN activity
ON activity.user_id = sr.user_id AND activity.run_id = sr.run_id;
"""
)
op.execute(
"""
CREATE OR REPLACE VIEW admin_engine_health AS
WITH activity AS (
SELECT user_id, run_id, MAX(ts) AS last_event_time
FROM (
SELECT user_id, run_id, ts FROM engine_event
UNION ALL
SELECT user_id, run_id, ts FROM strategy_log
UNION ALL
SELECT user_id, run_id, "timestamp" AS ts FROM event_ledger
) t
GROUP BY user_id, run_id
)
SELECT
sr.run_id,
sr.user_id,
sr.status,
activity.last_event_time,
es.status AS engine_status,
es.last_updated AS engine_status_ts
FROM strategy_run sr
LEFT JOIN activity
ON activity.user_id = sr.user_id AND activity.run_id = sr.run_id
LEFT JOIN engine_status es
ON es.user_id = sr.user_id AND es.run_id = sr.run_id;
"""
)
op.execute(
"""
CREATE OR REPLACE VIEW admin_order_stats AS
SELECT
user_id,
run_id,
COUNT(*) AS total_orders,
COUNT(*) FILTER (WHERE "timestamp" >= now() - interval '24 hours') AS orders_last_24h,
COUNT(*) FILTER (WHERE status = 'FILLED') AS filled_orders
FROM paper_order
GROUP BY user_id, run_id;
"""
)
op.execute(
"""
CREATE OR REPLACE VIEW admin_ledger_stats AS
WITH mtm_latest AS (
SELECT DISTINCT ON (user_id, run_id)
user_id,
run_id,
portfolio_value,
pnl,
"timestamp" AS mtm_ts
FROM mtm_ledger
ORDER BY user_id, run_id, "timestamp" DESC
),
equity_latest AS (
SELECT DISTINCT ON (user_id, run_id)
user_id,
run_id,
equity,
pnl,
"timestamp" AS equity_ts
FROM paper_equity_curve
ORDER BY user_id, run_id, "timestamp" DESC
)
SELECT
sr.user_id,
sr.run_id,
mtm_latest.portfolio_value AS mtm_value,
mtm_latest.pnl AS mtm_pnl,
mtm_latest.mtm_ts,
equity_latest.equity AS equity_value,
equity_latest.pnl AS equity_pnl,
equity_latest.equity_ts
FROM strategy_run sr
LEFT JOIN mtm_latest
ON mtm_latest.user_id = sr.user_id AND mtm_latest.run_id = sr.run_id
LEFT JOIN equity_latest
ON equity_latest.user_id = sr.user_id AND equity_latest.run_id = sr.run_id;
"""
)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.execute("DROP VIEW IF EXISTS admin_ledger_stats;")
op.execute("DROP VIEW IF EXISTS admin_order_stats;")
op.execute("DROP VIEW IF EXISTS admin_engine_health;")
op.execute("DROP VIEW IF EXISTS admin_run_metrics;")
op.execute("DROP VIEW IF EXISTS admin_user_metrics;")
op.execute("DROP TRIGGER IF EXISTS trg_prevent_super_admin_delete ON app_user;")
op.execute("DROP FUNCTION IF EXISTS prevent_super_admin_delete;")
op.drop_index('idx_paper_trade_user_run_ts', table_name='paper_trade')
op.drop_index('idx_paper_trade_ts', table_name='paper_trade')
op.drop_table('paper_trade')
op.drop_index('idx_strategy_log_user_run_ts', table_name='strategy_log')
op.drop_index('idx_strategy_log_ts', table_name='strategy_log')
op.drop_index('idx_strategy_log_event', table_name='strategy_log')
op.drop_table('strategy_log')
op.drop_table('strategy_config')
op.drop_index('idx_paper_position_user_run', table_name='paper_position')
op.drop_table('paper_position')
op.drop_index('idx_paper_order_user_run_ts', table_name='paper_order')
op.drop_index('idx_paper_order_ts', table_name='paper_order')
op.drop_table('paper_order')
op.drop_index('idx_paper_equity_curve_user_run_ts', table_name='paper_equity_curve')
op.drop_index('idx_paper_equity_curve_ts', table_name='paper_equity_curve')
op.drop_table('paper_equity_curve')
op.drop_table('paper_broker_account')
op.drop_index('idx_mtm_ledger_user_run_ts', table_name='mtm_ledger')
op.drop_index('idx_mtm_ledger_ts', table_name='mtm_ledger')
op.drop_table('mtm_ledger')
op.drop_index('idx_event_ledger_user_run_logical', table_name='event_ledger')
op.drop_index('idx_event_ledger_user_run_ts', table_name='event_ledger')
op.drop_index('idx_event_ledger_ts', table_name='event_ledger')
op.drop_table('event_ledger')
op.drop_index('idx_engine_status_user_run', table_name='engine_status')
op.drop_table('engine_status')
op.drop_table('engine_state_paper')
op.drop_table('engine_state')
op.drop_index('idx_engine_event_user_run_ts', table_name='engine_event')
op.drop_index('idx_engine_event_ts', table_name='engine_event')
op.drop_table('engine_event')
op.drop_index('idx_zerodha_session_user_id', table_name='zerodha_session')
op.drop_index('idx_zerodha_session_linked_at', table_name='zerodha_session')
op.drop_table('zerodha_session')
op.drop_table('zerodha_request_token')
op.drop_index('idx_user_broker_connected', table_name='user_broker')
op.drop_index('idx_user_broker_broker', table_name='user_broker')
op.drop_table('user_broker')
op.drop_index('uq_one_running_run_per_user', table_name='strategy_run', postgresql_where=sa.text("status = 'RUNNING'"))
op.drop_index('idx_strategy_run_user_status', table_name='strategy_run')
op.drop_index('idx_strategy_run_user_created', table_name='strategy_run')
op.drop_table('strategy_run')
op.drop_index('idx_app_session_user_id', table_name='app_session')
op.drop_index('idx_app_session_expires_at', table_name='app_session')
op.drop_table('app_session')
op.drop_index('idx_market_close_symbol', table_name='market_close')
op.drop_index('idx_market_close_date', table_name='market_close')
op.drop_table('market_close')
op.drop_index('idx_app_user_role', table_name='app_user')
op.drop_index('idx_app_user_is_super_admin', table_name='app_user')
op.drop_index('idx_app_user_is_admin', table_name='app_user')
op.drop_table('app_user')
op.drop_table('admin_role_audit')
op.drop_table('admin_audit_log')
# ### end Alembic commands ###