197 lines
6.0 KiB
Python
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')
|