trading-platform-database-v2/ddl/schemas/trading/tables/008_paper_trading.sql

465 lines
16 KiB
PL/PgSQL

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