Changes include: - Updated architecture documentation - Enhanced module definitions (OQI-001 to OQI-008) - ML integration documentation updates - Trading strategies documentation - Orchestration and inventory updates - Docker configuration updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
28 KiB
28 KiB
| id | title | type | status | priority | epic | project | version | created_date | updated_date |
|---|---|---|---|---|---|---|---|---|---|
| ET-TRD-003 | Especificación Técnica - Database Schema | Technical Specification | Done | Alta | OQI-003 | trading-platform | 1.0.0 | 2025-12-05 | 2026-01-04 |
ET-TRD-003: Especificación Técnica - Database Schema
Version: 1.0.0 Fecha: 2025-12-05 Estado: Pendiente Épica: OQI-003 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
-- 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
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
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
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
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
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
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
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
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
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
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
-- 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
-- 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
// 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
-- 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 $$;