trading-platform-database-v2/ddl/schemas/users/tables/005_risk_profiles.sql
rckrdmrd b86dfa2e06 [DDL] feat: Sprint 1 - Add 12 tables for users, admin, notifications, market_data
## New Tables Created (Sprint 1 - DDL Roadmap Q1-2026)

### users schema (4 tables):
- profiles: Extended user profile information
- user_settings: User preferences and configurations
- kyc_verifications: KYC/AML verification records
- risk_profiles: Trading risk assessment profiles

### admin schema (3 tables):
- admin_roles: Platform administrative roles
- platform_analytics: Aggregated platform metrics
- api_keys: Programmatic API access keys

### notifications schema (1 table):
- notifications: Multi-channel notification system

### market_data schema (4 tables):
- tickers: Financial instruments catalog
- ohlcv_5m: 5-minute OHLCV price data
- technical_indicators: Pre-calculated TA indicators
- ohlcv_5m_staging: Staging table for data ingestion

## Features:
- Multi-tenancy with RLS policies
- Comprehensive indexes for query optimization
- Triggers for computed fields and timestamps
- Helper functions for common operations
- Views for dashboard and reporting
- Full GRANTS configuration

Roadmap: orchestration/planes/ROADMAP-IMPLEMENTACION-DDL-2026-Q1.md

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 19:41:53 -06:00

242 lines
8.4 KiB
PL/PgSQL

