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>
806 lines
28 KiB
Markdown
806 lines
28 KiB
Markdown
---
|
|
id: "ET-TRD-003"
|
|
title: "Especificación Técnica - Database Schema"
|
|
type: "Technical Specification"
|
|
status: "Done"
|
|
priority: "Alta"
|
|
epic: "OQI-003"
|
|
project: "trading-platform"
|
|
version: "1.0.0"
|
|
created_date: "2025-12-05"
|
|
updated_date: "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](../_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)
|