[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 <noreply@anthropic.com>
This commit is contained in:
parent
62c811be45
commit
4631a58b42
255
ddl/schemas/broker_integration/tables/001_broker_accounts.sql
Normal file
255
ddl/schemas/broker_integration/tables/001_broker_accounts.sql
Normal file
@ -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;
|
||||||
96
ddl/schemas/broker_integration/tables/002_broker_prices.sql
Normal file
96
ddl/schemas/broker_integration/tables/002_broker_prices.sql
Normal file
@ -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;
|
||||||
259
ddl/schemas/broker_integration/tables/003_spread_statistics.sql
Normal file
259
ddl/schemas/broker_integration/tables/003_spread_statistics.sql
Normal file
@ -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;
|
||||||
258
ddl/schemas/broker_integration/tables/004_trade_execution.sql
Normal file
258
ddl/schemas/broker_integration/tables/004_trade_execution.sql
Normal file
@ -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;
|
||||||
310
ddl/schemas/portfolio_management/tables/001_portfolios.sql
Normal file
310
ddl/schemas/portfolio_management/tables/001_portfolios.sql
Normal file
@ -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;
|
||||||
333
ddl/schemas/portfolio_management/tables/002_investment_goals.sql
Normal file
333
ddl/schemas/portfolio_management/tables/002_investment_goals.sql
Normal file
@ -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;
|
||||||
@ -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;
|
||||||
Loading…
Reference in New Issue
Block a user