## 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>
242 lines
8.4 KiB
PL/PgSQL
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;
|