465 lines
16 KiB
PL/PgSQL
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;
|