# ET-TRD-003: Especificación Técnica - Database Schema **Version:** 1.0.0 **Fecha:** 2025-12-05 **Estado:** Pendiente **Épica:** [OQI-003](../_MAP.md) **Requerimiento:** RF-TRD-003 --- ## Resumen Esta especificación detalla el modelo de datos completo para el módulo de trading, incluyendo watchlists, paper trading (órdenes, posiciones, balances) y market data histórico en PostgreSQL 15+. --- ## Arquitectura ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ PostgreSQL 15+ │ │ │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ Schema: public │ │ │ │ ┌──────────┐ ┌────────────┐ ┌──────────────┐ │ │ │ │ │ users │ │ sessions │ │ oauth_accounts│ │ │ │ │ └──────────┘ └────────────┘ └──────────────┘ │ │ │ └────────────────────────────────────────────────────────────────┘ │ │ │ │ ┌────────────────────────────────────────────────────────────────┐ │ │ │ Schema: trading │ │ │ │ │ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ │ │ WATCHLISTS │ │ │ │ │ │ ┌──────────────┐ ┌────────────────────┐ │ │ │ │ │ │ │ watchlists │─────<│ watchlist_symbols │ │ │ │ │ │ │ └──────────────┘ └────────────────────┘ │ │ │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ │ │ PAPER TRADING │ │ │ │ │ │ ┌──────────────┐ │ │ │ │ │ │ │paper_balances│ │ │ │ │ │ │ └──────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ │ │ ┌──────────────┐ ┌─────────────────┐ │ │ │ │ │ │ │paper_orders │─────<│ paper_trades │ │ │ │ │ │ │ └──────────────┘ └─────────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ ▼ │ │ │ │ │ │ ┌───────────────┐ │ │ │ │ │ │ │paper_positions│ │ │ │ │ │ │ └───────────────┘ │ │ │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ │ │ MARKET DATA (Optional Cache) │ │ │ │ │ │ ┌──────────────┐ ┌────────────────┐ │ │ │ │ │ │ │ market_data │ │ rate_limits │ │ │ │ │ │ │ └──────────────┘ └────────────────┘ │ │ │ │ │ └───────────────────────────────────────────────────────┘ │ │ │ └────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## Schema Trading - ENUMs ```sql -- Crear schema CREATE SCHEMA IF NOT EXISTS trading; -- ENUM: Lado de la orden CREATE TYPE trading.order_side_enum AS ENUM ( 'buy', 'sell' ); -- ENUM: Tipo de orden CREATE TYPE trading.order_type_enum AS ENUM ( 'market', -- Orden a mercado 'limit', -- Orden limitada 'stop_loss', -- Stop loss 'stop_limit', -- Stop limit 'take_profit' -- Take profit ); -- ENUM: Estado de la orden CREATE TYPE trading.order_status_enum AS ENUM ( 'pending', -- Pendiente de ejecución 'open', -- Abierta (parcialmente ejecutada) 'filled', -- Completamente ejecutada 'cancelled', -- Cancelada 'rejected', -- Rechazada 'expired' -- Expirada ); -- ENUM: Lado de la posición CREATE TYPE trading.position_side_enum AS ENUM ( 'long', -- Posición larga 'short' -- Posición corta ); -- ENUM: Estado de la posición CREATE TYPE trading.position_status_enum AS ENUM ( 'open', -- Abierta 'closed' -- Cerrada ); -- ENUM: Tipo de trade CREATE TYPE trading.trade_type_enum AS ENUM ( 'entry', -- Entrada a posición 'exit', -- Salida de posición 'partial' -- Ejecución parcial ); ``` --- ## Tablas - Watchlists ### watchlists ```sql CREATE TABLE trading.watchlists ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, name VARCHAR(100) NOT NULL, description TEXT, color VARCHAR(7), -- Hex color code (#FF5733) is_default BOOLEAN DEFAULT false, order_index INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT watchlists_name_user_unique UNIQUE(user_id, name) ); -- Índices CREATE INDEX idx_watchlists_user_id ON trading.watchlists(user_id); CREATE INDEX idx_watchlists_is_default ON trading.watchlists(user_id, is_default) WHERE is_default = true; -- Trigger para updated_at CREATE TRIGGER update_watchlists_updated_at BEFORE UPDATE ON trading.watchlists FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Comentarios COMMENT ON TABLE trading.watchlists IS 'Listas de seguimiento de símbolos/activos del usuario'; COMMENT ON COLUMN trading.watchlists.is_default IS 'Indica si es la watchlist predeterminada del usuario'; COMMENT ON COLUMN trading.watchlists.order_index IS 'Orden de visualización de la watchlist'; ``` ### watchlist_symbols ```sql CREATE TABLE trading.watchlist_symbols ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), watchlist_id UUID NOT NULL REFERENCES trading.watchlists(id) ON DELETE CASCADE, symbol VARCHAR(20) NOT NULL, -- e.g., BTCUSDT base_asset VARCHAR(10) NOT NULL, -- e.g., BTC quote_asset VARCHAR(10) NOT NULL, -- e.g., USDT notes TEXT, alert_price_high DECIMAL(20, 8), -- Alerta cuando precio > este valor alert_price_low DECIMAL(20, 8), -- Alerta cuando precio < este valor order_index INTEGER DEFAULT 0, added_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT watchlist_symbols_unique UNIQUE(watchlist_id, symbol) ); -- Índices CREATE INDEX idx_watchlist_symbols_watchlist ON trading.watchlist_symbols(watchlist_id); CREATE INDEX idx_watchlist_symbols_symbol ON trading.watchlist_symbols(symbol); CREATE INDEX idx_watchlist_symbols_order ON trading.watchlist_symbols(watchlist_id, order_index); -- Trigger CREATE TRIGGER update_watchlist_symbols_updated_at BEFORE UPDATE ON trading.watchlist_symbols FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Comentarios COMMENT ON TABLE trading.watchlist_symbols IS 'Símbolos individuales dentro de cada watchlist'; COMMENT ON COLUMN trading.watchlist_symbols.alert_price_high IS 'Precio para alerta superior'; COMMENT ON COLUMN trading.watchlist_symbols.alert_price_low IS 'Precio para alerta inferior'; ``` --- ## Tablas - Paper Trading ### paper_balances ```sql CREATE TABLE trading.paper_balances ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, asset VARCHAR(10) NOT NULL, -- USDT, BTC, ETH, etc. total DECIMAL(20, 8) NOT NULL DEFAULT 0, -- Balance total available DECIMAL(20, 8) NOT NULL DEFAULT 0, -- Disponible para trading locked DECIMAL(20, 8) NOT NULL DEFAULT 0, -- Bloqueado en órdenes created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT paper_balances_user_asset_unique UNIQUE(user_id, asset), CONSTRAINT paper_balances_total_check CHECK (total >= 0), CONSTRAINT paper_balances_available_check CHECK (available >= 0), CONSTRAINT paper_balances_locked_check CHECK (locked >= 0), CONSTRAINT paper_balances_consistency CHECK (total = available + locked) ); -- Índices CREATE INDEX idx_paper_balances_user_id ON trading.paper_balances(user_id); CREATE INDEX idx_paper_balances_asset ON trading.paper_balances(user_id, asset); -- Trigger CREATE TRIGGER update_paper_balances_updated_at BEFORE UPDATE ON trading.paper_balances FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Comentarios COMMENT ON TABLE trading.paper_balances IS 'Balances de paper trading por usuario y activo'; COMMENT ON COLUMN trading.paper_balances.locked IS 'Cantidad bloqueada en órdenes abiertas'; ``` ### paper_orders ```sql CREATE TABLE trading.paper_orders ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, symbol VARCHAR(20) NOT NULL, side trading.order_side_enum NOT NULL, type trading.order_type_enum NOT NULL, status trading.order_status_enum NOT NULL DEFAULT 'pending', -- Cantidades quantity DECIMAL(20, 8) NOT NULL, filled_quantity DECIMAL(20, 8) DEFAULT 0, remaining_quantity DECIMAL(20, 8), -- Precios price DECIMAL(20, 8), -- Para limit orders stop_price DECIMAL(20, 8), -- Para stop orders average_fill_price DECIMAL(20, 8), -- Precio promedio de ejecución -- Valores monetarios quote_quantity DECIMAL(20, 8), -- Valor total en quote asset filled_quote_quantity DECIMAL(20, 8) DEFAULT 0, -- Fees commission DECIMAL(20, 8) DEFAULT 0, commission_asset VARCHAR(10), -- Time in force time_in_force VARCHAR(10) DEFAULT 'GTC', -- GTC, IOC, FOK -- Metadatos client_order_id VARCHAR(50), notes TEXT, -- Timestamps placed_at TIMESTAMPTZ DEFAULT NOW(), filled_at TIMESTAMPTZ, cancelled_at TIMESTAMPTZ, expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT paper_orders_quantity_check CHECK (quantity > 0), CONSTRAINT paper_orders_filled_check CHECK (filled_quantity >= 0 AND filled_quantity <= quantity), CONSTRAINT paper_orders_price_check CHECK ( (type = 'market') OR (type IN ('limit', 'stop_limit') AND price IS NOT NULL) OR (type IN ('stop_loss', 'stop_limit') AND stop_price IS NOT NULL) ) ); -- Índices CREATE INDEX idx_paper_orders_user_id ON trading.paper_orders(user_id); CREATE INDEX idx_paper_orders_symbol ON trading.paper_orders(symbol); CREATE INDEX idx_paper_orders_status ON trading.paper_orders(status); CREATE INDEX idx_paper_orders_user_status ON trading.paper_orders(user_id, status); CREATE INDEX idx_paper_orders_user_symbol ON trading.paper_orders(user_id, symbol); CREATE INDEX idx_paper_orders_placed_at ON trading.paper_orders(placed_at DESC); CREATE INDEX idx_paper_orders_client_id ON trading.paper_orders(client_order_id) WHERE client_order_id IS NOT NULL; -- Trigger CREATE TRIGGER update_paper_orders_updated_at BEFORE UPDATE ON trading.paper_orders FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Trigger para calcular remaining_quantity CREATE OR REPLACE FUNCTION trading.update_remaining_quantity() RETURNS TRIGGER AS $$ BEGIN NEW.remaining_quantity = NEW.quantity - NEW.filled_quantity; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER calculate_remaining_quantity BEFORE INSERT OR UPDATE ON trading.paper_orders FOR EACH ROW EXECUTE FUNCTION trading.update_remaining_quantity(); -- Comentarios COMMENT ON TABLE trading.paper_orders IS 'Órdenes de paper trading'; COMMENT ON COLUMN trading.paper_orders.time_in_force IS 'GTC (Good Till Cancelled), IOC (Immediate or Cancel), FOK (Fill or Kill)'; ``` ### paper_positions ```sql CREATE TABLE trading.paper_positions ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, symbol VARCHAR(20) NOT NULL, side trading.position_side_enum NOT NULL, status trading.position_status_enum NOT NULL DEFAULT 'open', -- Entry entry_price DECIMAL(20, 8) NOT NULL, entry_quantity DECIMAL(20, 8) NOT NULL, entry_value DECIMAL(20, 8) NOT NULL, -- entry_price * entry_quantity entry_order_id UUID REFERENCES trading.paper_orders(id), -- Exit exit_price DECIMAL(20, 8), exit_quantity DECIMAL(20, 8), exit_value DECIMAL(20, 8), exit_order_id UUID REFERENCES trading.paper_orders(id), -- Current state current_quantity DECIMAL(20, 8) NOT NULL, average_entry_price DECIMAL(20, 8) NOT NULL, -- PnL realized_pnl DECIMAL(20, 8) DEFAULT 0, unrealized_pnl DECIMAL(20, 8) DEFAULT 0, total_pnl DECIMAL(20, 8) DEFAULT 0, pnl_percentage DECIMAL(10, 4) DEFAULT 0, -- Fees total_commission DECIMAL(20, 8) DEFAULT 0, -- Risk management stop_loss_price DECIMAL(20, 8), take_profit_price DECIMAL(20, 8), -- Timestamps opened_at TIMESTAMPTZ DEFAULT NOW(), closed_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT paper_positions_quantity_check CHECK (entry_quantity > 0), CONSTRAINT paper_positions_current_check CHECK (current_quantity >= 0), CONSTRAINT paper_positions_user_symbol_open UNIQUE(user_id, symbol, status) WHERE status = 'open' ); -- Índices CREATE INDEX idx_paper_positions_user_id ON trading.paper_positions(user_id); CREATE INDEX idx_paper_positions_symbol ON trading.paper_positions(symbol); CREATE INDEX idx_paper_positions_status ON trading.paper_positions(status); CREATE INDEX idx_paper_positions_user_status ON trading.paper_positions(user_id, status); CREATE INDEX idx_paper_positions_user_symbol ON trading.paper_positions(user_id, symbol); CREATE INDEX idx_paper_positions_opened_at ON trading.paper_positions(opened_at DESC); -- Trigger CREATE TRIGGER update_paper_positions_updated_at BEFORE UPDATE ON trading.paper_positions FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- Comentarios COMMENT ON TABLE trading.paper_positions IS 'Posiciones activas e históricas de paper trading'; COMMENT ON COLUMN trading.paper_positions.unrealized_pnl IS 'PnL no realizado (calculado con precio actual)'; COMMENT ON COLUMN trading.paper_positions.realized_pnl IS 'PnL realizado (de trades cerrados)'; ``` ### paper_trades ```sql CREATE TABLE trading.paper_trades ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), user_id UUID NOT NULL REFERENCES public.users(id) ON DELETE CASCADE, order_id UUID NOT NULL REFERENCES trading.paper_orders(id) ON DELETE CASCADE, position_id UUID REFERENCES trading.paper_positions(id) ON DELETE SET NULL, symbol VARCHAR(20) NOT NULL, side trading.order_side_enum NOT NULL, type trading.trade_type_enum NOT NULL, -- Execution details price DECIMAL(20, 8) NOT NULL, quantity DECIMAL(20, 8) NOT NULL, quote_quantity DECIMAL(20, 8) NOT NULL, -- Fees commission DECIMAL(20, 8) DEFAULT 0, commission_asset VARCHAR(10), -- Market context market_price DECIMAL(20, 8), -- Precio de mercado en el momento slippage DECIMAL(20, 8), -- Diferencia vs precio esperado -- Metadatos is_maker BOOLEAN DEFAULT false, executed_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(), CONSTRAINT paper_trades_quantity_check CHECK (quantity > 0), CONSTRAINT paper_trades_price_check CHECK (price > 0) ); -- Índices CREATE INDEX idx_paper_trades_user_id ON trading.paper_trades(user_id); CREATE INDEX idx_paper_trades_order_id ON trading.paper_trades(order_id); CREATE INDEX idx_paper_trades_position_id ON trading.paper_trades(position_id); CREATE INDEX idx_paper_trades_symbol ON trading.paper_trades(symbol); CREATE INDEX idx_paper_trades_executed_at ON trading.paper_trades(executed_at DESC); CREATE INDEX idx_paper_trades_user_executed ON trading.paper_trades(user_id, executed_at DESC); -- Comentarios COMMENT ON TABLE trading.paper_trades IS 'Historial de ejecuciones de trades (fills)'; COMMENT ON COLUMN trading.paper_trades.is_maker IS 'True si la orden fue maker (agregó liquidez)'; COMMENT ON COLUMN trading.paper_trades.slippage IS 'Slippage simulado para realismo'; ``` --- ## Funciones Auxiliares ### Función: update_updated_at_column ```sql CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; ``` ### Función: Inicializar Balance Paper Trading ```sql CREATE OR REPLACE FUNCTION trading.initialize_paper_balance( p_user_id UUID, p_initial_amount DECIMAL DEFAULT 10000.00 ) RETURNS void AS $$ BEGIN INSERT INTO trading.paper_balances (user_id, asset, total, available, locked) VALUES (p_user_id, 'USDT', p_initial_amount, p_initial_amount, 0) ON CONFLICT (user_id, asset) DO NOTHING; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION trading.initialize_paper_balance IS 'Inicializa el balance de paper trading para un usuario nuevo'; ``` ### Función: Calcular PnL de Posición ```sql CREATE OR REPLACE FUNCTION trading.calculate_position_pnl( p_position_id UUID, p_current_price DECIMAL ) RETURNS TABLE( unrealized_pnl DECIMAL, total_pnl DECIMAL, pnl_percentage DECIMAL ) AS $$ DECLARE v_position RECORD; v_unrealized DECIMAL; v_total DECIMAL; v_percentage DECIMAL; BEGIN SELECT * INTO v_position FROM trading.paper_positions WHERE id = p_position_id; IF NOT FOUND THEN RAISE EXCEPTION 'Position not found'; END IF; -- Calcular PnL no realizado IF v_position.side = 'long' THEN v_unrealized := (p_current_price - v_position.average_entry_price) * v_position.current_quantity; ELSE -- short v_unrealized := (v_position.average_entry_price - p_current_price) * v_position.current_quantity; END IF; v_total := v_position.realized_pnl + v_unrealized; v_percentage := (v_total / v_position.entry_value) * 100; RETURN QUERY SELECT v_unrealized, v_total, v_percentage; END; $$ LANGUAGE plpgsql; ``` --- ## Views ### Posiciones Abiertas con PnL ```sql CREATE OR REPLACE VIEW trading.open_positions_with_pnl AS SELECT p.*, COALESCE(t.current_price, p.average_entry_price) as current_price, CASE WHEN p.side = 'long' THEN (COALESCE(t.current_price, p.average_entry_price) - p.average_entry_price) * p.current_quantity ELSE (p.average_entry_price - COALESCE(t.current_price, p.average_entry_price)) * p.current_quantity END as calculated_unrealized_pnl, (p.realized_pnl + CASE WHEN p.side = 'long' THEN (COALESCE(t.current_price, p.average_entry_price) - p.average_entry_price) * p.current_quantity ELSE (p.average_entry_price - COALESCE(t.current_price, p.average_entry_price)) * p.current_quantity END ) as calculated_total_pnl FROM trading.paper_positions p LEFT JOIN LATERAL ( SELECT price as current_price FROM trading.paper_trades WHERE symbol = p.symbol ORDER BY executed_at DESC LIMIT 1 ) t ON true WHERE p.status = 'open'; ``` --- ## Seeders ### Datos Iniciales ```sql -- Watchlist por defecto para usuarios nuevos CREATE OR REPLACE FUNCTION trading.create_default_watchlist() RETURNS TRIGGER AS $$ BEGIN INSERT INTO trading.watchlists (user_id, name, is_default, order_index) VALUES (NEW.id, 'My Watchlist', true, 0); -- Agregar símbolos populares INSERT INTO trading.watchlist_symbols (watchlist_id, symbol, base_asset, quote_asset, order_index) SELECT w.id, s.symbol, s.base_asset, s.quote_asset, s.order_index FROM trading.watchlists w CROSS JOIN ( VALUES ('BTCUSDT', 'BTC', 'USDT', 0), ('ETHUSDT', 'ETH', 'USDT', 1), ('BNBUSDT', 'BNB', 'USDT', 2) ) s(symbol, base_asset, quote_asset, order_index) WHERE w.user_id = NEW.id AND w.is_default = true; -- Inicializar balance de paper trading PERFORM trading.initialize_paper_balance(NEW.id, 10000.00); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER create_user_default_watchlist AFTER INSERT ON public.users FOR EACH ROW EXECUTE FUNCTION trading.create_default_watchlist(); ``` --- ## Migraciones ```sql -- Migration: 001_create_trading_schema.sql BEGIN; -- Crear schema y extensiones necesarias CREATE SCHEMA IF NOT EXISTS trading; CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; -- Crear ENUMs -- (código de ENUMs aquí) -- Crear tablas -- (código de tablas aquí) -- Crear funciones -- (código de funciones aquí) -- Crear views -- (código de views aquí) -- Crear triggers -- (código de triggers aquí) COMMIT; ``` --- ## Interfaces TypeScript ```typescript // types/trading.types.ts export type OrderSide = 'buy' | 'sell'; export type OrderType = 'market' | 'limit' | 'stop_loss' | 'stop_limit' | 'take_profit'; export type OrderStatus = 'pending' | 'open' | 'filled' | 'cancelled' | 'rejected' | 'expired'; export type PositionSide = 'long' | 'short'; export type PositionStatus = 'open' | 'closed'; export type TradeType = 'entry' | 'exit' | 'partial'; export interface Watchlist { id: string; userId: string; name: string; description?: string; color?: string; isDefault: boolean; orderIndex: number; createdAt: Date; updatedAt: Date; } export interface WatchlistSymbol { id: string; watchlistId: string; symbol: string; baseAsset: string; quoteAsset: string; notes?: string; alertPriceHigh?: number; alertPriceLow?: number; orderIndex: number; addedAt: Date; updatedAt: Date; } export interface PaperBalance { id: string; userId: string; asset: string; total: number; available: number; locked: number; createdAt: Date; updatedAt: Date; } export interface PaperOrder { id: string; userId: string; symbol: string; side: OrderSide; type: OrderType; status: OrderStatus; quantity: number; filledQuantity: number; remainingQuantity: number; price?: number; stopPrice?: number; averageFillPrice?: number; quoteQuantity?: number; filledQuoteQuantity: number; commission: number; commissionAsset?: string; timeInForce: string; clientOrderId?: string; notes?: string; placedAt: Date; filledAt?: Date; cancelledAt?: Date; expiresAt?: Date; createdAt: Date; updatedAt: Date; } export interface PaperPosition { id: string; userId: string; symbol: string; side: PositionSide; status: PositionStatus; entryPrice: number; entryQuantity: number; entryValue: number; entryOrderId?: string; exitPrice?: number; exitQuantity?: number; exitValue?: number; exitOrderId?: string; currentQuantity: number; averageEntryPrice: number; realizedPnl: number; unrealizedPnl: number; totalPnl: number; pnlPercentage: number; totalCommission: number; stopLossPrice?: number; takeProfitPrice?: number; openedAt: Date; closedAt?: Date; createdAt: Date; updatedAt: Date; } export interface PaperTrade { id: string; userId: string; orderId: string; positionId?: string; symbol: string; side: OrderSide; type: TradeType; price: number; quantity: number; quoteQuantity: number; commission: number; commissionAsset?: string; marketPrice?: number; slippage?: number; isMaker: boolean; executedAt: Date; createdAt: Date; } ``` --- ## Testing ```sql -- Test: Crear watchlist y símbolos DO $$ DECLARE v_user_id UUID; v_watchlist_id UUID; BEGIN -- Crear usuario de prueba INSERT INTO public.users (email, first_name, last_name) VALUES ('test@example.com', 'Test', 'User') RETURNING id INTO v_user_id; -- Verificar watchlist creada automáticamente SELECT id INTO v_watchlist_id FROM trading.watchlists WHERE user_id = v_user_id AND is_default = true; ASSERT v_watchlist_id IS NOT NULL, 'Default watchlist not created'; -- Verificar balance inicial ASSERT EXISTS ( SELECT 1 FROM trading.paper_balances WHERE user_id = v_user_id AND asset = 'USDT' AND total = 10000.00 ), 'Initial balance not created'; RAISE NOTICE 'Tests passed!'; END $$; ``` --- ## Referencias - [PostgreSQL 15 Documentation](https://www.postgresql.org/docs/15/) - [PostgreSQL ENUM Types](https://www.postgresql.org/docs/current/datatype-enum.html) - [PostgreSQL Triggers](https://www.postgresql.org/docs/current/triggers.html)