From 4631a58b42adedd26dc69d83f0b926c82cf7e631 Mon Sep 17 00:00:00 2001 From: rckrdmrd Date: Fri, 16 Jan 2026 20:19:27 -0600 Subject: [PATCH] [DDL] feat: Sprint 6 - Add broker_integration and portfolio_management schemas broker_integration (5 tables): - broker_accounts: Cuentas MT4/MT5/API conectadas - broker_prices: Precios en tiempo real - spread_statistics: Estadisticas historicas de spread - price_adjustment_model: Modelos de ajuste de precio - trade_execution: Ejecucion de ordenes portfolio_management (6 tables): - portfolios: Portafolios de inversion - portfolio_accounts: Cuentas vinculadas a portafolios - investment_goals: Metas de inversion - rebalance_suggestions: Sugerencias de rebalanceo - portfolio_snapshots: Snapshots historicos - monte_carlo_projections: Proyecciones Monte Carlo Co-Authored-By: Claude Opus 4.5 --- .../tables/001_broker_accounts.sql | 255 ++++++++++++++ .../tables/002_broker_prices.sql | 96 +++++ .../tables/003_spread_statistics.sql | 259 ++++++++++++++ .../tables/004_trade_execution.sql | 258 ++++++++++++++ .../tables/001_portfolios.sql | 310 ++++++++++++++++ .../tables/002_investment_goals.sql | 333 ++++++++++++++++++ .../tables/003_portfolio_snapshots.sql | 282 +++++++++++++++ 7 files changed, 1793 insertions(+) create mode 100644 ddl/schemas/broker_integration/tables/001_broker_accounts.sql create mode 100644 ddl/schemas/broker_integration/tables/002_broker_prices.sql create mode 100644 ddl/schemas/broker_integration/tables/003_spread_statistics.sql create mode 100644 ddl/schemas/broker_integration/tables/004_trade_execution.sql create mode 100644 ddl/schemas/portfolio_management/tables/001_portfolios.sql create mode 100644 ddl/schemas/portfolio_management/tables/002_investment_goals.sql create mode 100644 ddl/schemas/portfolio_management/tables/003_portfolio_snapshots.sql diff --git a/ddl/schemas/broker_integration/tables/001_broker_accounts.sql b/ddl/schemas/broker_integration/tables/001_broker_accounts.sql new file mode 100644 index 0000000..3e0d7f2 --- /dev/null +++ b/ddl/schemas/broker_integration/tables/001_broker_accounts.sql @@ -0,0 +1,255 @@ +-- ============================================================================ +-- SCHEMA: broker_integration +-- TABLE: broker_accounts +-- DESCRIPTION: Cuentas de broker conectadas (MT4, MT5, etc.) +-- VERSION: 1.0.0 +-- CREATED: 2026-01-16 +-- SPRINT: Sprint 6 - DDL Implementation Roadmap Q1-2026 +-- ============================================================================ + +-- Crear schema +CREATE SCHEMA IF NOT EXISTS broker_integration; + +COMMENT ON SCHEMA broker_integration IS +'Integracion con brokers de trading (MT4, MT5, API de brokers)'; + +-- Enum para tipo de broker +DO $$ BEGIN + CREATE TYPE broker_integration.broker_type AS ENUM ( + 'mt4', + 'mt5', + 'ctrader', + 'fix_api', + 'rest_api', + 'binance', + 'interactive_brokers', + 'oanda', + 'alpaca', + 'custom' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Enum para tipo de cuenta +DO $$ BEGIN + CREATE TYPE broker_integration.account_type AS ENUM ( + 'demo', + 'live', + 'contest' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Enum para estado de conexion +DO $$ BEGIN + CREATE TYPE broker_integration.connection_status AS ENUM ( + 'connected', + 'disconnected', + 'connecting', + 'error', + 'maintenance', + 'suspended', + 'expired' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Tabla de Cuentas de Broker +CREATE TABLE IF NOT EXISTS broker_integration.broker_accounts ( + -- Identificadores + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, + + -- Broker info + broker_type broker_integration.broker_type NOT NULL, + broker_name VARCHAR(100) NOT NULL, -- "XM", "IC Markets" + broker_server VARCHAR(255), -- Server name/address + + -- Cuenta + account_type broker_integration.account_type NOT NULL DEFAULT 'demo', + account_number VARCHAR(50) NOT NULL, + account_name VARCHAR(100), + + -- Conexion + connection_status broker_integration.connection_status NOT NULL DEFAULT 'disconnected', + connection_provider VARCHAR(50), -- "metaapi", "direct" + provider_account_id VARCHAR(100), -- ID en el proveedor + + -- Credenciales (encriptadas) + login_encrypted TEXT, + password_encrypted TEXT, + investor_password_encrypted TEXT, + api_token_encrypted TEXT, + additional_credentials JSONB DEFAULT '{}'::JSONB, + + -- Configuracion + leverage INTEGER, + currency VARCHAR(10) NOT NULL DEFAULT 'USD', + timezone VARCHAR(50) DEFAULT 'UTC', + + -- Balance y equity + balance DECIMAL(15, 2), + equity DECIMAL(15, 2), + margin DECIMAL(15, 2), + free_margin DECIMAL(15, 2), + margin_level DECIMAL(10, 2), + + -- Configuracion de trading + enable_trading BOOLEAN NOT NULL DEFAULT FALSE, + max_positions INTEGER, + max_lot_size DECIMAL(10, 2), + min_lot_size DECIMAL(10, 4) DEFAULT 0.01, + lot_step DECIMAL(10, 4) DEFAULT 0.01, + + -- Permisos + allow_copy_trading BOOLEAN NOT NULL DEFAULT FALSE, + allow_signals BOOLEAN NOT NULL DEFAULT TRUE, + allow_auto_trading BOOLEAN NOT NULL DEFAULT FALSE, + + -- Restricciones + allowed_symbols VARCHAR(20)[], -- NULL = todos + blocked_symbols VARCHAR(20)[], + allowed_directions VARCHAR(10)[], -- ['long', 'short'] + trading_hours JSONB, + + -- Estado + is_active BOOLEAN NOT NULL DEFAULT TRUE, + is_default BOOLEAN NOT NULL DEFAULT FALSE, + is_verified BOOLEAN NOT NULL DEFAULT FALSE, + + -- Ultima sincronizacion + last_sync_at TIMESTAMPTZ, + last_sync_error TEXT, + sync_interval_seconds INTEGER DEFAULT 60, + + -- Estadisticas + total_trades INTEGER NOT NULL DEFAULT 0, + open_positions INTEGER NOT NULL DEFAULT 0, + total_profit DECIMAL(15, 2) NOT NULL DEFAULT 0, + today_profit DECIMAL(15, 2) NOT NULL DEFAULT 0, + + -- Metadata + metadata JSONB DEFAULT '{}'::JSONB, + notes TEXT, + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + connected_at TIMESTAMPTZ, + disconnected_at TIMESTAMPTZ, + verified_at TIMESTAMPTZ, + + -- Constraints + CONSTRAINT broker_accounts_unique UNIQUE (tenant_id, broker_type, account_number) +); + +COMMENT ON TABLE broker_integration.broker_accounts IS +'Cuentas de broker conectadas para trading real o demo'; + +COMMENT ON COLUMN broker_integration.broker_accounts.enable_trading IS +'Si TRUE, permite ejecutar ordenes reales en esta cuenta'; + +-- Indices +CREATE INDEX IF NOT EXISTS idx_broker_accounts_tenant + ON broker_integration.broker_accounts(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_broker_accounts_user + ON broker_integration.broker_accounts(user_id); + +CREATE INDEX IF NOT EXISTS idx_broker_accounts_type + ON broker_integration.broker_accounts(broker_type); + +CREATE INDEX IF NOT EXISTS idx_broker_accounts_status + ON broker_integration.broker_accounts(connection_status); + +CREATE INDEX IF NOT EXISTS idx_broker_accounts_active + ON broker_integration.broker_accounts(user_id, is_active, connection_status) + WHERE is_active = TRUE; + +CREATE INDEX IF NOT EXISTS idx_broker_accounts_default + ON broker_integration.broker_accounts(user_id, is_default) + WHERE is_default = TRUE; + +CREATE INDEX IF NOT EXISTS idx_broker_accounts_sync + ON broker_integration.broker_accounts(last_sync_at) + WHERE is_active = TRUE AND connection_status = 'connected'; + +-- Funcion de timestamp +CREATE OR REPLACE FUNCTION broker_integration.update_broker_timestamp() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at := NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Trigger para updated_at +DROP TRIGGER IF EXISTS broker_account_updated_at ON broker_integration.broker_accounts; +CREATE TRIGGER broker_account_updated_at + BEFORE UPDATE ON broker_integration.broker_accounts + FOR EACH ROW + EXECUTE FUNCTION broker_integration.update_broker_timestamp(); + +-- Trigger para asegurar solo una cuenta default +CREATE OR REPLACE FUNCTION broker_integration.ensure_single_default_broker() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.is_default = TRUE THEN + UPDATE broker_integration.broker_accounts + SET is_default = FALSE + WHERE user_id = NEW.user_id + AND id != NEW.id + AND is_default = TRUE; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS broker_account_single_default ON broker_integration.broker_accounts; +CREATE TRIGGER broker_account_single_default + BEFORE INSERT OR UPDATE OF is_default ON broker_integration.broker_accounts + FOR EACH ROW + WHEN (NEW.is_default = TRUE) + EXECUTE FUNCTION broker_integration.ensure_single_default_broker(); + +-- Vista de cuentas activas +CREATE OR REPLACE VIEW broker_integration.v_active_accounts AS +SELECT + id, + user_id, + broker_type, + broker_name, + account_type, + account_number, + connection_status, + balance, + equity, + leverage, + currency, + open_positions, + is_default, + last_sync_at +FROM broker_integration.broker_accounts +WHERE is_active = TRUE +ORDER BY is_default DESC, broker_name; + +-- RLS Policies +ALTER TABLE broker_integration.broker_accounts ENABLE ROW LEVEL SECURITY; + +CREATE POLICY broker_accounts_tenant ON broker_integration.broker_accounts + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY broker_accounts_user ON broker_integration.broker_accounts + FOR ALL + USING (user_id = current_setting('app.current_user_id', true)::UUID); + +-- Grants +GRANT USAGE ON SCHEMA broker_integration TO trading_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON broker_integration.broker_accounts TO trading_app; +GRANT SELECT ON broker_integration.broker_accounts TO trading_readonly; +GRANT SELECT ON broker_integration.v_active_accounts TO trading_app; diff --git a/ddl/schemas/broker_integration/tables/002_broker_prices.sql b/ddl/schemas/broker_integration/tables/002_broker_prices.sql new file mode 100644 index 0000000..7d8f4d4 --- /dev/null +++ b/ddl/schemas/broker_integration/tables/002_broker_prices.sql @@ -0,0 +1,96 @@ +-- ============================================================================ +-- SCHEMA: broker_integration +-- TABLE: broker_prices +-- DESCRIPTION: Precios en tiempo real de brokers +-- VERSION: 1.0.0 +-- CREATED: 2026-01-16 +-- SPRINT: Sprint 6 - DDL Implementation Roadmap Q1-2026 +-- ============================================================================ + +-- Tabla de Precios de Broker +CREATE TABLE IF NOT EXISTS broker_integration.broker_prices ( + -- Identificadores + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + broker_account_id UUID NOT NULL REFERENCES broker_integration.broker_accounts(id) ON DELETE CASCADE, + + -- Simbolo + symbol VARCHAR(20) NOT NULL, + + -- Precios + bid DECIMAL(15, 8) NOT NULL, + ask DECIMAL(15, 8) NOT NULL, + spread DECIMAL(10, 4), -- En pips + spread_points INTEGER, -- En puntos + + -- Volumen + bid_volume DECIMAL(15, 4), + ask_volume DECIMAL(15, 4), + + -- Cambio + change_24h DECIMAL(15, 8), + change_percent_24h DECIMAL(10, 4), + high_24h DECIMAL(15, 8), + low_24h DECIMAL(15, 8), + + -- Sesion + session_open DECIMAL(15, 8), + session_high DECIMAL(15, 8), + session_low DECIMAL(15, 8), + previous_close DECIMAL(15, 8), + + -- Trading + is_trading_enabled BOOLEAN NOT NULL DEFAULT TRUE, + trading_session VARCHAR(20), -- 'pre_market', 'regular', 'after_hours' + + -- Timestamp del broker + broker_timestamp TIMESTAMPTZ, + + -- Timestamps + received_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Constraints + CONSTRAINT broker_prices_unique UNIQUE (broker_account_id, symbol) +); + +COMMENT ON TABLE broker_integration.broker_prices IS +'Precios en tiempo real recibidos de brokers conectados'; + +-- Indices +CREATE INDEX IF NOT EXISTS idx_broker_prices_account + ON broker_integration.broker_prices(broker_account_id); + +CREATE INDEX IF NOT EXISTS idx_broker_prices_symbol + ON broker_integration.broker_prices(symbol); + +CREATE INDEX IF NOT EXISTS idx_broker_prices_received + ON broker_integration.broker_prices(received_at DESC); + +-- Vista de spreads actuales +CREATE OR REPLACE VIEW broker_integration.v_current_spreads AS +SELECT + bp.symbol, + ba.broker_name, + bp.bid, + bp.ask, + bp.spread, + bp.spread_points, + bp.is_trading_enabled, + bp.received_at +FROM broker_integration.broker_prices bp +JOIN broker_integration.broker_accounts ba ON bp.broker_account_id = ba.id +WHERE ba.is_active = TRUE +ORDER BY bp.symbol, bp.spread; + +-- RLS +ALTER TABLE broker_integration.broker_prices ENABLE ROW LEVEL SECURITY; + +CREATE POLICY broker_prices_tenant ON broker_integration.broker_prices + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- Grants +GRANT SELECT, INSERT, UPDATE, DELETE ON broker_integration.broker_prices TO trading_app; +GRANT SELECT ON broker_integration.broker_prices TO trading_readonly; +GRANT SELECT ON broker_integration.v_current_spreads TO trading_app; diff --git a/ddl/schemas/broker_integration/tables/003_spread_statistics.sql b/ddl/schemas/broker_integration/tables/003_spread_statistics.sql new file mode 100644 index 0000000..678367b --- /dev/null +++ b/ddl/schemas/broker_integration/tables/003_spread_statistics.sql @@ -0,0 +1,259 @@ +-- ============================================================================ +-- SCHEMA: broker_integration +-- TABLE: spread_statistics, price_adjustment_model +-- DESCRIPTION: Estadisticas de spread y modelos de ajuste de precio +-- VERSION: 1.0.0 +-- CREATED: 2026-01-16 +-- SPRINT: Sprint 6 - DDL Implementation Roadmap Q1-2026 +-- ============================================================================ + +-- Tabla de Estadisticas de Spread +CREATE TABLE IF NOT EXISTS broker_integration.spread_statistics ( + -- Identificadores + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + broker_account_id UUID REFERENCES broker_integration.broker_accounts(id) ON DELETE CASCADE, + + -- Simbolo y periodo + symbol VARCHAR(20) NOT NULL, + period_start TIMESTAMPTZ NOT NULL, + period_end TIMESTAMPTZ NOT NULL, + period_type VARCHAR(20) NOT NULL, -- 'hourly', 'daily', 'session' + + -- Estadisticas de spread + spread_min DECIMAL(10, 4), + spread_max DECIMAL(10, 4), + spread_avg DECIMAL(10, 4), + spread_median DECIMAL(10, 4), + spread_stddev DECIMAL(10, 4), + spread_percentile_95 DECIMAL(10, 4), + + -- Spread por sesion + spread_asia_avg DECIMAL(10, 4), + spread_london_avg DECIMAL(10, 4), + spread_ny_avg DECIMAL(10, 4), + + -- Frecuencia de widening + widening_events INTEGER NOT NULL DEFAULT 0, + max_widening_factor DECIMAL(10, 2), + + -- Samples + sample_count INTEGER NOT NULL DEFAULT 0, + trading_hours_covered INTEGER, -- Horas de trading cubiertas + + -- Comparacion historica + spread_vs_7d_avg DECIMAL(10, 4), -- % vs promedio 7 dias + spread_vs_30d_avg DECIMAL(10, 4), -- % vs promedio 30 dias + + -- Metadata + metadata JSONB DEFAULT '{}'::JSONB, + + -- Timestamps + calculated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Constraints + CONSTRAINT spread_stats_unique UNIQUE (broker_account_id, symbol, period_start, period_type) +); + +COMMENT ON TABLE broker_integration.spread_statistics IS +'Estadisticas historicas de spread por simbolo, broker y periodo'; + +-- Indices +CREATE INDEX IF NOT EXISTS idx_spread_stats_account + ON broker_integration.spread_statistics(broker_account_id); + +CREATE INDEX IF NOT EXISTS idx_spread_stats_symbol + ON broker_integration.spread_statistics(symbol, period_start DESC); + +CREATE INDEX IF NOT EXISTS idx_spread_stats_period + ON broker_integration.spread_statistics(period_type, period_start DESC); + +-- ============================================================================ +-- TABLE: price_adjustment_model +-- ============================================================================ + +-- Tabla de Modelos de Ajuste de Precio +CREATE TABLE IF NOT EXISTS broker_integration.price_adjustment_model ( + -- Identificadores + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL, + + -- Simbolo + symbol VARCHAR(20) NOT NULL, + + -- Tipo de modelo + model_type VARCHAR(50) NOT NULL DEFAULT 'spread_based', + + -- Parametros del modelo + base_spread_pips DECIMAL(10, 4) NOT NULL DEFAULT 1, + slippage_avg_pips DECIMAL(10, 4) NOT NULL DEFAULT 0.2, + slippage_max_pips DECIMAL(10, 4) NOT NULL DEFAULT 2, + + -- Ajustes por sesion + asia_adjustment DECIMAL(5, 4) DEFAULT 1.2, -- Multiplicador + london_adjustment DECIMAL(5, 4) DEFAULT 1.0, + ny_adjustment DECIMAL(5, 4) DEFAULT 1.0, + overlap_adjustment DECIMAL(5, 4) DEFAULT 0.9, -- London-NY overlap + + -- Ajustes por volatilidad + low_volatility_adjustment DECIMAL(5, 4) DEFAULT 0.8, + high_volatility_adjustment DECIMAL(5, 4) DEFAULT 1.5, + extreme_volatility_adjustment DECIMAL(5, 4) DEFAULT 2.5, + + -- Ajustes por horario + market_open_adjustment DECIMAL(5, 4) DEFAULT 1.5, + market_close_adjustment DECIMAL(5, 4) DEFAULT 1.3, + news_event_adjustment DECIMAL(5, 4) DEFAULT 2.0, + + -- Limites + max_total_adjustment DECIMAL(5, 2) DEFAULT 5.0, -- Max pips de ajuste total + min_acceptable_spread DECIMAL(10, 4), + max_acceptable_spread DECIMAL(10, 4), + + -- Estado + is_active BOOLEAN NOT NULL DEFAULT TRUE, + + -- Validez + valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(), + valid_until TIMESTAMPTZ, + + -- Performance + accuracy_score DECIMAL(5, 2), -- 0-100 + last_accuracy_check TIMESTAMPTZ, + predictions_count INTEGER NOT NULL DEFAULT 0, + accurate_predictions INTEGER NOT NULL DEFAULT 0, + + -- Metadata + training_data_range JSONB, -- {"start": "...", "end": "..."} + metadata JSONB DEFAULT '{}'::JSONB, + notes TEXT, + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Constraints + CONSTRAINT price_adj_model_unique UNIQUE (tenant_id, symbol, model_type) +); + +COMMENT ON TABLE broker_integration.price_adjustment_model IS +'Modelos para ajustar precios de entrada considerando spread y slippage'; + +-- Indices +CREATE INDEX IF NOT EXISTS idx_price_adj_symbol + ON broker_integration.price_adjustment_model(symbol); + +CREATE INDEX IF NOT EXISTS idx_price_adj_active + ON broker_integration.price_adjustment_model(symbol, is_active) + WHERE is_active = TRUE; + +-- Funcion para calcular ajuste de precio +CREATE OR REPLACE FUNCTION broker_integration.calculate_price_adjustment( + p_symbol VARCHAR, + p_direction VARCHAR, + p_current_price DECIMAL, + p_session VARCHAR DEFAULT NULL, + p_volatility VARCHAR DEFAULT 'normal' +) +RETURNS TABLE ( + adjusted_entry DECIMAL, + adjusted_sl DECIMAL, + adjusted_tp DECIMAL, + total_adjustment_pips DECIMAL, + adjustment_breakdown JSONB +) AS $$ +DECLARE + v_model broker_integration.price_adjustment_model; + v_adjustment DECIMAL; + v_session_mult DECIMAL; + v_volatility_mult DECIMAL; +BEGIN + SELECT * INTO v_model + FROM broker_integration.price_adjustment_model + WHERE symbol = p_symbol + AND is_active = TRUE + AND (valid_until IS NULL OR valid_until > NOW()) + ORDER BY valid_from DESC + LIMIT 1; + + IF NOT FOUND THEN + -- Retornar sin ajuste + RETURN QUERY SELECT + p_current_price, + p_current_price, + p_current_price, + 0::DECIMAL, + '{}'::JSONB; + RETURN; + END IF; + + -- Determinar multiplicador de sesion + v_session_mult := CASE p_session + WHEN 'asia' THEN v_model.asia_adjustment + WHEN 'london' THEN v_model.london_adjustment + WHEN 'ny' THEN v_model.ny_adjustment + WHEN 'overlap' THEN v_model.overlap_adjustment + ELSE 1.0 + END; + + -- Determinar multiplicador de volatilidad + v_volatility_mult := CASE p_volatility + WHEN 'low' THEN v_model.low_volatility_adjustment + WHEN 'high' THEN v_model.high_volatility_adjustment + WHEN 'extreme' THEN v_model.extreme_volatility_adjustment + ELSE 1.0 + END; + + -- Calcular ajuste total en pips + v_adjustment := (v_model.base_spread_pips + v_model.slippage_avg_pips) + * v_session_mult * v_volatility_mult; + + -- Limitar ajuste + v_adjustment := LEAST(v_adjustment, v_model.max_total_adjustment); + + -- Convertir a precio + v_adjustment := v_adjustment * 0.0001; -- 1 pip = 0.0001 + + RETURN QUERY SELECT + CASE p_direction + WHEN 'long' THEN p_current_price + v_adjustment + ELSE p_current_price - v_adjustment + END, + p_current_price, -- SL sin ajustar + p_current_price, -- TP sin ajustar + v_adjustment * 10000, -- En pips + jsonb_build_object( + 'base_spread', v_model.base_spread_pips, + 'slippage', v_model.slippage_avg_pips, + 'session_multiplier', v_session_mult, + 'volatility_multiplier', v_volatility_mult + ); +END; +$$ LANGUAGE plpgsql; + +-- Trigger para updated_at +DROP TRIGGER IF EXISTS price_adj_updated_at ON broker_integration.price_adjustment_model; +CREATE TRIGGER price_adj_updated_at + BEFORE UPDATE ON broker_integration.price_adjustment_model + FOR EACH ROW + EXECUTE FUNCTION broker_integration.update_broker_timestamp(); + +-- RLS +ALTER TABLE broker_integration.spread_statistics ENABLE ROW LEVEL SECURITY; +ALTER TABLE broker_integration.price_adjustment_model ENABLE ROW LEVEL SECURITY; + +CREATE POLICY spread_stats_tenant ON broker_integration.spread_statistics + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY price_adj_tenant ON broker_integration.price_adjustment_model + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +-- Grants +GRANT SELECT, INSERT, UPDATE, DELETE ON broker_integration.spread_statistics TO trading_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON broker_integration.price_adjustment_model TO trading_app; +GRANT SELECT ON broker_integration.spread_statistics TO trading_readonly; +GRANT SELECT ON broker_integration.price_adjustment_model TO trading_readonly; +GRANT EXECUTE ON FUNCTION broker_integration.calculate_price_adjustment TO trading_app; diff --git a/ddl/schemas/broker_integration/tables/004_trade_execution.sql b/ddl/schemas/broker_integration/tables/004_trade_execution.sql new file mode 100644 index 0000000..f08396e --- /dev/null +++ b/ddl/schemas/broker_integration/tables/004_trade_execution.sql @@ -0,0 +1,258 @@ +-- ============================================================================ +-- SCHEMA: broker_integration +-- TABLE: trade_execution +-- DESCRIPTION: Ejecucion de ordenes en broker +-- VERSION: 1.0.0 +-- CREATED: 2026-01-16 +-- SPRINT: Sprint 6 - DDL Implementation Roadmap Q1-2026 +-- ============================================================================ + +-- Enum para estado de ejecucion +DO $$ BEGIN + CREATE TYPE broker_integration.execution_status AS ENUM ( + 'pending', -- Pendiente de enviar + 'submitted', -- Enviada al broker + 'accepted', -- Aceptada por broker + 'filled', -- Ejecutada completamente + 'partial_fill', -- Ejecutada parcialmente + 'rejected', -- Rechazada + 'cancelled', -- Cancelada + 'expired', -- Expirada + 'error' -- Error de sistema + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Enum para tipo de orden +DO $$ BEGIN + CREATE TYPE broker_integration.order_type AS ENUM ( + 'market', + 'limit', + 'stop', + 'stop_limit', + 'trailing_stop' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Enum para direccion +DO $$ BEGIN + CREATE TYPE broker_integration.trade_direction AS ENUM ( + 'buy', + 'sell' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Tabla de Ejecucion de Trades +CREATE TABLE IF NOT EXISTS broker_integration.trade_execution ( + -- Identificadores + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, + + -- Referencias + broker_account_id UUID NOT NULL REFERENCES broker_integration.broker_accounts(id) ON DELETE CASCADE, + signal_id UUID, -- Senal que origino + position_id UUID, -- Posicion resultante + + -- Orden + order_type broker_integration.order_type NOT NULL, + direction broker_integration.trade_direction NOT NULL, + symbol VARCHAR(20) NOT NULL, + + -- TamaƱo + lot_size DECIMAL(10, 4) NOT NULL, + units INTEGER, + + -- Precios solicitados + requested_price DECIMAL(15, 8), + limit_price DECIMAL(15, 8), + stop_price DECIMAL(15, 8), + + -- Stop Loss / Take Profit + stop_loss DECIMAL(15, 8), + take_profit DECIMAL(15, 8), + + -- Estado + status broker_integration.execution_status NOT NULL DEFAULT 'pending', + status_message TEXT, + + -- Ejecucion + executed_price DECIMAL(15, 8), + executed_lot_size DECIMAL(10, 4), + slippage DECIMAL(10, 4), -- En pips + slippage_cost DECIMAL(15, 4), -- En moneda de cuenta + + -- IDs del broker + broker_order_id VARCHAR(100), + broker_ticket VARCHAR(100), + broker_deal_id VARCHAR(100), + + -- Timestamps de ejecucion + submitted_at TIMESTAMPTZ, + accepted_at TIMESTAMPTZ, + executed_at TIMESTAMPTZ, + cancelled_at TIMESTAMPTZ, + + -- Latencia + submission_latency_ms INTEGER, -- Tiempo hasta submit + execution_latency_ms INTEGER, -- Tiempo hasta fill + total_latency_ms INTEGER, + + -- Costos + commission DECIMAL(15, 4) DEFAULT 0, + swap DECIMAL(15, 4) DEFAULT 0, + spread_cost DECIMAL(15, 4) DEFAULT 0, + + -- Contexto + execution_source VARCHAR(50), -- 'manual', 'signal', 'bot', 'copy_trade' + execution_context JSONB DEFAULT '{}'::JSONB, + + -- Errores + error_code VARCHAR(50), + error_message TEXT, + retry_count INTEGER NOT NULL DEFAULT 0, + max_retries INTEGER DEFAULT 3, + + -- Validez + valid_until TIMESTAMPTZ, + time_in_force VARCHAR(20) DEFAULT 'GTC', -- 'GTC', 'IOC', 'FOK', 'GTD' + + -- Metadata + metadata JSONB DEFAULT '{}'::JSONB, + notes TEXT, + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +COMMENT ON TABLE broker_integration.trade_execution IS +'Registro de todas las ejecuciones de ordenes en brokers conectados'; + +COMMENT ON COLUMN broker_integration.trade_execution.slippage IS +'Diferencia entre precio solicitado y ejecutado en pips'; + +-- Indices +CREATE INDEX IF NOT EXISTS idx_trade_exec_tenant + ON broker_integration.trade_execution(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_trade_exec_user + ON broker_integration.trade_execution(user_id); + +CREATE INDEX IF NOT EXISTS idx_trade_exec_account + ON broker_integration.trade_execution(broker_account_id); + +CREATE INDEX IF NOT EXISTS idx_trade_exec_status + ON broker_integration.trade_execution(status); + +CREATE INDEX IF NOT EXISTS idx_trade_exec_pending + ON broker_integration.trade_execution(status, created_at) + WHERE status IN ('pending', 'submitted'); + +CREATE INDEX IF NOT EXISTS idx_trade_exec_symbol + ON broker_integration.trade_execution(symbol, executed_at DESC); + +CREATE INDEX IF NOT EXISTS idx_trade_exec_broker_order + ON broker_integration.trade_execution(broker_order_id) + WHERE broker_order_id IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_trade_exec_signal + ON broker_integration.trade_execution(signal_id) + WHERE signal_id IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_trade_exec_created + ON broker_integration.trade_execution(created_at DESC); + +-- Trigger para updated_at +DROP TRIGGER IF EXISTS trade_exec_updated_at ON broker_integration.trade_execution; +CREATE TRIGGER trade_exec_updated_at + BEFORE UPDATE ON broker_integration.trade_execution + FOR EACH ROW + EXECUTE FUNCTION broker_integration.update_broker_timestamp(); + +-- Trigger para calcular latencias +CREATE OR REPLACE FUNCTION broker_integration.calculate_execution_latency() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.submitted_at IS NOT NULL AND NEW.submission_latency_ms IS NULL THEN + NEW.submission_latency_ms := EXTRACT(EPOCH FROM (NEW.submitted_at - NEW.created_at)) * 1000; + END IF; + + IF NEW.executed_at IS NOT NULL AND NEW.submitted_at IS NOT NULL AND NEW.execution_latency_ms IS NULL THEN + NEW.execution_latency_ms := EXTRACT(EPOCH FROM (NEW.executed_at - NEW.submitted_at)) * 1000; + END IF; + + IF NEW.executed_at IS NOT NULL AND NEW.total_latency_ms IS NULL THEN + NEW.total_latency_ms := EXTRACT(EPOCH FROM (NEW.executed_at - NEW.created_at)) * 1000; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS trade_exec_latency ON broker_integration.trade_execution; +CREATE TRIGGER trade_exec_latency + BEFORE UPDATE OF submitted_at, executed_at ON broker_integration.trade_execution + FOR EACH ROW + EXECUTE FUNCTION broker_integration.calculate_execution_latency(); + +-- Vista de ejecuciones recientes +CREATE OR REPLACE VIEW broker_integration.v_recent_executions AS +SELECT + te.id, + te.user_id, + ba.broker_name, + te.symbol, + te.direction, + te.order_type, + te.lot_size, + te.status, + te.requested_price, + te.executed_price, + te.slippage, + te.total_latency_ms, + te.created_at, + te.executed_at +FROM broker_integration.trade_execution te +JOIN broker_integration.broker_accounts ba ON te.broker_account_id = ba.id +ORDER BY te.created_at DESC; + +-- Vista de estadisticas de ejecucion +CREATE OR REPLACE VIEW broker_integration.v_execution_stats AS +SELECT + ba.broker_name, + te.symbol, + COUNT(*) AS total_executions, + COUNT(*) FILTER (WHERE te.status = 'filled') AS filled, + COUNT(*) FILTER (WHERE te.status = 'rejected') AS rejected, + ROUND((COUNT(*) FILTER (WHERE te.status = 'filled')::DECIMAL + / NULLIF(COUNT(*), 0) * 100), 2) AS fill_rate, + ROUND(AVG(te.slippage) FILTER (WHERE te.slippage IS NOT NULL)::NUMERIC, 2) AS avg_slippage, + ROUND(AVG(te.total_latency_ms) FILTER (WHERE te.total_latency_ms IS NOT NULL)::NUMERIC, 0) AS avg_latency_ms +FROM broker_integration.trade_execution te +JOIN broker_integration.broker_accounts ba ON te.broker_account_id = ba.id +WHERE te.created_at >= NOW() - INTERVAL '30 days' +GROUP BY ba.broker_name, te.symbol +ORDER BY total_executions DESC; + +-- RLS +ALTER TABLE broker_integration.trade_execution ENABLE ROW LEVEL SECURITY; + +CREATE POLICY trade_exec_tenant ON broker_integration.trade_execution + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY trade_exec_user ON broker_integration.trade_execution + FOR ALL + USING (user_id = current_setting('app.current_user_id', true)::UUID); + +-- Grants +GRANT SELECT, INSERT, UPDATE ON broker_integration.trade_execution TO trading_app; +GRANT SELECT ON broker_integration.trade_execution TO trading_readonly; +GRANT SELECT ON broker_integration.v_recent_executions TO trading_app; +GRANT SELECT ON broker_integration.v_execution_stats TO trading_app; diff --git a/ddl/schemas/portfolio_management/tables/001_portfolios.sql b/ddl/schemas/portfolio_management/tables/001_portfolios.sql new file mode 100644 index 0000000..cc87e57 --- /dev/null +++ b/ddl/schemas/portfolio_management/tables/001_portfolios.sql @@ -0,0 +1,310 @@ +-- ============================================================================ +-- SCHEMA: portfolio_management +-- TABLE: portfolios, portfolio_accounts +-- DESCRIPTION: Gestion de portafolios de inversion +-- VERSION: 1.0.0 +-- CREATED: 2026-01-16 +-- SPRINT: Sprint 6 - DDL Implementation Roadmap Q1-2026 +-- ============================================================================ + +-- Crear schema +CREATE SCHEMA IF NOT EXISTS portfolio_management; + +COMMENT ON SCHEMA portfolio_management IS +'Gestion de portafolios, metas de inversion y proyecciones'; + +-- Enum para tipo de portafolio +DO $$ BEGIN + CREATE TYPE portfolio_management.portfolio_type AS ENUM ( + 'investment', -- Inversion a largo plazo + 'trading', -- Trading activo + 'retirement', -- Retiro + 'education', -- Fondo educativo + 'emergency', -- Fondo de emergencia + 'custom' -- Personalizado + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Enum para perfil de riesgo +DO $$ BEGIN + CREATE TYPE portfolio_management.risk_profile AS ENUM ( + 'conservative', + 'moderately_conservative', + 'moderate', + 'moderately_aggressive', + 'aggressive' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Enum para estrategia de rebalanceo +DO $$ BEGIN + CREATE TYPE portfolio_management.rebalance_strategy AS ENUM ( + 'threshold', -- Por umbral de desviacion + 'calendar', -- Por calendario + 'tactical', -- Tactico/oportunista + 'never', -- Sin rebalanceo + 'custom' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Tabla de Portafolios +CREATE TABLE IF NOT EXISTS portfolio_management.portfolios ( + -- Identificadores + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, + + -- Informacion basica + name VARCHAR(100) NOT NULL, + description TEXT, + type portfolio_management.portfolio_type NOT NULL DEFAULT 'investment', + risk_profile portfolio_management.risk_profile NOT NULL DEFAULT 'moderate', + + -- Valor + total_value DECIMAL(15, 2) NOT NULL DEFAULT 0, + cash_value DECIMAL(15, 2) NOT NULL DEFAULT 0, + invested_value DECIMAL(15, 2) NOT NULL DEFAULT 0, + currency VARCHAR(10) NOT NULL DEFAULT 'USD', + + -- Alocacion objetivo + target_allocation JSONB DEFAULT '{}'::JSONB, -- {"stocks": 60, "bonds": 30, "cash": 10} + current_allocation JSONB DEFAULT '{}'::JSONB, + allocation_drift DECIMAL(5, 2), -- % de desviacion + + -- Rebalanceo + rebalance_strategy portfolio_management.rebalance_strategy NOT NULL DEFAULT 'threshold', + rebalance_threshold DECIMAL(5, 2) DEFAULT 5, -- % de desviacion para trigger + rebalance_frequency VARCHAR(20), -- 'monthly', 'quarterly', 'annually' + last_rebalance_at TIMESTAMPTZ, + next_rebalance_at TIMESTAMPTZ, + + -- Performance + total_return DECIMAL(15, 4) NOT NULL DEFAULT 0, + total_return_percent DECIMAL(10, 4) NOT NULL DEFAULT 0, + ytd_return DECIMAL(15, 4) NOT NULL DEFAULT 0, + ytd_return_percent DECIMAL(10, 4) NOT NULL DEFAULT 0, + inception_date DATE, + + -- Metricas de riesgo + volatility_annual DECIMAL(10, 4), + sharpe_ratio DECIMAL(10, 4), + sortino_ratio DECIMAL(10, 4), + max_drawdown DECIMAL(10, 4), + max_drawdown_date DATE, + beta DECIMAL(10, 4), + alpha DECIMAL(10, 4), + + -- Benchmark + benchmark_symbol VARCHAR(20) DEFAULT 'SPY', + benchmark_return DECIMAL(10, 4), + active_return DECIMAL(10, 4), -- Alpha vs benchmark + + -- Dividendos + total_dividends DECIMAL(15, 4) NOT NULL DEFAULT 0, + dividend_yield DECIMAL(10, 4), + reinvest_dividends BOOLEAN NOT NULL DEFAULT TRUE, + + -- Meta + goal_id UUID, -- FK a investment_goals + goal_progress_percent DECIMAL(5, 2), + + -- Estado + is_active BOOLEAN NOT NULL DEFAULT TRUE, + is_default BOOLEAN NOT NULL DEFAULT FALSE, + is_archived BOOLEAN NOT NULL DEFAULT FALSE, + + -- Configuracion + show_in_dashboard BOOLEAN NOT NULL DEFAULT TRUE, + notify_on_rebalance BOOLEAN NOT NULL DEFAULT TRUE, + auto_invest_enabled BOOLEAN NOT NULL DEFAULT FALSE, + auto_invest_amount DECIMAL(15, 2), + auto_invest_frequency VARCHAR(20), + + -- Metadata + tags VARCHAR(50)[], + metadata JSONB DEFAULT '{}'::JSONB, + notes TEXT, + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + archived_at TIMESTAMPTZ, + + -- Constraints + CONSTRAINT portfolios_name_unique UNIQUE (user_id, name) +); + +COMMENT ON TABLE portfolio_management.portfolios IS +'Portafolios de inversion con tracking de performance y rebalanceo'; + +-- ============================================================================ +-- TABLE: portfolio_accounts +-- ============================================================================ + +-- Tabla de Cuentas en Portafolio +CREATE TABLE IF NOT EXISTS portfolio_management.portfolio_accounts ( + -- Identificadores + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + portfolio_id UUID NOT NULL REFERENCES portfolio_management.portfolios(id) ON DELETE CASCADE, + + -- Cuenta vinculada + account_type VARCHAR(50) NOT NULL, -- 'broker', 'investment', 'wallet' + account_id UUID NOT NULL, -- ID de la cuenta + account_name VARCHAR(100), + + -- Valor en este portafolio + value DECIMAL(15, 2) NOT NULL DEFAULT 0, + allocation_percent DECIMAL(5, 2), -- % del portafolio + + -- Posiciones + positions_count INTEGER NOT NULL DEFAULT 0, + positions_value DECIMAL(15, 2) NOT NULL DEFAULT 0, + + -- Estado + is_active BOOLEAN NOT NULL DEFAULT TRUE, + sync_enabled BOOLEAN NOT NULL DEFAULT TRUE, + last_sync_at TIMESTAMPTZ, + + -- Metadata + metadata JSONB DEFAULT '{}'::JSONB, + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Constraints + CONSTRAINT portfolio_accounts_unique UNIQUE (portfolio_id, account_type, account_id) +); + +COMMENT ON TABLE portfolio_management.portfolio_accounts IS +'Cuentas (broker, investment, wallet) vinculadas a un portafolio'; + +-- Indices para portfolios +CREATE INDEX IF NOT EXISTS idx_portfolios_tenant + ON portfolio_management.portfolios(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_portfolios_user + ON portfolio_management.portfolios(user_id); + +CREATE INDEX IF NOT EXISTS idx_portfolios_type + ON portfolio_management.portfolios(type); + +CREATE INDEX IF NOT EXISTS idx_portfolios_active + ON portfolio_management.portfolios(user_id, is_active) + WHERE is_active = TRUE; + +CREATE INDEX IF NOT EXISTS idx_portfolios_default + ON portfolio_management.portfolios(user_id, is_default) + WHERE is_default = TRUE; + +CREATE INDEX IF NOT EXISTS idx_portfolios_rebalance + ON portfolio_management.portfolios(next_rebalance_at) + WHERE is_active = TRUE AND next_rebalance_at IS NOT NULL; + +-- Indices para portfolio_accounts +CREATE INDEX IF NOT EXISTS idx_portfolio_accounts_portfolio + ON portfolio_management.portfolio_accounts(portfolio_id); + +CREATE INDEX IF NOT EXISTS idx_portfolio_accounts_account + ON portfolio_management.portfolio_accounts(account_type, account_id); + +-- Funcion de timestamp +CREATE OR REPLACE FUNCTION portfolio_management.update_portfolio_timestamp() +RETURNS TRIGGER AS $$ +BEGIN + NEW.updated_at := NOW(); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +-- Triggers +DROP TRIGGER IF EXISTS portfolio_updated_at ON portfolio_management.portfolios; +CREATE TRIGGER portfolio_updated_at + BEFORE UPDATE ON portfolio_management.portfolios + FOR EACH ROW + EXECUTE FUNCTION portfolio_management.update_portfolio_timestamp(); + +DROP TRIGGER IF EXISTS portfolio_account_updated_at ON portfolio_management.portfolio_accounts; +CREATE TRIGGER portfolio_account_updated_at + BEFORE UPDATE ON portfolio_management.portfolio_accounts + FOR EACH ROW + EXECUTE FUNCTION portfolio_management.update_portfolio_timestamp(); + +-- Trigger para asegurar solo un portafolio default +CREATE OR REPLACE FUNCTION portfolio_management.ensure_single_default_portfolio() +RETURNS TRIGGER AS $$ +BEGIN + IF NEW.is_default = TRUE THEN + UPDATE portfolio_management.portfolios + SET is_default = FALSE + WHERE user_id = NEW.user_id + AND id != NEW.id + AND is_default = TRUE; + END IF; + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS portfolio_single_default ON portfolio_management.portfolios; +CREATE TRIGGER portfolio_single_default + BEFORE INSERT OR UPDATE OF is_default ON portfolio_management.portfolios + FOR EACH ROW + WHEN (NEW.is_default = TRUE) + EXECUTE FUNCTION portfolio_management.ensure_single_default_portfolio(); + +-- Vista de portafolios con resumen +CREATE OR REPLACE VIEW portfolio_management.v_portfolio_summary AS +SELECT + p.id, + p.user_id, + p.name, + p.type, + p.risk_profile, + p.total_value, + p.total_return_percent, + p.ytd_return_percent, + p.sharpe_ratio, + p.max_drawdown, + p.allocation_drift, + p.is_default, + (SELECT COUNT(*) FROM portfolio_management.portfolio_accounts pa WHERE pa.portfolio_id = p.id) AS accounts_count, + p.last_rebalance_at, + p.created_at +FROM portfolio_management.portfolios p +WHERE p.is_active = TRUE +ORDER BY p.is_default DESC, p.total_value DESC; + +-- RLS +ALTER TABLE portfolio_management.portfolios ENABLE ROW LEVEL SECURITY; +ALTER TABLE portfolio_management.portfolio_accounts ENABLE ROW LEVEL SECURITY; + +CREATE POLICY portfolios_tenant ON portfolio_management.portfolios + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY portfolios_user ON portfolio_management.portfolios + FOR ALL + USING (user_id = current_setting('app.current_user_id', true)::UUID); + +CREATE POLICY portfolio_accounts_via_portfolio ON portfolio_management.portfolio_accounts + FOR ALL + USING ( + portfolio_id IN ( + SELECT id FROM portfolio_management.portfolios + WHERE user_id = current_setting('app.current_user_id', true)::UUID + ) + ); + +-- Grants +GRANT USAGE ON SCHEMA portfolio_management TO trading_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON portfolio_management.portfolios TO trading_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON portfolio_management.portfolio_accounts TO trading_app; +GRANT SELECT ON portfolio_management.portfolios TO trading_readonly; +GRANT SELECT ON portfolio_management.portfolio_accounts TO trading_readonly; +GRANT SELECT ON portfolio_management.v_portfolio_summary TO trading_app; diff --git a/ddl/schemas/portfolio_management/tables/002_investment_goals.sql b/ddl/schemas/portfolio_management/tables/002_investment_goals.sql new file mode 100644 index 0000000..e5a67c9 --- /dev/null +++ b/ddl/schemas/portfolio_management/tables/002_investment_goals.sql @@ -0,0 +1,333 @@ +-- ============================================================================ +-- SCHEMA: portfolio_management +-- TABLE: investment_goals, rebalance_suggestions +-- DESCRIPTION: Metas de inversion y sugerencias de rebalanceo +-- VERSION: 1.0.0 +-- CREATED: 2026-01-16 +-- SPRINT: Sprint 6 - DDL Implementation Roadmap Q1-2026 +-- ============================================================================ + +-- Enum para tipo de meta +DO $$ BEGIN + CREATE TYPE portfolio_management.goal_type AS ENUM ( + 'retirement', + 'house', + 'education', + 'emergency_fund', + 'vacation', + 'car', + 'wedding', + 'wealth_building', + 'financial_independence', + 'custom' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Enum para estado de meta +DO $$ BEGIN + CREATE TYPE portfolio_management.goal_status AS ENUM ( + 'planning', + 'active', + 'on_track', + 'behind', + 'ahead', + 'achieved', + 'abandoned' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Tabla de Metas de Inversion +CREATE TABLE IF NOT EXISTS portfolio_management.investment_goals ( + -- Identificadores + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, + user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, + portfolio_id UUID REFERENCES portfolio_management.portfolios(id) ON DELETE SET NULL, + + -- Informacion + name VARCHAR(100) NOT NULL, + description TEXT, + type portfolio_management.goal_type NOT NULL, + status portfolio_management.goal_status NOT NULL DEFAULT 'planning', + + -- Meta financiera + target_amount DECIMAL(15, 2) NOT NULL, + current_amount DECIMAL(15, 2) NOT NULL DEFAULT 0, + initial_amount DECIMAL(15, 2) NOT NULL DEFAULT 0, + currency VARCHAR(10) NOT NULL DEFAULT 'USD', + + -- Timeline + start_date DATE NOT NULL DEFAULT CURRENT_DATE, + target_date DATE NOT NULL, + months_remaining INTEGER, + + -- Contribuciones + monthly_contribution DECIMAL(15, 2), + contribution_frequency VARCHAR(20) DEFAULT 'monthly', + total_contributions DECIMAL(15, 2) NOT NULL DEFAULT 0, + next_contribution_date DATE, + + -- Rendimiento + expected_return_percent DECIMAL(5, 2) DEFAULT 7, -- Retorno anual esperado + actual_return_percent DECIMAL(10, 4), + inflation_rate DECIMAL(5, 2) DEFAULT 3, -- Para ajuste real + + -- Progreso + progress_percent DECIMAL(5, 2) NOT NULL DEFAULT 0, + projected_amount DECIMAL(15, 2), -- Monto proyectado al target_date + projected_status portfolio_management.goal_status, + on_track BOOLEAN, + + -- Gap analysis + amount_gap DECIMAL(15, 2), -- Diferencia con target + monthly_gap DECIMAL(15, 2), -- Contribucion adicional necesaria + + -- Alertas + alert_behind_threshold DECIMAL(5, 2) DEFAULT 10, -- % detras para alertar + last_alert_at TIMESTAMPTZ, + + -- Milestones + milestones JSONB DEFAULT '[]'::JSONB, -- [{"percent": 25, "reached_at": "..."}] + + -- Icono y color + icon VARCHAR(50), + color VARCHAR(7), + + -- Metadata + metadata JSONB DEFAULT '{}'::JSONB, + notes TEXT, + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + achieved_at TIMESTAMPTZ, + + -- Constraints + CONSTRAINT goals_name_unique UNIQUE (user_id, name), + CONSTRAINT goals_valid_dates CHECK (target_date > start_date) +); + +COMMENT ON TABLE portfolio_management.investment_goals IS +'Metas de inversion con tracking de progreso y proyecciones'; + +-- ============================================================================ +-- TABLE: rebalance_suggestions +-- ============================================================================ + +-- Enum para tipo de accion +DO $$ BEGIN + CREATE TYPE portfolio_management.rebalance_action AS ENUM ( + 'buy', + 'sell', + 'hold', + 'reduce', + 'increase' + ); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; + +-- Tabla de Sugerencias de Rebalanceo +CREATE TABLE IF NOT EXISTS portfolio_management.rebalance_suggestions ( + -- Identificadores + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + portfolio_id UUID NOT NULL REFERENCES portfolio_management.portfolios(id) ON DELETE CASCADE, + + -- Fecha de sugerencia + suggestion_date DATE NOT NULL DEFAULT CURRENT_DATE, + + -- Asset + asset_class VARCHAR(50) NOT NULL, -- 'stocks', 'bonds', 'crypto' + symbol VARCHAR(20), + asset_name VARCHAR(100), + + -- Alocacion + current_allocation DECIMAL(5, 2) NOT NULL, + target_allocation DECIMAL(5, 2) NOT NULL, + deviation DECIMAL(5, 2) NOT NULL, -- current - target + + -- Accion sugerida + action portfolio_management.rebalance_action NOT NULL, + suggested_amount DECIMAL(15, 2), + suggested_units DECIMAL(15, 4), + + -- Impacto + estimated_cost DECIMAL(15, 4), -- Comisiones, spreads + estimated_tax DECIMAL(15, 4), -- Impuestos estimados + net_amount DECIMAL(15, 2), + + -- Prioridad + priority INTEGER NOT NULL DEFAULT 50, -- 1-100, mayor = mas urgente + is_urgent BOOLEAN NOT NULL DEFAULT FALSE, + + -- Razon + reason TEXT, + analysis JSONB DEFAULT '{}'::JSONB, + + -- Estado + is_applied BOOLEAN NOT NULL DEFAULT FALSE, + applied_at TIMESTAMPTZ, + applied_amount DECIMAL(15, 2), + + -- Expiracion + valid_until TIMESTAMPTZ, + is_expired BOOLEAN NOT NULL DEFAULT FALSE, + + -- Metadata + metadata JSONB DEFAULT '{}'::JSONB, + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +COMMENT ON TABLE portfolio_management.rebalance_suggestions IS +'Sugerencias de rebalanceo generadas para mantener alocacion objetivo'; + +-- Indices para investment_goals +CREATE INDEX IF NOT EXISTS idx_goals_tenant + ON portfolio_management.investment_goals(tenant_id); + +CREATE INDEX IF NOT EXISTS idx_goals_user + ON portfolio_management.investment_goals(user_id); + +CREATE INDEX IF NOT EXISTS idx_goals_portfolio + ON portfolio_management.investment_goals(portfolio_id) + WHERE portfolio_id IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_goals_status + ON portfolio_management.investment_goals(status); + +CREATE INDEX IF NOT EXISTS idx_goals_target_date + ON portfolio_management.investment_goals(target_date) + WHERE status IN ('active', 'on_track', 'behind'); + +-- Indices para rebalance_suggestions +CREATE INDEX IF NOT EXISTS idx_rebalance_portfolio + ON portfolio_management.rebalance_suggestions(portfolio_id, suggestion_date DESC); + +CREATE INDEX IF NOT EXISTS idx_rebalance_pending + ON portfolio_management.rebalance_suggestions(portfolio_id, is_applied, priority DESC) + WHERE is_applied = FALSE AND is_expired = FALSE; + +-- Triggers +DROP TRIGGER IF EXISTS goal_updated_at ON portfolio_management.investment_goals; +CREATE TRIGGER goal_updated_at + BEFORE UPDATE ON portfolio_management.investment_goals + FOR EACH ROW + EXECUTE FUNCTION portfolio_management.update_portfolio_timestamp(); + +-- Trigger para calcular progreso de meta +CREATE OR REPLACE FUNCTION portfolio_management.calculate_goal_progress() +RETURNS TRIGGER AS $$ +BEGIN + -- Calcular progreso + IF NEW.target_amount > 0 THEN + NEW.progress_percent := (NEW.current_amount / NEW.target_amount * 100); + END IF; + + -- Calcular meses restantes + NEW.months_remaining := EXTRACT(MONTH FROM AGE(NEW.target_date, CURRENT_DATE)) + + EXTRACT(YEAR FROM AGE(NEW.target_date, CURRENT_DATE)) * 12; + + -- Calcular gap + NEW.amount_gap := NEW.target_amount - NEW.current_amount; + + -- Determinar estado + IF NEW.current_amount >= NEW.target_amount THEN + NEW.status := 'achieved'; + NEW.on_track := TRUE; + ELSIF NEW.progress_percent >= (100.0 * (CURRENT_DATE - NEW.start_date) + / (NEW.target_date - NEW.start_date)) THEN + NEW.status := 'on_track'; + NEW.on_track := TRUE; + ELSE + NEW.status := 'behind'; + NEW.on_track := FALSE; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +DROP TRIGGER IF EXISTS goal_progress ON portfolio_management.investment_goals; +CREATE TRIGGER goal_progress + BEFORE INSERT OR UPDATE OF current_amount, target_amount, target_date + ON portfolio_management.investment_goals + FOR EACH ROW + EXECUTE FUNCTION portfolio_management.calculate_goal_progress(); + +-- Vista de metas activas +CREATE OR REPLACE VIEW portfolio_management.v_active_goals AS +SELECT + id, + user_id, + name, + type, + status, + target_amount, + current_amount, + progress_percent, + target_date, + months_remaining, + monthly_contribution, + on_track, + amount_gap +FROM portfolio_management.investment_goals +WHERE status NOT IN ('achieved', 'abandoned') +ORDER BY target_date; + +-- Vista de sugerencias pendientes +CREATE OR REPLACE VIEW portfolio_management.v_pending_rebalances AS +SELECT + rs.id, + rs.portfolio_id, + p.name AS portfolio_name, + rs.asset_class, + rs.symbol, + rs.action, + rs.current_allocation, + rs.target_allocation, + rs.deviation, + rs.suggested_amount, + rs.priority, + rs.is_urgent, + rs.suggestion_date +FROM portfolio_management.rebalance_suggestions rs +JOIN portfolio_management.portfolios p ON rs.portfolio_id = p.id +WHERE rs.is_applied = FALSE + AND rs.is_expired = FALSE + AND (rs.valid_until IS NULL OR rs.valid_until > NOW()) +ORDER BY rs.priority DESC, rs.suggestion_date DESC; + +-- RLS +ALTER TABLE portfolio_management.investment_goals ENABLE ROW LEVEL SECURITY; +ALTER TABLE portfolio_management.rebalance_suggestions ENABLE ROW LEVEL SECURITY; + +CREATE POLICY goals_tenant ON portfolio_management.investment_goals + FOR ALL + USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); + +CREATE POLICY goals_user ON portfolio_management.investment_goals + FOR ALL + USING (user_id = current_setting('app.current_user_id', true)::UUID); + +CREATE POLICY rebalance_via_portfolio ON portfolio_management.rebalance_suggestions + FOR ALL + USING ( + portfolio_id IN ( + SELECT id FROM portfolio_management.portfolios + WHERE user_id = current_setting('app.current_user_id', true)::UUID + ) + ); + +-- Grants +GRANT SELECT, INSERT, UPDATE, DELETE ON portfolio_management.investment_goals TO trading_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON portfolio_management.rebalance_suggestions TO trading_app; +GRANT SELECT ON portfolio_management.investment_goals TO trading_readonly; +GRANT SELECT ON portfolio_management.rebalance_suggestions TO trading_readonly; +GRANT SELECT ON portfolio_management.v_active_goals TO trading_app; +GRANT SELECT ON portfolio_management.v_pending_rebalances TO trading_app; diff --git a/ddl/schemas/portfolio_management/tables/003_portfolio_snapshots.sql b/ddl/schemas/portfolio_management/tables/003_portfolio_snapshots.sql new file mode 100644 index 0000000..3c035a5 --- /dev/null +++ b/ddl/schemas/portfolio_management/tables/003_portfolio_snapshots.sql @@ -0,0 +1,282 @@ +-- ============================================================================ +-- SCHEMA: portfolio_management +-- TABLE: portfolio_snapshots, monte_carlo_projections +-- DESCRIPTION: Snapshots historicos y proyecciones Monte Carlo +-- VERSION: 1.0.0 +-- CREATED: 2026-01-16 +-- SPRINT: Sprint 6 - DDL Implementation Roadmap Q1-2026 +-- ============================================================================ + +-- Tabla de Snapshots de Portafolio +CREATE TABLE IF NOT EXISTS portfolio_management.portfolio_snapshots ( + -- Identificadores + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + portfolio_id UUID NOT NULL REFERENCES portfolio_management.portfolios(id) ON DELETE CASCADE, + + -- Fecha del snapshot + snapshot_date DATE NOT NULL, + snapshot_type VARCHAR(20) NOT NULL DEFAULT 'daily', -- 'daily', 'weekly', 'monthly', 'quarterly' + + -- Valores + total_value DECIMAL(15, 2) NOT NULL, + cash_value DECIMAL(15, 2) NOT NULL DEFAULT 0, + invested_value DECIMAL(15, 2) NOT NULL DEFAULT 0, + + -- Performance del dia + daily_change DECIMAL(15, 4), + daily_change_percent DECIMAL(10, 4), + + -- Performance acumulado + total_return DECIMAL(15, 4), + total_return_percent DECIMAL(10, 4), + ytd_return DECIMAL(15, 4), + ytd_return_percent DECIMAL(10, 4), + + -- Contribuciones/Retiros del dia + contributions DECIMAL(15, 2) DEFAULT 0, + withdrawals DECIMAL(15, 2) DEFAULT 0, + net_flow DECIMAL(15, 2) DEFAULT 0, + + -- Dividendos + dividends_received DECIMAL(15, 4) DEFAULT 0, + + -- Alocacion + allocation JSONB NOT NULL DEFAULT '{}'::JSONB, + + -- Posiciones top + top_holdings JSONB DEFAULT '[]'::JSONB, -- [{"symbol": "AAPL", "value": 1000, "percent": 10}] + positions_count INTEGER NOT NULL DEFAULT 0, + + -- Metricas de riesgo (al cierre) + volatility_30d DECIMAL(10, 4), + sharpe_ratio_30d DECIMAL(10, 4), + max_drawdown_30d DECIMAL(10, 4), + beta_30d DECIMAL(10, 4), + + -- Benchmark + benchmark_value DECIMAL(15, 8), + benchmark_return DECIMAL(10, 4), + active_return DECIMAL(10, 4), + + -- Metadata + metadata JSONB DEFAULT '{}'::JSONB, + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + + -- Constraints + CONSTRAINT snapshot_unique UNIQUE (portfolio_id, snapshot_date, snapshot_type) +); + +COMMENT ON TABLE portfolio_management.portfolio_snapshots IS +'Snapshots historicos diarios/semanales/mensuales del estado del portafolio'; + +-- ============================================================================ +-- TABLE: monte_carlo_projections +-- ============================================================================ + +-- Tabla de Proyecciones Monte Carlo +CREATE TABLE IF NOT EXISTS portfolio_management.monte_carlo_projections ( + -- Identificadores + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + portfolio_id UUID REFERENCES portfolio_management.portfolios(id) ON DELETE CASCADE, + goal_id UUID REFERENCES portfolio_management.investment_goals(id) ON DELETE CASCADE, + + -- Parametros de simulacion + simulation_date DATE NOT NULL DEFAULT CURRENT_DATE, + num_simulations INTEGER NOT NULL DEFAULT 10000, + time_horizon_months INTEGER NOT NULL, + confidence_levels DECIMAL(5, 2)[] DEFAULT ARRAY[10, 25, 50, 75, 90], + + -- Inputs + initial_value DECIMAL(15, 2) NOT NULL, + monthly_contribution DECIMAL(15, 2) DEFAULT 0, + expected_return DECIMAL(10, 4) NOT NULL, -- Anual + volatility DECIMAL(10, 4) NOT NULL, -- Anual + inflation_rate DECIMAL(10, 4) DEFAULT 0.03, + + -- Resultados por percentil + percentile_10 DECIMAL(15, 2), -- Pesimista + percentile_25 DECIMAL(15, 2), + percentile_50 DECIMAL(15, 2), -- Mediana + percentile_75 DECIMAL(15, 2), + percentile_90 DECIMAL(15, 2), -- Optimista + + -- Valor real ajustado por inflacion + real_percentile_10 DECIMAL(15, 2), + real_percentile_50 DECIMAL(15, 2), + real_percentile_90 DECIMAL(15, 2), + + -- Probabilidades + prob_reach_goal DECIMAL(5, 2), -- % de alcanzar meta + prob_positive_return DECIMAL(5, 2), -- % de retorno positivo + prob_beat_inflation DECIMAL(5, 2), -- % de vencer inflacion + + -- Riesgo de ruina + prob_ruin DECIMAL(5, 2), -- % de perder >50% + worst_case DECIMAL(15, 2), -- Peor caso (percentil 1) + best_case DECIMAL(15, 2), -- Mejor caso (percentil 99) + + -- Drawdown esperado + expected_max_drawdown DECIMAL(10, 4), + prob_drawdown_20_plus DECIMAL(5, 2), -- % de drawdown >20% + + -- Paths de simulacion (muestra) + sample_paths JSONB DEFAULT '[]'::JSONB, -- 5-10 paths de ejemplo + + -- Distribucion final + final_distribution JSONB DEFAULT '{}'::JSONB, -- Histograma de valores finales + + -- Sensibilidad + sensitivity_return JSONB DEFAULT '{}'::JSONB, -- Impacto de cambio en return + sensitivity_volatility JSONB DEFAULT '{}'::JSONB, -- Impacto de cambio en vol + sensitivity_contribution JSONB DEFAULT '{}'::JSONB, -- Impacto de cambio en contribucion + + -- Metadata + computation_time_ms INTEGER, + random_seed BIGINT, + metadata JSONB DEFAULT '{}'::JSONB, + notes TEXT, + + -- Timestamps + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + expires_at TIMESTAMPTZ DEFAULT NOW() + INTERVAL '7 days', + + -- Constraints + CONSTRAINT monte_carlo_portfolio_or_goal CHECK ( + portfolio_id IS NOT NULL OR goal_id IS NOT NULL + ) +); + +COMMENT ON TABLE portfolio_management.monte_carlo_projections IS +'Proyecciones Monte Carlo para portafolios y metas de inversion'; + +COMMENT ON COLUMN portfolio_management.monte_carlo_projections.prob_reach_goal IS +'Probabilidad de alcanzar el objetivo basado en simulaciones'; + +-- Indices para snapshots +CREATE INDEX IF NOT EXISTS idx_snapshots_portfolio + ON portfolio_management.portfolio_snapshots(portfolio_id, snapshot_date DESC); + +CREATE INDEX IF NOT EXISTS idx_snapshots_date + ON portfolio_management.portfolio_snapshots(snapshot_date DESC); + +CREATE INDEX IF NOT EXISTS idx_snapshots_type + ON portfolio_management.portfolio_snapshots(snapshot_type, snapshot_date DESC); + +-- Indice BRIN para datos temporales +CREATE INDEX IF NOT EXISTS idx_snapshots_brin + ON portfolio_management.portfolio_snapshots USING BRIN (snapshot_date); + +-- Indices para monte_carlo +CREATE INDEX IF NOT EXISTS idx_monte_carlo_portfolio + ON portfolio_management.monte_carlo_projections(portfolio_id, simulation_date DESC) + WHERE portfolio_id IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_monte_carlo_goal + ON portfolio_management.monte_carlo_projections(goal_id, simulation_date DESC) + WHERE goal_id IS NOT NULL; + +CREATE INDEX IF NOT EXISTS idx_monte_carlo_recent + ON portfolio_management.monte_carlo_projections(simulation_date DESC); + +-- Funcion para obtener performance historico +CREATE OR REPLACE FUNCTION portfolio_management.get_portfolio_performance( + p_portfolio_id UUID, + p_start_date DATE DEFAULT NULL, + p_end_date DATE DEFAULT NULL +) +RETURNS TABLE ( + snapshot_date DATE, + total_value DECIMAL, + daily_change_percent DECIMAL, + total_return_percent DECIMAL, + cumulative_return DECIMAL +) AS $$ +BEGIN + RETURN QUERY + SELECT + ps.snapshot_date, + ps.total_value, + ps.daily_change_percent, + ps.total_return_percent, + SUM(ps.daily_change_percent) OVER (ORDER BY ps.snapshot_date) AS cumulative_return + FROM portfolio_management.portfolio_snapshots ps + WHERE ps.portfolio_id = p_portfolio_id + AND (p_start_date IS NULL OR ps.snapshot_date >= p_start_date) + AND (p_end_date IS NULL OR ps.snapshot_date <= p_end_date) + ORDER BY ps.snapshot_date; +END; +$$ LANGUAGE plpgsql; + +-- Vista de performance mensual +CREATE OR REPLACE VIEW portfolio_management.v_monthly_performance AS +SELECT + portfolio_id, + DATE_TRUNC('month', snapshot_date)::DATE AS month, + FIRST_VALUE(total_value) OVER w AS start_value, + LAST_VALUE(total_value) OVER w AS end_value, + SUM(contributions) AS total_contributions, + SUM(dividends_received) AS total_dividends, + MAX(total_return_percent) - MIN(total_return_percent) AS monthly_return_percent +FROM portfolio_management.portfolio_snapshots +WHERE snapshot_type = 'daily' +WINDOW w AS (PARTITION BY portfolio_id, DATE_TRUNC('month', snapshot_date) + ORDER BY snapshot_date + ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) +GROUP BY portfolio_id, DATE_TRUNC('month', snapshot_date), total_value, snapshot_date +ORDER BY portfolio_id, month DESC; + +-- Vista de ultima proyeccion +CREATE OR REPLACE VIEW portfolio_management.v_latest_projections AS +SELECT DISTINCT ON (COALESCE(portfolio_id, goal_id)) + id, + portfolio_id, + goal_id, + simulation_date, + time_horizon_months, + initial_value, + percentile_10, + percentile_50, + percentile_90, + prob_reach_goal, + prob_positive_return, + expected_max_drawdown +FROM portfolio_management.monte_carlo_projections +WHERE expires_at > NOW() +ORDER BY COALESCE(portfolio_id, goal_id), simulation_date DESC; + +-- RLS +ALTER TABLE portfolio_management.portfolio_snapshots ENABLE ROW LEVEL SECURITY; +ALTER TABLE portfolio_management.monte_carlo_projections ENABLE ROW LEVEL SECURITY; + +CREATE POLICY snapshots_via_portfolio ON portfolio_management.portfolio_snapshots + FOR ALL + USING ( + portfolio_id IN ( + SELECT id FROM portfolio_management.portfolios + WHERE user_id = current_setting('app.current_user_id', true)::UUID + ) + ); + +CREATE POLICY monte_carlo_via_portfolio ON portfolio_management.monte_carlo_projections + FOR ALL + USING ( + portfolio_id IN ( + SELECT id FROM portfolio_management.portfolios + WHERE user_id = current_setting('app.current_user_id', true)::UUID + ) + OR goal_id IN ( + SELECT id FROM portfolio_management.investment_goals + WHERE user_id = current_setting('app.current_user_id', true)::UUID + ) + ); + +-- Grants +GRANT SELECT, INSERT, UPDATE, DELETE ON portfolio_management.portfolio_snapshots TO trading_app; +GRANT SELECT, INSERT, UPDATE, DELETE ON portfolio_management.monte_carlo_projections TO trading_app; +GRANT SELECT ON portfolio_management.portfolio_snapshots TO trading_readonly; +GRANT SELECT ON portfolio_management.monte_carlo_projections TO trading_readonly; +GRANT SELECT ON portfolio_management.v_monthly_performance TO trading_app; +GRANT SELECT ON portfolio_management.v_latest_projections TO trading_app; +GRANT EXECUTE ON FUNCTION portfolio_management.get_portfolio_performance TO trading_app;