---
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)
