[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