trading-platform-database-v2/ddl/schemas/investment/001_agent_allocations.sql
rckrdmrd e520268348 Migración desde trading-platform/apps/database - Estándar multi-repo v2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 08:32:52 -06:00

521 lines
18 KiB
PL/PgSQL

-- ============================================================================
-- SCHEMA: investment
-- TABLES: agent_allocations, allocation_transactions, profit_distributions
-- DESCRIPTION: Sistema de fondeo de Money Manager Agents desde Wallet
-- VERSION: 1.0.0
-- CREATED: 2026-01-10
-- DEPENDS: financial.wallets, agents (existing)
-- ============================================================================
-- Crear schema si no existe
CREATE SCHEMA IF NOT EXISTS investment;
-- Enum para tipo de agente
DO $$ BEGIN
CREATE TYPE investment.agent_type AS ENUM (
'ATLAS', -- Conservador - bajo riesgo
'ORION', -- Moderado - riesgo medio
'NOVA' -- Agresivo - alto riesgo
);
EXCEPTION WHEN duplicate_object THEN null; END $$;
-- Enum para estado de allocation
DO $$ BEGIN
CREATE TYPE investment.allocation_status AS ENUM (
'pending', -- Esperando confirmacion
'active', -- Activa y operando
'paused', -- Pausada por usuario
'liquidating', -- En proceso de liquidacion
'closed' -- Cerrada
);
EXCEPTION WHEN duplicate_object THEN null; END $$;
-- Enum para tipo de transaccion de allocation
DO $$ BEGIN
CREATE TYPE investment.allocation_tx_type AS ENUM (
'INITIAL_FUNDING', -- Fondeo inicial
'ADDITIONAL_FUNDING', -- Fondeo adicional
'PARTIAL_WITHDRAWAL', -- Retiro parcial
'FULL_WITHDRAWAL', -- Retiro total
'PROFIT_REALIZED', -- Ganancia realizada
'LOSS_REALIZED', -- Perdida realizada
'FEE_CHARGED', -- Comision cobrada
'REBALANCE' -- Rebalanceo
);
EXCEPTION WHEN duplicate_object THEN null; END $$;
-- Tabla principal de allocations (fondeo a agentes)
CREATE TABLE IF NOT EXISTS investment.agent_allocations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
wallet_id UUID NOT NULL,
-- Agente
agent_type investment.agent_type NOT NULL,
agent_instance_id UUID, -- Referencia a instancia especifica del agente
-- Montos
initial_amount DECIMAL(15, 4) NOT NULL CHECK (initial_amount > 0),
current_amount DECIMAL(15, 4) NOT NULL CHECK (current_amount >= 0),
total_deposited DECIMAL(15, 4) NOT NULL DEFAULT 0,
total_withdrawn DECIMAL(15, 4) NOT NULL DEFAULT 0,
-- Performance
total_profit DECIMAL(15, 4) NOT NULL DEFAULT 0,
total_loss DECIMAL(15, 4) NOT NULL DEFAULT 0,
realized_pnl DECIMAL(15, 4) GENERATED ALWAYS AS (total_profit - total_loss) STORED,
unrealized_pnl DECIMAL(15, 4) DEFAULT 0,
-- Metricas
roi_percentage DECIMAL(10, 4) DEFAULT 0,
max_drawdown DECIMAL(10, 4) DEFAULT 0,
win_rate DECIMAL(5, 2) DEFAULT 0,
total_trades INT DEFAULT 0,
winning_trades INT DEFAULT 0,
losing_trades INT DEFAULT 0,
-- Configuracion
risk_level VARCHAR(20), -- LOW, MEDIUM, HIGH
auto_compound BOOLEAN DEFAULT FALSE,
compound_threshold DECIMAL(15, 4) DEFAULT 100,
-- Limites
max_allocation DECIMAL(15, 4),
stop_loss_percentage DECIMAL(5, 2) DEFAULT 20,
take_profit_percentage DECIMAL(5, 2),
-- Estado
status investment.allocation_status NOT NULL DEFAULT 'pending',
-- Fechas importantes
activated_at TIMESTAMPTZ,
last_trade_at TIMESTAMPTZ,
paused_at TIMESTAMPTZ,
closed_at TIMESTAMPTZ,
-- Metadata
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
-- Un usuario puede tener multiples allocations al mismo agente
-- pero con limites
CONSTRAINT positive_performance CHECK (
total_deposited >= total_withdrawn
OR status IN ('closed', 'liquidating')
)
);
COMMENT ON TABLE investment.agent_allocations IS
'Fondeos de usuarios a Money Manager Agents (Atlas, Orion, Nova)';
-- Tabla de transacciones de allocation
CREATE TABLE IF NOT EXISTS investment.allocation_transactions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
allocation_id UUID NOT NULL REFERENCES investment.agent_allocations(id),
tenant_id UUID NOT NULL,
-- Tipo
type investment.allocation_tx_type NOT NULL,
-- Montos
amount DECIMAL(15, 4) NOT NULL,
balance_before DECIMAL(15, 4) NOT NULL,
balance_after DECIMAL(15, 4) NOT NULL,
-- Referencia a wallet transaction
wallet_transaction_id UUID,
-- Para trades
trade_id UUID,
trade_symbol VARCHAR(20),
trade_pnl DECIMAL(15, 4),
-- Descripcion
description TEXT,
-- Metadata
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE investment.allocation_transactions IS
'Historial de todas las transacciones en allocations de agentes';
-- Tabla de distribuciones de profit
CREATE TABLE IF NOT EXISTS investment.profit_distributions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
allocation_id UUID NOT NULL REFERENCES investment.agent_allocations(id),
tenant_id UUID NOT NULL,
user_id UUID NOT NULL,
-- Periodo
period_start TIMESTAMPTZ NOT NULL,
period_end TIMESTAMPTZ NOT NULL,
-- Montos
gross_profit DECIMAL(15, 4) NOT NULL,
platform_fee DECIMAL(15, 4) NOT NULL DEFAULT 0,
agent_fee DECIMAL(15, 4) NOT NULL DEFAULT 0,
net_profit DECIMAL(15, 4) NOT NULL,
-- Fee percentages aplicados
platform_fee_rate DECIMAL(5, 4) DEFAULT 0.10, -- 10%
agent_fee_rate DECIMAL(5, 4) DEFAULT 0.20, -- 20%
-- Distribucion
distributed_to_wallet BOOLEAN DEFAULT FALSE,
wallet_transaction_id UUID,
distributed_at TIMESTAMPTZ,
-- Compounding
compounded_amount DECIMAL(15, 4) DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW()
);
COMMENT ON TABLE investment.profit_distributions IS
'Distribuciones periodicas de ganancias de agentes a usuarios';
-- Tabla de configuracion de agentes por tenant
CREATE TABLE IF NOT EXISTS investment.agent_configs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
agent_type investment.agent_type NOT NULL,
-- Limites
min_allocation DECIMAL(15, 4) DEFAULT 100,
max_allocation_per_user DECIMAL(15, 4) DEFAULT 10000,
max_total_allocation DECIMAL(15, 4) DEFAULT 1000000,
-- Fees
platform_fee_rate DECIMAL(5, 4) DEFAULT 0.10,
performance_fee_rate DECIMAL(5, 4) DEFAULT 0.20,
-- Estado
is_active BOOLEAN DEFAULT TRUE,
accepting_new_allocations BOOLEAN DEFAULT TRUE,
-- Descripcion
description TEXT,
risk_disclosure TEXT,
-- Stats agregados
total_users INT DEFAULT 0,
total_allocated DECIMAL(15, 4) DEFAULT 0,
total_profit_generated DECIMAL(15, 4) DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(tenant_id, agent_type)
);
COMMENT ON TABLE investment.agent_configs IS
'Configuracion de Money Manager Agents por tenant';
-- Indices para agent_allocations
CREATE INDEX IF NOT EXISTS idx_alloc_tenant ON investment.agent_allocations(tenant_id);
CREATE INDEX IF NOT EXISTS idx_alloc_user ON investment.agent_allocations(user_id);
CREATE INDEX IF NOT EXISTS idx_alloc_wallet ON investment.agent_allocations(wallet_id);
CREATE INDEX IF NOT EXISTS idx_alloc_agent ON investment.agent_allocations(agent_type);
CREATE INDEX IF NOT EXISTS idx_alloc_status ON investment.agent_allocations(status);
CREATE INDEX IF NOT EXISTS idx_alloc_active ON investment.agent_allocations(status, agent_type)
WHERE status = 'active';
-- Indices para allocation_transactions
CREATE INDEX IF NOT EXISTS idx_alloc_tx_alloc ON investment.allocation_transactions(allocation_id);
CREATE INDEX IF NOT EXISTS idx_alloc_tx_tenant ON investment.allocation_transactions(tenant_id);
CREATE INDEX IF NOT EXISTS idx_alloc_tx_type ON investment.allocation_transactions(type);
CREATE INDEX IF NOT EXISTS idx_alloc_tx_created ON investment.allocation_transactions(created_at DESC);
-- Indices para profit_distributions
CREATE INDEX IF NOT EXISTS idx_profit_dist_alloc ON investment.profit_distributions(allocation_id);
CREATE INDEX IF NOT EXISTS idx_profit_dist_user ON investment.profit_distributions(user_id);
CREATE INDEX IF NOT EXISTS idx_profit_dist_period ON investment.profit_distributions(period_start, period_end);
CREATE INDEX IF NOT EXISTS idx_profit_dist_pending ON investment.profit_distributions(distributed_to_wallet)
WHERE distributed_to_wallet = FALSE;
-- Trigger updated_at
CREATE OR REPLACE FUNCTION investment.update_timestamp()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS alloc_updated ON investment.agent_allocations;
CREATE TRIGGER alloc_updated
BEFORE UPDATE ON investment.agent_allocations
FOR EACH ROW
EXECUTE FUNCTION investment.update_timestamp();
DROP TRIGGER IF EXISTS agent_config_updated ON investment.agent_configs;
CREATE TRIGGER agent_config_updated
BEFORE UPDATE ON investment.agent_configs
FOR EACH ROW
EXECUTE FUNCTION investment.update_timestamp();
-- Funcion para crear allocation (fondear agente)
CREATE OR REPLACE FUNCTION investment.create_allocation(
p_tenant_id UUID,
p_user_id UUID,
p_wallet_id UUID,
p_agent_type investment.agent_type,
p_amount DECIMAL(15, 4),
p_auto_compound BOOLEAN DEFAULT FALSE
)
RETURNS UUID AS $$
DECLARE
v_config investment.agent_configs%ROWTYPE;
v_allocation_id UUID;
v_wallet_tx_id UUID;
BEGIN
-- Get agent config
SELECT * INTO v_config
FROM investment.agent_configs
WHERE tenant_id = p_tenant_id
AND agent_type = p_agent_type;
IF NOT FOUND OR NOT v_config.is_active THEN
RAISE EXCEPTION 'Agent % is not available', p_agent_type;
END IF;
IF NOT v_config.accepting_new_allocations THEN
RAISE EXCEPTION 'Agent % is not accepting new allocations', p_agent_type;
END IF;
IF p_amount < v_config.min_allocation THEN
RAISE EXCEPTION 'Minimum allocation is %', v_config.min_allocation;
END IF;
IF p_amount > v_config.max_allocation_per_user THEN
RAISE EXCEPTION 'Maximum allocation per user is %', v_config.max_allocation_per_user;
END IF;
-- Debit from wallet (this validates balance)
v_wallet_tx_id := financial.create_wallet_transaction(
p_wallet_id,
'AGENT_FUNDING',
-p_amount,
'Funding to ' || p_agent_type || ' agent',
'agent_allocation',
NULL,
jsonb_build_object('agent_type', p_agent_type)
);
-- Create allocation
INSERT INTO investment.agent_allocations (
tenant_id, user_id, wallet_id, agent_type,
initial_amount, current_amount, total_deposited,
auto_compound, risk_level, status
) VALUES (
p_tenant_id, p_user_id, p_wallet_id, p_agent_type,
p_amount, p_amount, p_amount,
p_auto_compound,
CASE p_agent_type
WHEN 'ATLAS' THEN 'LOW'
WHEN 'ORION' THEN 'MEDIUM'
WHEN 'NOVA' THEN 'HIGH'
END,
'active'
) RETURNING id INTO v_allocation_id;
-- Update allocation with wallet tx reference
UPDATE investment.agent_allocations
SET activated_at = NOW(),
metadata = jsonb_build_object('initial_wallet_tx', v_wallet_tx_id)
WHERE id = v_allocation_id;
-- Record allocation transaction
INSERT INTO investment.allocation_transactions (
allocation_id, tenant_id, type,
amount, balance_before, balance_after,
wallet_transaction_id, description
) VALUES (
v_allocation_id, p_tenant_id, 'INITIAL_FUNDING',
p_amount, 0, p_amount,
v_wallet_tx_id, 'Initial funding'
);
-- Update agent config stats
UPDATE investment.agent_configs
SET total_users = total_users + 1,
total_allocated = total_allocated + p_amount
WHERE tenant_id = p_tenant_id AND agent_type = p_agent_type;
RETURN v_allocation_id;
END;
$$ LANGUAGE plpgsql;
-- Funcion para retirar de allocation
CREATE OR REPLACE FUNCTION investment.withdraw_from_allocation(
p_allocation_id UUID,
p_amount DECIMAL(15, 4),
p_full_withdrawal BOOLEAN DEFAULT FALSE
)
RETURNS UUID AS $$
DECLARE
v_alloc investment.agent_allocations%ROWTYPE;
v_wallet_tx_id UUID;
v_withdraw_amount DECIMAL(15, 4);
v_tx_type investment.allocation_tx_type;
BEGIN
-- Lock allocation
SELECT * INTO v_alloc
FROM investment.agent_allocations
WHERE id = p_allocation_id
FOR UPDATE;
IF NOT FOUND THEN
RAISE EXCEPTION 'Allocation not found';
END IF;
IF v_alloc.status NOT IN ('active', 'paused') THEN
RAISE EXCEPTION 'Cannot withdraw from allocation in status %', v_alloc.status;
END IF;
-- Determine withdrawal amount
IF p_full_withdrawal THEN
v_withdraw_amount := v_alloc.current_amount;
v_tx_type := 'FULL_WITHDRAWAL';
ELSE
IF p_amount > v_alloc.current_amount THEN
RAISE EXCEPTION 'Insufficient balance. Available: %', v_alloc.current_amount;
END IF;
v_withdraw_amount := p_amount;
v_tx_type := 'PARTIAL_WITHDRAWAL';
END IF;
-- Credit to wallet
v_wallet_tx_id := financial.create_wallet_transaction(
v_alloc.wallet_id,
'AGENT_WITHDRAWAL',
v_withdraw_amount,
'Withdrawal from ' || v_alloc.agent_type || ' agent',
'agent_allocation',
p_allocation_id,
jsonb_build_object('agent_type', v_alloc.agent_type)
);
-- Record transaction
INSERT INTO investment.allocation_transactions (
allocation_id, tenant_id, type,
amount, balance_before, balance_after,
wallet_transaction_id, description
) VALUES (
p_allocation_id, v_alloc.tenant_id, v_tx_type,
-v_withdraw_amount, v_alloc.current_amount,
v_alloc.current_amount - v_withdraw_amount,
v_wallet_tx_id,
CASE WHEN p_full_withdrawal THEN 'Full withdrawal' ELSE 'Partial withdrawal' END
);
-- Update allocation
UPDATE investment.agent_allocations
SET current_amount = current_amount - v_withdraw_amount,
total_withdrawn = total_withdrawn + v_withdraw_amount,
status = CASE WHEN p_full_withdrawal THEN 'closed' ELSE status END,
closed_at = CASE WHEN p_full_withdrawal THEN NOW() ELSE closed_at END
WHERE id = p_allocation_id;
-- Update agent config stats
UPDATE investment.agent_configs
SET total_allocated = total_allocated - v_withdraw_amount,
total_users = CASE WHEN p_full_withdrawal THEN total_users - 1 ELSE total_users END
WHERE tenant_id = v_alloc.tenant_id AND agent_type = v_alloc.agent_type;
RETURN v_wallet_tx_id;
END;
$$ LANGUAGE plpgsql;
-- Funcion para registrar profit/loss de trade
CREATE OR REPLACE FUNCTION investment.record_trade_result(
p_allocation_id UUID,
p_trade_id UUID,
p_symbol VARCHAR(20),
p_pnl DECIMAL(15, 4),
p_is_win BOOLEAN
)
RETURNS VOID AS $$
DECLARE
v_alloc investment.agent_allocations%ROWTYPE;
v_tx_type investment.allocation_tx_type;
BEGIN
SELECT * INTO v_alloc
FROM investment.agent_allocations
WHERE id = p_allocation_id
FOR UPDATE;
v_tx_type := CASE WHEN p_pnl >= 0 THEN 'PROFIT_REALIZED' ELSE 'LOSS_REALIZED' END;
-- Record transaction
INSERT INTO investment.allocation_transactions (
allocation_id, tenant_id, type,
amount, balance_before, balance_after,
trade_id, trade_symbol, trade_pnl
) VALUES (
p_allocation_id, v_alloc.tenant_id, v_tx_type,
p_pnl, v_alloc.current_amount, v_alloc.current_amount + p_pnl,
p_trade_id, p_symbol, p_pnl
);
-- Update allocation
UPDATE investment.agent_allocations
SET current_amount = current_amount + p_pnl,
total_profit = CASE WHEN p_pnl > 0 THEN total_profit + p_pnl ELSE total_profit END,
total_loss = CASE WHEN p_pnl < 0 THEN total_loss + ABS(p_pnl) ELSE total_loss END,
total_trades = total_trades + 1,
winning_trades = CASE WHEN p_is_win THEN winning_trades + 1 ELSE winning_trades END,
losing_trades = CASE WHEN NOT p_is_win THEN losing_trades + 1 ELSE losing_trades END,
win_rate = CASE
WHEN total_trades > 0 THEN
(winning_trades + CASE WHEN p_is_win THEN 1 ELSE 0 END)::DECIMAL * 100 /
(total_trades + 1)
ELSE 0
END,
roi_percentage = CASE
WHEN total_deposited > 0 THEN
((current_amount + p_pnl - total_deposited + total_withdrawn) / total_deposited) * 100
ELSE 0
END,
last_trade_at = NOW()
WHERE id = p_allocation_id;
END;
$$ LANGUAGE plpgsql;
-- RLS
ALTER TABLE investment.agent_allocations ENABLE ROW LEVEL SECURITY;
ALTER TABLE investment.allocation_transactions ENABLE ROW LEVEL SECURITY;
ALTER TABLE investment.profit_distributions ENABLE ROW LEVEL SECURITY;
ALTER TABLE investment.agent_configs ENABLE ROW LEVEL SECURITY;
CREATE POLICY alloc_tenant ON investment.agent_allocations
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
CREATE POLICY alloc_tx_tenant ON investment.allocation_transactions
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
CREATE POLICY profit_tenant ON investment.profit_distributions
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
CREATE POLICY config_tenant ON investment.agent_configs
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- Grants
GRANT SELECT, INSERT, UPDATE ON investment.agent_allocations TO trading_app;
GRANT SELECT, INSERT ON investment.allocation_transactions TO trading_app;
GRANT SELECT, INSERT, UPDATE ON investment.profit_distributions TO trading_app;
GRANT SELECT, UPDATE ON investment.agent_configs TO trading_app;
GRANT SELECT ON investment.agent_allocations TO trading_readonly;
GRANT SELECT ON investment.allocation_transactions TO trading_readonly;
GRANT SELECT ON investment.profit_distributions TO trading_readonly;
GRANT SELECT ON investment.agent_configs TO trading_readonly;