-- ============================================================================ -- 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;