-- ============================================================================
-- SCHEMA: users
-- TABLE: risk_profiles
-- DESCRIPTION: Perfil de riesgo del usuario para trading
-- VERSION: 1.0.0
-- CREATED: 2026-01-16
-- SPRINT: Sprint 1 - DDL Implementation Roadmap Q1-2026
-- ============================================================================
-- Enum para perfil de riesgo
DO $$ BEGIN
CREATE TYPE users.risk_tolerance AS ENUM (
'conservative', -- Bajo riesgo, preservacion de capital
'moderate', -- Riesgo moderado, balance crecimiento/seguridad
'aggressive', -- Alto riesgo, maximizar retornos
'speculative' -- Muy alto riesgo, trading activo
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Enum para experiencia de trading
DO $$ BEGIN
CREATE TYPE users.trading_experience AS ENUM (
'none', -- Sin experiencia
'beginner', -- Menos de 1 año
'intermediate', -- 1-3 años
'advanced', -- 3-5 años
'expert' -- 5+ años
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tabla de Perfiles de Riesgo
CREATE TABLE IF NOT EXISTS users.risk_profiles (
-- Identificadores
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL UNIQUE REFERENCES users.users(id) ON DELETE CASCADE,
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
-- Perfil de riesgo
risk_tolerance users.risk_tolerance NOT NULL DEFAULT 'moderate',
trading_experience users.trading_experience NOT NULL DEFAULT 'none',
-- Cuestionario de adecuacion (suitability)
questionnaire_completed BOOLEAN NOT NULL DEFAULT FALSE,
questionnaire_completed_at TIMESTAMPTZ,
questionnaire_version VARCHAR(20),
questionnaire_responses JSONB,
questionnaire_score INTEGER CHECK (questionnaire_score BETWEEN 0 AND 100),
-- Objetivos de inversion
investment_objectives JSONB DEFAULT '{
"primary_goal": "growth",
"time_horizon": "medium_term",
"liquidity_needs": "moderate"
}'::JSONB,
-- Situacion financiera
financial_situation JSONB DEFAULT '{
"net_worth_range": "not_specified",
"annual_income_range": "not_specified",
"liquid_assets_range": "not_specified",
"investable_amount_range": "not_specified"
}'::JSONB,
-- Conocimiento de productos
product_knowledge JSONB DEFAULT '{
"forex": "none",
"stocks": "none",
"options": "none",
"futures": "none",
"crypto": "none",
"leverage_products": "none"
}'::JSONB,
-- Historial de trading declarado
trading_history JSONB DEFAULT '{
"years_trading": 0,
"average_trades_per_month": 0,
"largest_single_trade": 0,
"has_professional_experience": false
}'::JSONB,
-- Limites basados en perfil
max_position_size_percent DECIMAL(5, 2) NOT NULL DEFAULT 5.00, -- % del capital por posicion
max_daily_loss_percent DECIMAL(5, 2) NOT NULL DEFAULT 3.00, -- % perdida diaria maxima
max_total_exposure_percent DECIMAL(5, 2) NOT NULL DEFAULT 50.00, -- % exposicion total
max_leverage INTEGER NOT NULL DEFAULT 10,
allowed_instruments JSONB DEFAULT '["forex_majors", "indices"]'::JSONB,
-- Score de riesgo calculado
calculated_risk_score INTEGER CHECK (calculated_risk_score BETWEEN 1 AND 10),
risk_score_factors JSONB,
last_risk_assessment TIMESTAMPTZ,
-- Warnings y restricciones
risk_warnings_acknowledged JSONB DEFAULT '[]'::JSONB,
trading_restrictions JSONB DEFAULT '[]'::JSONB,
requires_additional_disclosure BOOLEAN NOT NULL DEFAULT FALSE,
-- Clasificacion regulatoria
regulatory_classification VARCHAR(50) DEFAULT 'retail', -- 'retail', 'professional', 'eligible_counterparty'
classification_request_status VARCHAR(50),
classification_approved_at TIMESTAMPTZ,
-- Metadata
metadata JSONB DEFAULT '{}'::JSONB,
-- Timestamps
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE users.risk_profiles IS
'Perfil de riesgo del usuario basado en cuestionario de adecuacion';
COMMENT ON COLUMN users.risk_profiles.risk_tolerance IS
'Tolerancia al riesgo declarada: conservative, moderate, aggressive, speculative';
COMMENT ON COLUMN users.risk_profiles.calculated_risk_score IS
'Score de riesgo calculado (1-10), donde 10 es maximo riesgo permitido';
COMMENT ON COLUMN users.risk_profiles.regulatory_classification IS
'Clasificacion segun MiFID II: retail, professional, eligible_counterparty';
-- Indices
CREATE INDEX IF NOT EXISTS idx_risk_profiles_user_id
ON users.risk_profiles(user_id);
CREATE INDEX IF NOT EXISTS idx_risk_profiles_tenant_id
ON users.risk_profiles(tenant_id);
CREATE INDEX IF NOT EXISTS idx_risk_profiles_tolerance
ON users.risk_profiles(risk_tolerance);
CREATE INDEX IF NOT EXISTS idx_risk_profiles_experience
ON users.risk_profiles(trading_experience);
CREATE INDEX IF NOT EXISTS idx_risk_profiles_classification
ON users.risk_profiles(regulatory_classification);
CREATE INDEX IF NOT EXISTS idx_risk_profiles_pending_questionnaire
ON users.risk_profiles(tenant_id, created_at)
WHERE questionnaire_completed = FALSE;
-- GIN index para busquedas en JSONB
CREATE INDEX IF NOT EXISTS idx_risk_profiles_knowledge_gin
ON users.risk_profiles USING GIN (product_knowledge);
-- Trigger para updated_at
DROP TRIGGER IF EXISTS risk_profile_updated_at ON users.risk_profiles;
CREATE TRIGGER risk_profile_updated_at
BEFORE UPDATE ON users.risk_profiles
FOR EACH ROW
EXECUTE FUNCTION users.update_user_timestamp();
-- Funcion para calcular risk score automaticamente
CREATE OR REPLACE FUNCTION users.calculate_risk_score()
RETURNS TRIGGER AS $$
DECLARE
v_score INTEGER := 5; -- Base score
v_experience_weight INTEGER;
v_tolerance_weight INTEGER;
BEGIN
-- Ajustar por experiencia
CASE NEW.trading_experience
WHEN 'none' THEN v_experience_weight := -2;
WHEN 'beginner' THEN v_experience_weight := -1;
WHEN 'intermediate' THEN v_experience_weight := 0;
WHEN 'advanced' THEN v_experience_weight := 1;
WHEN 'expert' THEN v_experience_weight := 2;
ELSE v_experience_weight := 0;
END CASE;
-- Ajustar por tolerancia al riesgo
CASE NEW.risk_tolerance
WHEN 'conservative' THEN v_tolerance_weight := -2;
WHEN 'moderate' THEN v_tolerance_weight := 0;
WHEN 'aggressive' THEN v_tolerance_weight := 2;
WHEN 'speculative' THEN v_tolerance_weight := 3;
ELSE v_tolerance_weight := 0;
END CASE;
-- Calcular score final
v_score := v_score + v_experience_weight + v_tolerance_weight;
-- Asegurar rango 1-10
v_score := GREATEST(1, LEAST(10, v_score));
NEW.calculated_risk_score := v_score;
NEW.last_risk_assessment := NOW();
-- Guardar factores del calculo
NEW.risk_score_factors := jsonb_build_object(
'base_score', 5,
'experience_weight', v_experience_weight,
'tolerance_weight', v_tolerance_weight,
'final_score', v_score
);
-- Ajustar limites basados en score
IF v_score <= 3 THEN
NEW.max_leverage := 5;
NEW.max_position_size_percent := 2.00;
NEW.max_daily_loss_percent := 1.00;
ELSIF v_score <= 5 THEN
NEW.max_leverage := 10;
NEW.max_position_size_percent := 5.00;
NEW.max_daily_loss_percent := 3.00;
ELSIF v_score <= 7 THEN
NEW.max_leverage := 20;
NEW.max_position_size_percent := 10.00;
NEW.max_daily_loss_percent := 5.00;
ELSE
NEW.max_leverage := 50;
NEW.max_position_size_percent := 15.00;
NEW.max_daily_loss_percent := 10.00;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS risk_score_calc ON users.risk_profiles;
CREATE TRIGGER risk_score_calc
BEFORE INSERT OR UPDATE OF risk_tolerance, trading_experience ON users.risk_profiles
FOR EACH ROW
EXECUTE FUNCTION users.calculate_risk_score();
-- RLS Policy para multi-tenancy
ALTER TABLE users.risk_profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY risk_profiles_tenant_isolation ON users.risk_profiles
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- Grants
GRANT SELECT, INSERT, UPDATE ON users.risk_profiles TO trading_app;
GRANT SELECT ON users.risk_profiles TO trading_readonly;