TradingAgents/migrations/versions/004_add_settings_model.py

197 lines
6.0 KiB
Python

"""Add Settings model for user preferences and risk management
Revision ID: 004
Revises: 003
Create Date: 2025-12-26 14:30:00.000000
This migration adds the settings table for managing user trading preferences,
risk profiles, and alert configurations. Each user has exactly one Settings
record (one-to-one relationship).
Table: settings
Columns:
- id: Primary key, auto-increment
- user_id: Foreign key to users.id (cascade delete, unique)
- risk_profile: Risk tolerance profile (ENUM: CONSERVATIVE, MODERATE, AGGRESSIVE)
- risk_score: Numeric risk score 0-10 (NUMERIC 5,2, default: 5.0)
- max_position_pct: Max % of portfolio for single position (NUMERIC 5,2, default: 10.0)
- max_portfolio_risk_pct: Max portfolio-wide risk % (NUMERIC 5,2, default: 2.0)
- investment_horizon_years: Investment time horizon in years (INTEGER, default: 5)
- alert_preferences: JSON config for notifications (TEXT/JSON, default: '{}')
- created_at: Timestamp when created (auto)
- updated_at: Timestamp when last updated (auto)
Constraints:
- UNIQUE (user_id): One settings per user (one-to-one)
- CHECK risk_score >= 0 AND risk_score <= 10
- CHECK max_position_pct >= 0 AND max_position_pct <= 100
- CHECK max_portfolio_risk_pct >= 0 AND max_portfolio_risk_pct <= 100
- CHECK investment_horizon_years >= 0
Indexes:
- ix_settings_user_id: Unique index on user_id (one-to-one lookup)
Relationships:
- settings.user_id -> users.id (CASCADE DELETE)
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '004'
down_revision: Union[str, None] = '003'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Create settings table with all constraints and indexes.
Creates the settings table for managing user trading preferences
with proper foreign keys, constraints, and indexes.
"""
# Create settings table
op.create_table(
'settings',
# Primary key
sa.Column(
'id',
sa.Integer(),
nullable=False,
primary_key=True,
autoincrement=True
),
# Foreign key to users (cascade delete, unique for one-to-one)
sa.Column(
'user_id',
sa.Integer(),
sa.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False,
unique=True,
comment='User who owns these settings (one-to-one)'
),
# Risk profile enum
sa.Column(
'risk_profile',
sa.String(length=20),
nullable=False,
server_default='MODERATE',
comment='Risk tolerance: CONSERVATIVE, MODERATE, or AGGRESSIVE'
),
# Risk score (0-10 scale with 2 decimal places)
sa.Column(
'risk_score',
sa.Numeric(precision=5, scale=2),
nullable=False,
server_default='5.0',
comment='Numeric risk score from 0 (conservative) to 10 (aggressive)'
),
# Position sizing limits (percentages with 2 decimal places)
sa.Column(
'max_position_pct',
sa.Numeric(precision=5, scale=2),
nullable=False,
server_default='10.0',
comment='Maximum percentage of portfolio for single position (0-100)'
),
sa.Column(
'max_portfolio_risk_pct',
sa.Numeric(precision=5, scale=2),
nullable=False,
server_default='2.0',
comment='Maximum portfolio-wide risk percentage (0-100)'
),
# Investment horizon
sa.Column(
'investment_horizon_years',
sa.Integer(),
nullable=False,
server_default='5',
comment='Investment time horizon in years'
),
# Alert preferences (JSON)
# Use TEXT for SQLite compatibility, PostgreSQL will handle as JSON
sa.Column(
'alert_preferences',
sa.Text(),
nullable=False,
server_default='{}',
comment='JSON configuration for email/SMS/push notifications'
),
# Timestamps (from TimestampMixin)
sa.Column(
'created_at',
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text('CURRENT_TIMESTAMP'),
comment='Timestamp when settings were created'
),
sa.Column(
'updated_at',
sa.DateTime(timezone=True),
nullable=False,
server_default=sa.text('CURRENT_TIMESTAMP'),
comment='Timestamp when settings were last updated'
),
# Table-level constraints
# Unique constraint: one settings per user
sa.UniqueConstraint(
'user_id',
name='uq_settings_user_id'
),
# Check constraints: valid numeric ranges
sa.CheckConstraint(
'risk_score >= 0 AND risk_score <= 10',
name='ck_settings_risk_score_range'
),
sa.CheckConstraint(
'max_position_pct >= 0 AND max_position_pct <= 100',
name='ck_settings_max_position_pct_range'
),
sa.CheckConstraint(
'max_portfolio_risk_pct >= 0 AND max_portfolio_risk_pct <= 100',
name='ck_settings_max_portfolio_risk_pct_range'
),
sa.CheckConstraint(
'investment_horizon_years >= 0',
name='ck_settings_investment_horizon_positive'
),
)
# Create unique index on user_id for efficient one-to-one lookups
op.create_index(
'ix_settings_user_id',
'settings',
['user_id'],
unique=True
)
def downgrade() -> None:
"""Drop settings table and all associated indexes and constraints.
WARNING: This will permanently delete all settings data!
"""
# Drop index
op.drop_index('ix_settings_user_id', 'settings')
# Drop the table (constraints are dropped automatically)
op.drop_table('settings')