-- ============================================================================ -- SCHEMA: trading -- TABLES: paper_trading_accounts, paper_trading_positions -- DESCRIPTION: Sistema de paper trading (cuentas demo) -- VERSION: 1.0.0 -- CREATED: 2026-01-16 -- SPRINT: Sprint 3 - DDL Implementation Roadmap Q1-2026 -- ============================================================================ -- ============================================================================ -- TABLA: paper_trading_accounts -- ============================================================================ CREATE TABLE IF NOT EXISTS trading.paper_trading_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, -- Informacion de cuenta name VARCHAR(100) NOT NULL DEFAULT 'Paper Account', description TEXT, -- Capital initial_balance DECIMAL(15, 2) NOT NULL DEFAULT 10000, current_balance DECIMAL(15, 2) NOT NULL DEFAULT 10000, currency VARCHAR(3) NOT NULL DEFAULT 'USD', -- Configuracion leverage INTEGER NOT NULL DEFAULT 100, margin_call_level DECIMAL(5, 2) DEFAULT 50, -- % de margen para margin call stop_out_level DECIMAL(5, 2) DEFAULT 20, -- % de margen para stop out -- Estado is_active BOOLEAN NOT NULL DEFAULT TRUE, is_default BOOLEAN NOT NULL DEFAULT FALSE, -- Margen used_margin DECIMAL(15, 2) NOT NULL DEFAULT 0, free_margin DECIMAL(15, 2) NOT NULL DEFAULT 10000, margin_level DECIMAL(10, 2), -- (Equity / Used Margin) * 100 -- Equity y P&L equity DECIMAL(15, 2) NOT NULL DEFAULT 10000, -- Balance + Unrealized P&L unrealized_pnl DECIMAL(15, 4) NOT NULL DEFAULT 0, realized_pnl DECIMAL(15, 4) NOT NULL DEFAULT 0, -- Estadisticas total_trades INTEGER NOT NULL DEFAULT 0, winning_trades INTEGER NOT NULL DEFAULT 0, losing_trades INTEGER NOT NULL DEFAULT 0, win_rate DECIMAL(5, 2) DEFAULT 0, profit_factor DECIMAL(10, 4) DEFAULT 0, max_drawdown DECIMAL(15, 4) DEFAULT 0, max_drawdown_percent DECIMAL(5, 2) DEFAULT 0, -- Rendimiento total_profit DECIMAL(15, 4) NOT NULL DEFAULT 0, total_loss DECIMAL(15, 4) NOT NULL DEFAULT 0, net_profit DECIMAL(15, 4) NOT NULL DEFAULT 0, return_percent DECIMAL(10, 4) DEFAULT 0, -- Posiciones actuales open_positions_count INTEGER NOT NULL DEFAULT 0, -- Resets reset_count INTEGER NOT NULL DEFAULT 0, last_reset_at TIMESTAMPTZ, -- Metadata metadata JSONB DEFAULT '{}'::JSONB, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Constraints CONSTRAINT paper_accounts_balance_check CHECK (initial_balance > 0) ); COMMENT ON TABLE trading.paper_trading_accounts IS 'Cuentas de paper trading (demo) para practicar sin dinero real'; -- Indices para paper_trading_accounts CREATE INDEX IF NOT EXISTS idx_paper_accounts_tenant ON trading.paper_trading_accounts(tenant_id); CREATE INDEX IF NOT EXISTS idx_paper_accounts_user ON trading.paper_trading_accounts(user_id); CREATE INDEX IF NOT EXISTS idx_paper_accounts_active ON trading.paper_trading_accounts(user_id, is_active) WHERE is_active = TRUE; CREATE INDEX IF NOT EXISTS idx_paper_accounts_default ON trading.paper_trading_accounts(user_id, is_default) WHERE is_default = TRUE; -- ============================================================================ -- TABLA: paper_trading_positions -- ============================================================================ CREATE TABLE IF NOT EXISTS trading.paper_trading_positions ( -- Identificadores id UUID PRIMARY KEY DEFAULT gen_random_uuid(), account_id UUID NOT NULL REFERENCES trading.paper_trading_accounts(id) ON DELETE CASCADE, tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE, -- Simbolo symbol VARCHAR(20) NOT NULL, symbol_id UUID REFERENCES trading.symbols(id), -- Direccion y estado direction trading.trade_direction NOT NULL, status trading.position_status NOT NULL DEFAULT 'open', order_type trading.order_type NOT NULL DEFAULT 'market', -- TamaƱo lot_size DECIMAL(10, 4) NOT NULL, -- Precios entry_price DECIMAL(15, 8) NOT NULL, current_price DECIMAL(15, 8), exit_price DECIMAL(15, 8), -- Stop Loss / Take Profit stop_loss DECIMAL(15, 8), take_profit DECIMAL(15, 8), trailing_stop_enabled BOOLEAN NOT NULL DEFAULT FALSE, trailing_stop_distance DECIMAL(10, 4), -- P&L profit_loss DECIMAL(15, 4), profit_loss_pips DECIMAL(10, 4), unrealized_pnl DECIMAL(15, 4) NOT NULL DEFAULT 0, -- Margen margin_used DECIMAL(15, 4), -- Comisiones simuladas commission DECIMAL(15, 4) DEFAULT 0, swap DECIMAL(15, 4) DEFAULT 0, -- Analisis max_favorable_excursion DECIMAL(15, 4), max_adverse_excursion DECIMAL(15, 4), -- Notas notes TEXT, tags VARCHAR(50)[], -- Timestamps opened_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), closed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); COMMENT ON TABLE trading.paper_trading_positions IS 'Posiciones de paper trading asociadas a cuentas demo'; -- Indices para paper_trading_positions CREATE INDEX IF NOT EXISTS idx_paper_positions_account ON trading.paper_trading_positions(account_id); CREATE INDEX IF NOT EXISTS idx_paper_positions_user ON trading.paper_trading_positions(user_id); CREATE INDEX IF NOT EXISTS idx_paper_positions_tenant ON trading.paper_trading_positions(tenant_id); CREATE INDEX IF NOT EXISTS idx_paper_positions_symbol ON trading.paper_trading_positions(symbol); CREATE INDEX IF NOT EXISTS idx_paper_positions_status ON trading.paper_trading_positions(status); CREATE INDEX IF NOT EXISTS idx_paper_positions_open ON trading.paper_trading_positions(account_id, status) WHERE status = 'open'; -- ============================================================================ -- TRIGGERS -- ============================================================================ -- Trigger para updated_at en accounts DROP TRIGGER IF EXISTS paper_account_updated_at ON trading.paper_trading_accounts; CREATE TRIGGER paper_account_updated_at BEFORE UPDATE ON trading.paper_trading_accounts FOR EACH ROW EXECUTE FUNCTION trading.update_trading_timestamp(); -- Trigger para updated_at en positions DROP TRIGGER IF EXISTS paper_position_updated_at ON trading.paper_trading_positions; CREATE TRIGGER paper_position_updated_at BEFORE UPDATE ON trading.paper_trading_positions FOR EACH ROW EXECUTE FUNCTION trading.update_trading_timestamp(); -- Trigger para asegurar solo una cuenta default CREATE OR REPLACE FUNCTION trading.ensure_single_default_paper_account() RETURNS TRIGGER AS $$ BEGIN IF NEW.is_default = TRUE THEN UPDATE trading.paper_trading_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 paper_account_single_default ON trading.paper_trading_accounts; CREATE TRIGGER paper_account_single_default BEFORE INSERT OR UPDATE OF is_default ON trading.paper_trading_accounts FOR EACH ROW WHEN (NEW.is_default = TRUE) EXECUTE FUNCTION trading.ensure_single_default_paper_account(); -- Trigger para actualizar cuenta al abrir posicion CREATE OR REPLACE FUNCTION trading.update_paper_account_on_position_open() RETURNS TRIGGER AS $$ DECLARE v_margin DECIMAL(15, 4); v_account RECORD; BEGIN IF NEW.status = 'open' AND (OLD IS NULL OR OLD.status = 'pending') THEN SELECT * INTO v_account FROM trading.paper_trading_accounts WHERE id = NEW.account_id; -- Calcular margen requerido v_margin := (NEW.lot_size * 100000 * NEW.entry_price) / v_account.leverage; NEW.margin_used := v_margin; -- Actualizar cuenta UPDATE trading.paper_trading_accounts SET used_margin = used_margin + v_margin, free_margin = current_balance - (used_margin + v_margin), open_positions_count = open_positions_count + 1, margin_level = CASE WHEN (used_margin + v_margin) > 0 THEN (equity / (used_margin + v_margin)) * 100 ELSE NULL END WHERE id = NEW.account_id; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS paper_position_open ON trading.paper_trading_positions; CREATE TRIGGER paper_position_open BEFORE INSERT OR UPDATE OF status ON trading.paper_trading_positions FOR EACH ROW EXECUTE FUNCTION trading.update_paper_account_on_position_open(); -- Trigger para actualizar cuenta al cerrar posicion CREATE OR REPLACE FUNCTION trading.update_paper_account_on_position_close() RETURNS TRIGGER AS $$ BEGIN IF NEW.status IN ('closed', 'stopped', 'target_hit') AND OLD.status = 'open' THEN NEW.closed_at := NOW(); -- Calcular P&L IF NEW.direction = 'long' THEN NEW.profit_loss_pips := (NEW.exit_price - NEW.entry_price) * 10000; ELSE NEW.profit_loss_pips := (NEW.entry_price - NEW.exit_price) * 10000; END IF; NEW.profit_loss := NEW.profit_loss_pips * NEW.lot_size * 10; -- Aproximacion simplificada -- Actualizar cuenta UPDATE trading.paper_trading_accounts SET current_balance = current_balance + COALESCE(NEW.profit_loss, 0), used_margin = used_margin - COALESCE(NEW.margin_used, 0), free_margin = (current_balance + COALESCE(NEW.profit_loss, 0)) - (used_margin - COALESCE(NEW.margin_used, 0)), open_positions_count = open_positions_count - 1, total_trades = total_trades + 1, winning_trades = winning_trades + CASE WHEN NEW.profit_loss > 0 THEN 1 ELSE 0 END, losing_trades = losing_trades + CASE WHEN NEW.profit_loss < 0 THEN 1 ELSE 0 END, total_profit = total_profit + CASE WHEN NEW.profit_loss > 0 THEN NEW.profit_loss ELSE 0 END, total_loss = total_loss + CASE WHEN NEW.profit_loss < 0 THEN ABS(NEW.profit_loss) ELSE 0 END, realized_pnl = realized_pnl + COALESCE(NEW.profit_loss, 0) WHERE id = NEW.account_id; -- Recalcular estadisticas PERFORM trading.recalculate_paper_account_stats(NEW.account_id); END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS paper_position_close ON trading.paper_trading_positions; CREATE TRIGGER paper_position_close BEFORE UPDATE OF status ON trading.paper_trading_positions FOR EACH ROW EXECUTE FUNCTION trading.update_paper_account_on_position_close(); -- ============================================================================ -- FUNCIONES -- ============================================================================ -- Funcion para recalcular estadisticas de cuenta CREATE OR REPLACE FUNCTION trading.recalculate_paper_account_stats(p_account_id UUID) RETURNS VOID AS $$ DECLARE v_account RECORD; BEGIN SELECT * INTO v_account FROM trading.paper_trading_accounts WHERE id = p_account_id; UPDATE trading.paper_trading_accounts SET win_rate = CASE WHEN total_trades > 0 THEN (winning_trades::DECIMAL / total_trades * 100) ELSE 0 END, profit_factor = CASE WHEN total_loss > 0 THEN total_profit / total_loss ELSE 0 END, net_profit = total_profit - total_loss, return_percent = CASE WHEN initial_balance > 0 THEN ((current_balance - initial_balance) / initial_balance * 100) ELSE 0 END, equity = current_balance + unrealized_pnl WHERE id = p_account_id; END; $$ LANGUAGE plpgsql; -- Funcion para resetear cuenta paper trading CREATE OR REPLACE FUNCTION trading.reset_paper_account( p_account_id UUID, p_new_balance DECIMAL(15, 2) DEFAULT NULL ) RETURNS BOOLEAN AS $$ DECLARE v_account RECORD; BEGIN SELECT * INTO v_account FROM trading.paper_trading_accounts WHERE id = p_account_id; IF v_account IS NULL THEN RETURN FALSE; END IF; -- Cerrar todas las posiciones abiertas UPDATE trading.paper_trading_positions SET status = 'cancelled', closed_at = NOW() WHERE account_id = p_account_id AND status = 'open'; -- Resetear cuenta UPDATE trading.paper_trading_accounts SET current_balance = COALESCE(p_new_balance, initial_balance), equity = COALESCE(p_new_balance, initial_balance), used_margin = 0, free_margin = COALESCE(p_new_balance, initial_balance), margin_level = NULL, unrealized_pnl = 0, realized_pnl = 0, total_trades = 0, winning_trades = 0, losing_trades = 0, win_rate = 0, profit_factor = 0, max_drawdown = 0, max_drawdown_percent = 0, total_profit = 0, total_loss = 0, net_profit = 0, return_percent = 0, open_positions_count = 0, reset_count = reset_count + 1, last_reset_at = NOW() WHERE id = p_account_id; RETURN TRUE; END; $$ LANGUAGE plpgsql; -- ============================================================================ -- VISTAS -- ============================================================================ -- Vista de cuentas paper trading CREATE OR REPLACE VIEW trading.v_paper_accounts AS SELECT pa.id, pa.user_id, pa.name, pa.initial_balance, pa.current_balance, pa.currency, pa.leverage, pa.equity, pa.unrealized_pnl, pa.realized_pnl, pa.return_percent, pa.win_rate, pa.profit_factor, pa.total_trades, pa.open_positions_count, pa.is_default, pa.is_active FROM trading.paper_trading_accounts pa WHERE pa.is_active = TRUE ORDER BY pa.is_default DESC, pa.created_at DESC; -- Vista de posiciones abiertas paper trading CREATE OR REPLACE VIEW trading.v_paper_open_positions AS SELECT pp.id, pp.account_id, pa.name AS account_name, pp.symbol, pp.direction, pp.lot_size, pp.entry_price, pp.current_price, pp.stop_loss, pp.take_profit, pp.unrealized_pnl, pp.margin_used, pp.opened_at FROM trading.paper_trading_positions pp JOIN trading.paper_trading_accounts pa ON pp.account_id = pa.id WHERE pp.status = 'open' ORDER BY pp.opened_at DESC; -- ============================================================================ -- RLS POLICIES -- ============================================================================ ALTER TABLE trading.paper_trading_accounts ENABLE ROW LEVEL SECURITY; CREATE POLICY paper_accounts_tenant_isolation ON trading.paper_trading_accounts FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); CREATE POLICY paper_accounts_user_isolation ON trading.paper_trading_accounts FOR ALL USING (user_id = current_setting('app.current_user_id', true)::UUID); ALTER TABLE trading.paper_trading_positions ENABLE ROW LEVEL SECURITY; CREATE POLICY paper_positions_tenant_isolation ON trading.paper_trading_positions FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); CREATE POLICY paper_positions_user_isolation ON trading.paper_trading_positions FOR ALL USING (user_id = current_setting('app.current_user_id', true)::UUID); -- ============================================================================ -- GRANTS -- ============================================================================ GRANT SELECT, INSERT, UPDATE, DELETE ON trading.paper_trading_accounts TO trading_app; GRANT SELECT, INSERT, UPDATE, DELETE ON trading.paper_trading_positions TO trading_app; GRANT SELECT ON trading.paper_trading_accounts TO trading_readonly; GRANT SELECT ON trading.paper_trading_positions TO trading_readonly; GRANT SELECT ON trading.v_paper_accounts TO trading_app; GRANT SELECT ON trading.v_paper_open_positions TO trading_app; GRANT EXECUTE ON FUNCTION trading.recalculate_paper_account_stats TO trading_app; GRANT EXECUTE ON FUNCTION trading.reset_paper_account TO trading_app;