-- ============================================================================ -- 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;