[TRADING-DB] feat: Add trading schema DDL
This commit is contained in:
parent
cd6590ec25
commit
29dfb82d6c
229
ddl/schemas/trading/tables/001_symbols.sql
Normal file
229
ddl/schemas/trading/tables/001_symbols.sql
Normal file
@ -0,0 +1,229 @@
|
||||
-- ============================================================================
|
||||
-- SCHEMA: trading
|
||||
-- TABLE: symbols
|
||||
-- DESCRIPTION: Simbolos/instrumentos habilitados para trading por tenant
|
||||
-- VERSION: 1.0.0
|
||||
-- CREATED: 2026-01-16
|
||||
-- SPRINT: Sprint 3 - DDL Implementation Roadmap Q1-2026
|
||||
-- ============================================================================
|
||||
|
||||
-- Crear schema si no existe
|
||||
CREATE SCHEMA IF NOT EXISTS trading;
|
||||
|
||||
-- Grant usage
|
||||
GRANT USAGE ON SCHEMA trading TO trading_app;
|
||||
GRANT USAGE ON SCHEMA trading TO trading_readonly;
|
||||
|
||||
-- ============================================================================
|
||||
-- ENUMS COMPARTIDOS DEL SCHEMA TRADING
|
||||
-- ============================================================================
|
||||
|
||||
-- Enum para direccion de trade
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE trading.trade_direction AS ENUM (
|
||||
'long', -- Compra / Buy
|
||||
'short' -- Venta / Sell
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para estado de posicion
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE trading.position_status AS ENUM (
|
||||
'pending', -- Pendiente de ejecucion
|
||||
'open', -- Posicion abierta
|
||||
'closed', -- Cerrada manualmente
|
||||
'stopped', -- Cerrada por stop loss
|
||||
'target_hit', -- Cerrada por take profit
|
||||
'expired', -- Expirada
|
||||
'cancelled' -- Cancelada antes de ejecutar
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para tipo de orden
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE trading.order_type AS ENUM (
|
||||
'market', -- Orden de mercado
|
||||
'limit', -- Orden limitada
|
||||
'stop', -- Stop order
|
||||
'stop_limit' -- Stop limit order
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para timeframe
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE trading.timeframe AS ENUM (
|
||||
'M1', 'M5', 'M15', 'M30', -- Minutos
|
||||
'H1', 'H4', -- Horas
|
||||
'D1', 'W1', 'MN' -- Dia, Semana, Mes
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- TABLA: symbols
|
||||
-- Simbolos habilitados por tenant (copia de market_data.tickers con config tenant)
|
||||
-- ============================================================================
|
||||
|
||||
CREATE TABLE IF NOT EXISTS trading.symbols (
|
||||
-- Identificadores
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
||||
ticker_id UUID NOT NULL REFERENCES market_data.tickers(id),
|
||||
|
||||
-- Datos del simbolo (denormalizados para performance)
|
||||
symbol VARCHAR(20) NOT NULL,
|
||||
name VARCHAR(200),
|
||||
type market_data.instrument_type,
|
||||
|
||||
-- Configuracion por tenant
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
is_tradeable BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
is_visible BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
-- Limites especificos del tenant
|
||||
min_lot_size DECIMAL(10, 4),
|
||||
max_lot_size DECIMAL(10, 4),
|
||||
max_position_size DECIMAL(15, 2), -- Tamaño maximo de posicion en USD
|
||||
max_daily_volume DECIMAL(15, 2), -- Volumen diario maximo
|
||||
|
||||
-- Spread markup (si el tenant agrega spread)
|
||||
spread_markup_pips DECIMAL(10, 4) DEFAULT 0,
|
||||
|
||||
-- Comisiones del tenant
|
||||
commission_per_lot DECIMAL(10, 4) DEFAULT 0,
|
||||
commission_percent DECIMAL(5, 4) DEFAULT 0,
|
||||
|
||||
-- Margen requerido (override del default)
|
||||
margin_required_percent DECIMAL(5, 2),
|
||||
max_leverage INTEGER,
|
||||
|
||||
-- Trading hours override
|
||||
custom_trading_hours JSONB,
|
||||
|
||||
-- Categorias/tags del tenant
|
||||
categories VARCHAR(50)[],
|
||||
tags VARCHAR(50)[],
|
||||
display_order INTEGER DEFAULT 999,
|
||||
|
||||
-- Estadisticas del tenant
|
||||
trade_count INTEGER NOT NULL DEFAULT 0,
|
||||
total_volume DECIMAL(20, 4) NOT NULL DEFAULT 0,
|
||||
last_trade_at TIMESTAMPTZ,
|
||||
|
||||
-- Metadata
|
||||
metadata JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT symbols_unique_per_tenant UNIQUE (tenant_id, ticker_id),
|
||||
CONSTRAINT symbols_unique_symbol_per_tenant UNIQUE (tenant_id, symbol)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE trading.symbols IS
|
||||
'Simbolos habilitados para trading por tenant con configuracion personalizada';
|
||||
|
||||
COMMENT ON COLUMN trading.symbols.spread_markup_pips IS
|
||||
'Pips adicionales de spread que el tenant agrega al precio del broker';
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_symbols_tenant
|
||||
ON trading.symbols(tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_symbols_ticker
|
||||
ON trading.symbols(ticker_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_symbols_symbol
|
||||
ON trading.symbols(tenant_id, symbol);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_symbols_enabled
|
||||
ON trading.symbols(tenant_id, is_enabled, is_tradeable)
|
||||
WHERE is_enabled = TRUE AND is_tradeable = TRUE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_symbols_type
|
||||
ON trading.symbols(tenant_id, type);
|
||||
|
||||
-- GIN index para categorias
|
||||
CREATE INDEX IF NOT EXISTS idx_symbols_categories_gin
|
||||
ON trading.symbols USING GIN (categories);
|
||||
|
||||
-- Trigger para updated_at
|
||||
CREATE OR REPLACE FUNCTION trading.update_trading_timestamp()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
NEW.updated_at = NOW();
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS symbol_updated_at ON trading.symbols;
|
||||
CREATE TRIGGER symbol_updated_at
|
||||
BEFORE UPDATE ON trading.symbols
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.update_trading_timestamp();
|
||||
|
||||
-- Trigger para sincronizar datos del ticker
|
||||
CREATE OR REPLACE FUNCTION trading.sync_symbol_from_ticker()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
SELECT symbol, name, type
|
||||
INTO NEW.symbol, NEW.name, NEW.type
|
||||
FROM market_data.tickers
|
||||
WHERE id = NEW.ticker_id;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS symbol_sync ON trading.symbols;
|
||||
CREATE TRIGGER symbol_sync
|
||||
BEFORE INSERT OR UPDATE OF ticker_id ON trading.symbols
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.sync_symbol_from_ticker();
|
||||
|
||||
-- Vista de simbolos activos
|
||||
CREATE OR REPLACE VIEW trading.v_active_symbols AS
|
||||
SELECT
|
||||
s.id,
|
||||
s.tenant_id,
|
||||
s.ticker_id,
|
||||
s.symbol,
|
||||
s.name,
|
||||
s.type,
|
||||
t.pip_size,
|
||||
t.typical_spread_pips,
|
||||
s.spread_markup_pips,
|
||||
(COALESCE(t.typical_spread_pips, 0) + COALESCE(s.spread_markup_pips, 0)) AS total_spread_pips,
|
||||
t.current_bid,
|
||||
t.current_ask,
|
||||
s.min_lot_size,
|
||||
s.max_lot_size,
|
||||
COALESCE(s.max_leverage, t.max_leverage) AS max_leverage,
|
||||
s.display_order
|
||||
FROM trading.symbols s
|
||||
JOIN market_data.tickers t ON s.ticker_id = t.id
|
||||
WHERE s.is_enabled = TRUE
|
||||
AND s.is_tradeable = TRUE
|
||||
AND t.status = 'active'
|
||||
ORDER BY s.display_order, s.symbol;
|
||||
|
||||
-- RLS Policy para multi-tenancy
|
||||
ALTER TABLE trading.symbols ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY symbols_tenant_isolation ON trading.symbols
|
||||
FOR ALL
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||||
|
||||
-- Grants
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON trading.symbols TO trading_app;
|
||||
GRANT SELECT ON trading.symbols TO trading_readonly;
|
||||
GRANT SELECT ON trading.v_active_symbols TO trading_app;
|
||||
249
ddl/schemas/trading/tables/002_strategies.sql
Normal file
249
ddl/schemas/trading/tables/002_strategies.sql
Normal file
@ -0,0 +1,249 @@
|
||||
-- ============================================================================
|
||||
-- SCHEMA: trading
|
||||
-- TABLE: strategies
|
||||
-- DESCRIPTION: Estrategias de trading definidas
|
||||
-- VERSION: 1.0.0
|
||||
-- CREATED: 2026-01-16
|
||||
-- SPRINT: Sprint 3 - DDL Implementation Roadmap Q1-2026
|
||||
-- ============================================================================
|
||||
|
||||
-- Enum para tipo de estrategia
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE trading.strategy_type AS ENUM (
|
||||
'manual', -- Estrategia manual (usuario decide)
|
||||
'semi_auto', -- Semi-automatica (senales con confirmacion)
|
||||
'fully_auto', -- Totalmente automatizada
|
||||
'copy_trading' -- Copy trading de otro usuario/bot
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para metodologia de trading
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE trading.trading_methodology AS ENUM (
|
||||
'trend_following', -- Seguimiento de tendencia
|
||||
'mean_reversion', -- Reversion a la media
|
||||
'breakout', -- Rupturas
|
||||
'scalping', -- Scalping
|
||||
'swing', -- Swing trading
|
||||
'position', -- Position trading
|
||||
'arbitrage', -- Arbitraje
|
||||
'news_based', -- Basado en noticias
|
||||
'smc_ict', -- Smart Money Concepts / ICT
|
||||
'harmonic', -- Patrones armonicos
|
||||
'custom' -- Personalizada
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Tabla de Estrategias
|
||||
CREATE TABLE IF NOT EXISTS trading.strategies (
|
||||
-- Identificadores
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
||||
created_by UUID REFERENCES users.users(id),
|
||||
|
||||
-- Informacion basica
|
||||
name VARCHAR(100) NOT NULL,
|
||||
slug VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
short_description VARCHAR(500),
|
||||
|
||||
-- Clasificacion
|
||||
type trading.strategy_type NOT NULL DEFAULT 'manual',
|
||||
methodology trading.trading_methodology NOT NULL DEFAULT 'custom',
|
||||
|
||||
-- Configuracion de riesgo
|
||||
risk_config JSONB NOT NULL DEFAULT '{
|
||||
"risk_per_trade_percent": 2,
|
||||
"max_daily_loss_percent": 5,
|
||||
"max_drawdown_percent": 20,
|
||||
"max_concurrent_trades": 3,
|
||||
"risk_reward_min": 1.5
|
||||
}'::JSONB,
|
||||
|
||||
-- Configuracion de entrada
|
||||
entry_config JSONB NOT NULL DEFAULT '{
|
||||
"order_type": "market",
|
||||
"slippage_pips": 2,
|
||||
"max_spread_pips": 5,
|
||||
"entry_confirmation_required": false
|
||||
}'::JSONB,
|
||||
|
||||
-- Configuracion de salida
|
||||
exit_config JSONB NOT NULL DEFAULT '{
|
||||
"use_trailing_stop": false,
|
||||
"trailing_stop_pips": 20,
|
||||
"partial_close_enabled": false,
|
||||
"partial_close_percent": 50,
|
||||
"breakeven_enabled": false,
|
||||
"breakeven_pips": 10
|
||||
}'::JSONB,
|
||||
|
||||
-- Simbolos y timeframes
|
||||
allowed_symbols VARCHAR(20)[], -- NULL = todos
|
||||
allowed_timeframes trading.timeframe[],
|
||||
primary_timeframe trading.timeframe DEFAULT 'H1',
|
||||
|
||||
-- Horarios de trading
|
||||
trading_hours JSONB DEFAULT '{
|
||||
"enabled": false,
|
||||
"sessions": ["london", "new_york"],
|
||||
"avoid_news": true,
|
||||
"news_buffer_minutes": 30
|
||||
}'::JSONB,
|
||||
|
||||
-- Indicadores utilizados
|
||||
indicators_config JSONB DEFAULT '[]'::JSONB,
|
||||
|
||||
-- Reglas de entrada (para estrategias automaticas)
|
||||
entry_rules JSONB DEFAULT '[]'::JSONB,
|
||||
|
||||
-- Reglas de salida
|
||||
exit_rules JSONB DEFAULT '[]'::JSONB,
|
||||
|
||||
-- Estado
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
is_public BOOLEAN NOT NULL DEFAULT FALSE, -- Visible para otros usuarios
|
||||
is_premium BOOLEAN NOT NULL DEFAULT FALSE, -- Requiere suscripcion
|
||||
|
||||
-- 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,
|
||||
average_profit DECIMAL(15, 4) DEFAULT 0,
|
||||
average_loss DECIMAL(15, 4) DEFAULT 0,
|
||||
max_drawdown DECIMAL(15, 4) DEFAULT 0,
|
||||
total_profit_loss DECIMAL(15, 4) DEFAULT 0,
|
||||
|
||||
-- Backtesting
|
||||
last_backtest_at TIMESTAMPTZ,
|
||||
backtest_results JSONB,
|
||||
|
||||
-- Version control
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
previous_version_id UUID,
|
||||
|
||||
-- Metadata
|
||||
tags VARCHAR(50)[],
|
||||
metadata JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT strategies_unique_slug UNIQUE (tenant_id, slug),
|
||||
CONSTRAINT strategies_win_rate_check CHECK (win_rate BETWEEN 0 AND 100)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE trading.strategies IS
|
||||
'Estrategias de trading con configuracion de riesgo, entrada y salida';
|
||||
|
||||
COMMENT ON COLUMN trading.strategies.risk_config IS
|
||||
'Configuracion de gestion de riesgo de la estrategia';
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_strategies_tenant
|
||||
ON trading.strategies(tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_strategies_creator
|
||||
ON trading.strategies(created_by);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_strategies_type
|
||||
ON trading.strategies(type);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_strategies_methodology
|
||||
ON trading.strategies(methodology);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_strategies_active
|
||||
ON trading.strategies(tenant_id, is_active)
|
||||
WHERE is_active = TRUE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_strategies_public
|
||||
ON trading.strategies(is_public, win_rate DESC)
|
||||
WHERE is_public = TRUE;
|
||||
|
||||
-- GIN index para tags
|
||||
CREATE INDEX IF NOT EXISTS idx_strategies_tags_gin
|
||||
ON trading.strategies USING GIN (tags);
|
||||
|
||||
-- Trigger para updated_at
|
||||
DROP TRIGGER IF EXISTS strategy_updated_at ON trading.strategies;
|
||||
CREATE TRIGGER strategy_updated_at
|
||||
BEFORE UPDATE ON trading.strategies
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.update_trading_timestamp();
|
||||
|
||||
-- Funcion para calcular estadisticas de estrategia
|
||||
CREATE OR REPLACE FUNCTION trading.recalculate_strategy_stats(p_strategy_id UUID)
|
||||
RETURNS VOID AS $$
|
||||
DECLARE
|
||||
v_stats RECORD;
|
||||
BEGIN
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
COUNT(*) FILTER (WHERE profit_loss > 0) AS wins,
|
||||
COUNT(*) FILTER (WHERE profit_loss < 0) AS losses,
|
||||
COALESCE(AVG(profit_loss) FILTER (WHERE profit_loss > 0), 0) AS avg_profit,
|
||||
COALESCE(AVG(ABS(profit_loss)) FILTER (WHERE profit_loss < 0), 0) AS avg_loss,
|
||||
COALESCE(SUM(profit_loss), 0) AS total_pnl
|
||||
INTO v_stats
|
||||
FROM trading.positions
|
||||
WHERE strategy_id = p_strategy_id
|
||||
AND status IN ('closed', 'stopped', 'target_hit');
|
||||
|
||||
UPDATE trading.strategies
|
||||
SET total_trades = v_stats.total,
|
||||
winning_trades = v_stats.wins,
|
||||
losing_trades = v_stats.losses,
|
||||
win_rate = CASE WHEN v_stats.total > 0
|
||||
THEN (v_stats.wins::DECIMAL / v_stats.total * 100)
|
||||
ELSE 0 END,
|
||||
profit_factor = CASE WHEN v_stats.avg_loss > 0
|
||||
THEN v_stats.avg_profit / v_stats.avg_loss
|
||||
ELSE 0 END,
|
||||
average_profit = v_stats.avg_profit,
|
||||
average_loss = v_stats.avg_loss,
|
||||
total_profit_loss = v_stats.total_pnl
|
||||
WHERE id = p_strategy_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Vista de estrategias publicas
|
||||
CREATE OR REPLACE VIEW trading.v_public_strategies AS
|
||||
SELECT
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
type,
|
||||
methodology,
|
||||
primary_timeframe,
|
||||
total_trades,
|
||||
win_rate,
|
||||
profit_factor,
|
||||
max_drawdown,
|
||||
is_premium,
|
||||
created_at
|
||||
FROM trading.strategies
|
||||
WHERE is_public = TRUE
|
||||
AND is_active = TRUE
|
||||
AND total_trades >= 10 -- Minimo de trades para mostrar
|
||||
ORDER BY win_rate DESC, profit_factor DESC;
|
||||
|
||||
-- RLS Policy para multi-tenancy
|
||||
ALTER TABLE trading.strategies ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY strategies_tenant_isolation ON trading.strategies
|
||||
FOR ALL
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||||
|
||||
-- Grants
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON trading.strategies TO trading_app;
|
||||
GRANT SELECT ON trading.strategies TO trading_readonly;
|
||||
GRANT SELECT ON trading.v_public_strategies TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION trading.recalculate_strategy_stats TO trading_app;
|
||||
328
ddl/schemas/trading/tables/003_bots.sql
Normal file
328
ddl/schemas/trading/tables/003_bots.sql
Normal file
@ -0,0 +1,328 @@
|
||||
-- ============================================================================
|
||||
-- SCHEMA: trading
|
||||
-- TABLE: bots
|
||||
-- DESCRIPTION: Agentes de trading automatizado (Atlas, Orion, Nova)
|
||||
-- VERSION: 1.0.0
|
||||
-- CREATED: 2026-01-16
|
||||
-- SPRINT: Sprint 3 - DDL Implementation Roadmap Q1-2026
|
||||
-- ============================================================================
|
||||
|
||||
-- Enum para estado del bot
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE trading.bot_status AS ENUM (
|
||||
'inactive', -- Inactivo
|
||||
'starting', -- Iniciando
|
||||
'running', -- Ejecutando
|
||||
'paused', -- Pausado temporalmente
|
||||
'stopping', -- Deteniendo
|
||||
'error', -- Error
|
||||
'maintenance' -- En mantenimiento
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para nivel de riesgo del bot
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE trading.bot_risk_level AS ENUM (
|
||||
'conservative', -- Conservador (bajo riesgo)
|
||||
'moderate', -- Moderado
|
||||
'aggressive', -- Agresivo
|
||||
'ultra_aggressive' -- Ultra agresivo
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Tabla de Bots de Trading
|
||||
CREATE TABLE IF NOT EXISTS trading.bots (
|
||||
-- Identificadores
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
||||
strategy_id UUID REFERENCES trading.strategies(id),
|
||||
|
||||
-- Identificacion
|
||||
code VARCHAR(20) NOT NULL, -- 'ATLAS', 'ORION', 'NOVA'
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
avatar_url TEXT,
|
||||
|
||||
-- Tipo y version
|
||||
version VARCHAR(20) NOT NULL DEFAULT '1.0.0',
|
||||
risk_level trading.bot_risk_level NOT NULL DEFAULT 'moderate',
|
||||
|
||||
-- Estado
|
||||
status trading.bot_status NOT NULL DEFAULT 'inactive',
|
||||
status_message TEXT,
|
||||
last_status_change TIMESTAMPTZ DEFAULT NOW(),
|
||||
|
||||
-- Conexion MT4/MT5
|
||||
mt_account_id VARCHAR(50),
|
||||
mt_server VARCHAR(100),
|
||||
mt_connected BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
mt_last_heartbeat TIMESTAMPTZ,
|
||||
|
||||
-- Configuracion de trading
|
||||
trading_config JSONB NOT NULL DEFAULT '{
|
||||
"enabled": false,
|
||||
"symbols": ["EURUSD", "GBPUSD", "XAUUSD"],
|
||||
"timeframes": ["H1", "H4"],
|
||||
"max_trades_per_day": 5,
|
||||
"max_concurrent_positions": 3,
|
||||
"trading_hours": {
|
||||
"enabled": false,
|
||||
"start": "08:00",
|
||||
"end": "20:00",
|
||||
"timezone": "America/New_York"
|
||||
}
|
||||
}'::JSONB,
|
||||
|
||||
-- Configuracion de riesgo
|
||||
risk_config JSONB NOT NULL DEFAULT '{
|
||||
"capital_allocation": 10000,
|
||||
"risk_per_trade_percent": 1,
|
||||
"max_daily_loss_percent": 3,
|
||||
"max_drawdown_percent": 10,
|
||||
"lot_size_mode": "fixed",
|
||||
"fixed_lot_size": 0.1,
|
||||
"use_compounding": false
|
||||
}'::JSONB,
|
||||
|
||||
-- Configuracion de senales
|
||||
signal_config JSONB NOT NULL DEFAULT '{
|
||||
"min_confidence": 70,
|
||||
"require_confirmation": true,
|
||||
"confirmation_timeframe": "M15",
|
||||
"filter_by_trend": true,
|
||||
"filter_by_volatility": true
|
||||
}'::JSONB,
|
||||
|
||||
-- Capital y rendimiento
|
||||
initial_capital DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
||||
current_capital DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
||||
allocated_capital DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
||||
available_margin DECIMAL(15, 2) NOT NULL DEFAULT 0,
|
||||
|
||||
-- Estadisticas de rendimiento
|
||||
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,
|
||||
sharpe_ratio DECIMAL(10, 4) DEFAULT 0,
|
||||
max_drawdown DECIMAL(15, 4) DEFAULT 0,
|
||||
max_drawdown_percent DECIMAL(5, 2) DEFAULT 0,
|
||||
|
||||
-- P&L
|
||||
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,
|
||||
daily_pnl DECIMAL(15, 4) NOT NULL DEFAULT 0,
|
||||
weekly_pnl DECIMAL(15, 4) NOT NULL DEFAULT 0,
|
||||
monthly_pnl DECIMAL(15, 4) NOT NULL DEFAULT 0,
|
||||
|
||||
-- Rendimiento porcentual
|
||||
total_return_percent DECIMAL(10, 4) DEFAULT 0,
|
||||
daily_return_percent DECIMAL(10, 4) DEFAULT 0,
|
||||
weekly_return_percent DECIMAL(10, 4) DEFAULT 0,
|
||||
monthly_return_percent DECIMAL(10, 4) DEFAULT 0,
|
||||
|
||||
-- Posiciones actuales
|
||||
open_positions_count INTEGER NOT NULL DEFAULT 0,
|
||||
open_positions_value DECIMAL(15, 4) NOT NULL DEFAULT 0,
|
||||
unrealized_pnl DECIMAL(15, 4) NOT NULL DEFAULT 0,
|
||||
|
||||
-- Ultima actividad
|
||||
last_trade_at TIMESTAMPTZ,
|
||||
last_signal_at TIMESTAMPTZ,
|
||||
last_error_at TIMESTAMPTZ,
|
||||
last_error_message TEXT,
|
||||
|
||||
-- Subscribers (usuarios que invierten en este bot)
|
||||
subscriber_count INTEGER NOT NULL DEFAULT 0,
|
||||
total_aum DECIMAL(15, 2) NOT NULL DEFAULT 0, -- Assets Under Management
|
||||
|
||||
-- Disponibilidad
|
||||
is_public BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_premium BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
min_investment DECIMAL(15, 2) DEFAULT 100,
|
||||
max_investment DECIMAL(15, 2),
|
||||
|
||||
-- Comisiones
|
||||
performance_fee_percent DECIMAL(5, 2) DEFAULT 20, -- Fee sobre ganancias
|
||||
management_fee_percent DECIMAL(5, 2) DEFAULT 0, -- Fee anual sobre AUM
|
||||
|
||||
-- Metadata
|
||||
tags VARCHAR(50)[],
|
||||
metadata JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Timestamps
|
||||
started_at TIMESTAMPTZ,
|
||||
stopped_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT bots_unique_code UNIQUE (tenant_id, code),
|
||||
CONSTRAINT bots_capital_check CHECK (current_capital >= 0),
|
||||
CONSTRAINT bots_win_rate_check CHECK (win_rate BETWEEN 0 AND 100)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE trading.bots IS
|
||||
'Agentes de trading automatizado con configuracion y estadisticas';
|
||||
|
||||
COMMENT ON COLUMN trading.bots.code IS
|
||||
'Codigo unico del bot: ATLAS (conservador), ORION (moderado), NOVA (agresivo)';
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_bots_tenant
|
||||
ON trading.bots(tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_bots_code
|
||||
ON trading.bots(tenant_id, code);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_bots_status
|
||||
ON trading.bots(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_bots_running
|
||||
ON trading.bots(tenant_id, status)
|
||||
WHERE status = 'running';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_bots_public
|
||||
ON trading.bots(is_public, net_profit DESC)
|
||||
WHERE is_public = TRUE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_bots_performance
|
||||
ON trading.bots(tenant_id, total_return_percent DESC);
|
||||
|
||||
-- Trigger para updated_at
|
||||
DROP TRIGGER IF EXISTS bot_updated_at ON trading.bots;
|
||||
CREATE TRIGGER bot_updated_at
|
||||
BEFORE UPDATE ON trading.bots
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.update_trading_timestamp();
|
||||
|
||||
-- Trigger para registrar cambios de estado
|
||||
CREATE OR REPLACE FUNCTION trading.log_bot_status_change()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.status IS DISTINCT FROM OLD.status THEN
|
||||
NEW.last_status_change := NOW();
|
||||
|
||||
IF NEW.status = 'running' AND OLD.status != 'running' THEN
|
||||
NEW.started_at := NOW();
|
||||
ELSIF NEW.status IN ('inactive', 'stopped') AND OLD.status = 'running' THEN
|
||||
NEW.stopped_at := NOW();
|
||||
ELSIF NEW.status = 'error' THEN
|
||||
NEW.last_error_at := NOW();
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS bot_status_change ON trading.bots;
|
||||
CREATE TRIGGER bot_status_change
|
||||
BEFORE UPDATE OF status ON trading.bots
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.log_bot_status_change();
|
||||
|
||||
-- Funcion para actualizar estadisticas del bot
|
||||
CREATE OR REPLACE FUNCTION trading.recalculate_bot_stats(p_bot_id UUID)
|
||||
RETURNS VOID AS $$
|
||||
DECLARE
|
||||
v_stats RECORD;
|
||||
BEGIN
|
||||
SELECT
|
||||
COUNT(*) AS total,
|
||||
COUNT(*) FILTER (WHERE profit_loss > 0) AS wins,
|
||||
COUNT(*) FILTER (WHERE profit_loss < 0) AS losses,
|
||||
COALESCE(SUM(profit_loss) FILTER (WHERE profit_loss > 0), 0) AS total_profit,
|
||||
COALESCE(SUM(ABS(profit_loss)) FILTER (WHERE profit_loss < 0), 0) AS total_loss,
|
||||
COALESCE(SUM(profit_loss), 0) AS net_pnl
|
||||
INTO v_stats
|
||||
FROM trading.positions
|
||||
WHERE bot_id = p_bot_id
|
||||
AND status IN ('closed', 'stopped', 'target_hit');
|
||||
|
||||
UPDATE trading.bots
|
||||
SET total_trades = v_stats.total,
|
||||
winning_trades = v_stats.wins,
|
||||
losing_trades = v_stats.losses,
|
||||
win_rate = CASE WHEN v_stats.total > 0
|
||||
THEN (v_stats.wins::DECIMAL / v_stats.total * 100)
|
||||
ELSE 0 END,
|
||||
profit_factor = CASE WHEN v_stats.total_loss > 0
|
||||
THEN v_stats.total_profit / v_stats.total_loss
|
||||
ELSE 0 END,
|
||||
total_profit = v_stats.total_profit,
|
||||
total_loss = v_stats.total_loss,
|
||||
net_profit = v_stats.net_pnl,
|
||||
total_return_percent = CASE WHEN initial_capital > 0
|
||||
THEN (v_stats.net_pnl / initial_capital * 100)
|
||||
ELSE 0 END
|
||||
WHERE id = p_bot_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Vista de bots activos
|
||||
CREATE OR REPLACE VIEW trading.v_active_bots AS
|
||||
SELECT
|
||||
id,
|
||||
tenant_id,
|
||||
code,
|
||||
name,
|
||||
description,
|
||||
avatar_url,
|
||||
risk_level,
|
||||
status,
|
||||
win_rate,
|
||||
profit_factor,
|
||||
total_return_percent,
|
||||
max_drawdown_percent,
|
||||
subscriber_count,
|
||||
total_aum,
|
||||
is_premium,
|
||||
min_investment
|
||||
FROM trading.bots
|
||||
WHERE is_public = TRUE
|
||||
AND status IN ('running', 'paused')
|
||||
ORDER BY total_return_percent DESC;
|
||||
|
||||
-- Vista de rendimiento de bots
|
||||
CREATE OR REPLACE VIEW trading.v_bot_performance AS
|
||||
SELECT
|
||||
b.id,
|
||||
b.code,
|
||||
b.name,
|
||||
b.status,
|
||||
b.total_trades,
|
||||
b.win_rate,
|
||||
b.profit_factor,
|
||||
b.sharpe_ratio,
|
||||
b.net_profit,
|
||||
b.total_return_percent,
|
||||
b.daily_return_percent,
|
||||
b.weekly_return_percent,
|
||||
b.monthly_return_percent,
|
||||
b.max_drawdown_percent,
|
||||
b.open_positions_count,
|
||||
b.unrealized_pnl,
|
||||
b.last_trade_at
|
||||
FROM trading.bots b
|
||||
ORDER BY b.tenant_id, b.total_return_percent DESC;
|
||||
|
||||
-- RLS Policy para multi-tenancy
|
||||
ALTER TABLE trading.bots ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY bots_tenant_isolation ON trading.bots
|
||||
FOR ALL
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||||
|
||||
-- Grants
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON trading.bots TO trading_app;
|
||||
GRANT SELECT ON trading.bots TO trading_readonly;
|
||||
GRANT SELECT ON trading.v_active_bots TO trading_app;
|
||||
GRANT SELECT ON trading.v_bot_performance TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION trading.recalculate_bot_stats TO trading_app;
|
||||
323
ddl/schemas/trading/tables/004_signals.sql
Normal file
323
ddl/schemas/trading/tables/004_signals.sql
Normal file
@ -0,0 +1,323 @@
|
||||
-- ============================================================================
|
||||
-- SCHEMA: trading
|
||||
-- TABLE: signals
|
||||
-- DESCRIPTION: Senales de trading generadas por ML/bots
|
||||
-- VERSION: 1.0.0
|
||||
-- CREATED: 2026-01-16
|
||||
-- SPRINT: Sprint 3 - DDL Implementation Roadmap Q1-2026
|
||||
-- ============================================================================
|
||||
|
||||
-- Enum para tipo de senal
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE trading.signal_type AS ENUM (
|
||||
'entry', -- Senal de entrada
|
||||
'exit', -- Senal de salida
|
||||
'scale_in', -- Agregar a posicion
|
||||
'scale_out', -- Reducir posicion
|
||||
'move_sl', -- Mover stop loss
|
||||
'move_tp', -- Mover take profit
|
||||
'alert' -- Alerta informativa
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para estado de senal
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE trading.signal_status AS ENUM (
|
||||
'pending', -- Pendiente de validacion
|
||||
'active', -- Activa y disponible
|
||||
'executed', -- Ejecutada
|
||||
'expired', -- Expirada sin ejecutar
|
||||
'cancelled', -- Cancelada
|
||||
'invalidated' -- Invalidada por condiciones
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para fuente de senal
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE trading.signal_source AS ENUM (
|
||||
'ml_model', -- Modelo ML
|
||||
'technical', -- Analisis tecnico
|
||||
'fundamental', -- Analisis fundamental
|
||||
'sentiment', -- Analisis de sentimiento
|
||||
'manual', -- Analista manual
|
||||
'bot', -- Bot de trading
|
||||
'copy_trade' -- Copy trading
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Tabla de Senales de Trading
|
||||
CREATE TABLE IF NOT EXISTS trading.signals (
|
||||
-- Identificadores
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
|
||||
bot_id UUID REFERENCES trading.bots(id),
|
||||
strategy_id UUID REFERENCES trading.strategies(id),
|
||||
|
||||
-- Clasificacion
|
||||
type trading.signal_type NOT NULL DEFAULT 'entry',
|
||||
source trading.signal_source NOT NULL DEFAULT 'ml_model',
|
||||
status trading.signal_status NOT NULL DEFAULT 'pending',
|
||||
|
||||
-- Instrumento
|
||||
symbol_id UUID REFERENCES trading.symbols(id),
|
||||
symbol VARCHAR(20) NOT NULL,
|
||||
timeframe trading.timeframe DEFAULT 'H1',
|
||||
|
||||
-- Direccion y precios
|
||||
direction trading.trade_direction NOT NULL,
|
||||
entry_price DECIMAL(15, 8) NOT NULL,
|
||||
stop_loss DECIMAL(15, 8),
|
||||
take_profit DECIMAL(15, 8),
|
||||
take_profit_2 DECIMAL(15, 8), -- TP secundario
|
||||
take_profit_3 DECIMAL(15, 8), -- TP terciario
|
||||
|
||||
-- Risk/Reward
|
||||
risk_pips DECIMAL(10, 4),
|
||||
reward_pips DECIMAL(10, 4),
|
||||
risk_reward_ratio DECIMAL(10, 4),
|
||||
|
||||
-- Confianza y scoring
|
||||
confidence DECIMAL(5, 2) NOT NULL DEFAULT 50 -- 0-100
|
||||
CHECK (confidence BETWEEN 0 AND 100),
|
||||
strength VARCHAR(20), -- 'weak', 'moderate', 'strong', 'very_strong'
|
||||
quality_score DECIMAL(5, 2),
|
||||
|
||||
-- Modelo ML (si aplica)
|
||||
model_id VARCHAR(100),
|
||||
model_version VARCHAR(20),
|
||||
model_prediction JSONB, -- Prediccion completa del modelo
|
||||
|
||||
-- Analisis tecnico
|
||||
technical_analysis JSONB DEFAULT '{}'::JSONB, -- Indicadores al momento de la senal
|
||||
pattern_detected VARCHAR(100), -- Patron identificado
|
||||
key_levels JSONB, -- Niveles clave S/R
|
||||
|
||||
-- Contexto de mercado
|
||||
market_context JSONB DEFAULT '{}'::JSONB, -- Condiciones de mercado
|
||||
trend_direction VARCHAR(20),
|
||||
volatility_level VARCHAR(20),
|
||||
|
||||
-- Validez
|
||||
valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
valid_until TIMESTAMPTZ, -- NULL = sin expiracion
|
||||
invalidation_price DECIMAL(15, 8), -- Precio que invalida la senal
|
||||
|
||||
-- Ejecucion
|
||||
executed_at TIMESTAMPTZ,
|
||||
executed_price DECIMAL(15, 8),
|
||||
execution_slippage DECIMAL(10, 4),
|
||||
position_id UUID, -- Posicion resultante
|
||||
|
||||
-- Resultado
|
||||
result VARCHAR(20), -- 'win', 'loss', 'breakeven', 'partial'
|
||||
result_pips DECIMAL(10, 4),
|
||||
result_amount DECIMAL(15, 4),
|
||||
|
||||
-- Notificaciones
|
||||
notification_sent BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
notification_sent_at TIMESTAMPTZ,
|
||||
notification_channels JSONB DEFAULT '[]'::JSONB,
|
||||
|
||||
-- Usuarios que vieron/ejecutaron
|
||||
view_count INTEGER NOT NULL DEFAULT 0,
|
||||
execution_count INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Metadata
|
||||
tags VARCHAR(50)[],
|
||||
notes TEXT,
|
||||
metadata JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Timestamps
|
||||
generated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE trading.signals IS
|
||||
'Senales de trading generadas por modelos ML, bots o analistas';
|
||||
|
||||
COMMENT ON COLUMN trading.signals.confidence IS
|
||||
'Nivel de confianza de la senal (0-100%)';
|
||||
|
||||
COMMENT ON COLUMN trading.signals.invalidation_price IS
|
||||
'Precio que invalida la senal antes de ser ejecutada';
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_tenant
|
||||
ON trading.signals(tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_bot
|
||||
ON trading.signals(bot_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_symbol
|
||||
ON trading.signals(symbol, generated_at DESC);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_status
|
||||
ON trading.signals(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_active
|
||||
ON trading.signals(tenant_id, status, generated_at DESC)
|
||||
WHERE status = 'active';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_type_direction
|
||||
ON trading.signals(type, direction);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_confidence
|
||||
ON trading.signals(confidence DESC)
|
||||
WHERE status = 'active';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_valid_until
|
||||
ON trading.signals(valid_until)
|
||||
WHERE valid_until IS NOT NULL AND status = 'active';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_generated
|
||||
ON trading.signals(generated_at DESC);
|
||||
|
||||
-- GIN index para technical_analysis
|
||||
CREATE INDEX IF NOT EXISTS idx_signals_ta_gin
|
||||
ON trading.signals USING GIN (technical_analysis);
|
||||
|
||||
-- Trigger para updated_at
|
||||
DROP TRIGGER IF EXISTS signal_updated_at ON trading.signals;
|
||||
CREATE TRIGGER signal_updated_at
|
||||
BEFORE UPDATE ON trading.signals
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.update_trading_timestamp();
|
||||
|
||||
-- Trigger para calcular risk/reward
|
||||
CREATE OR REPLACE FUNCTION trading.calculate_signal_rr()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.entry_price IS NOT NULL AND NEW.stop_loss IS NOT NULL THEN
|
||||
IF NEW.direction = 'long' THEN
|
||||
NEW.risk_pips := (NEW.entry_price - NEW.stop_loss) * 10000; -- Para forex
|
||||
IF NEW.take_profit IS NOT NULL THEN
|
||||
NEW.reward_pips := (NEW.take_profit - NEW.entry_price) * 10000;
|
||||
END IF;
|
||||
ELSE
|
||||
NEW.risk_pips := (NEW.stop_loss - NEW.entry_price) * 10000;
|
||||
IF NEW.take_profit IS NOT NULL THEN
|
||||
NEW.reward_pips := (NEW.entry_price - NEW.take_profit) * 10000;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF NEW.risk_pips > 0 AND NEW.reward_pips IS NOT NULL THEN
|
||||
NEW.risk_reward_ratio := NEW.reward_pips / NEW.risk_pips;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Determinar strength basado en confidence
|
||||
NEW.strength := CASE
|
||||
WHEN NEW.confidence >= 85 THEN 'very_strong'
|
||||
WHEN NEW.confidence >= 70 THEN 'strong'
|
||||
WHEN NEW.confidence >= 55 THEN 'moderate'
|
||||
ELSE 'weak'
|
||||
END;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS signal_calc_rr ON trading.signals;
|
||||
CREATE TRIGGER signal_calc_rr
|
||||
BEFORE INSERT OR UPDATE OF entry_price, stop_loss, take_profit, confidence ON trading.signals
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.calculate_signal_rr();
|
||||
|
||||
-- Trigger para expirar senales
|
||||
CREATE OR REPLACE FUNCTION trading.expire_old_signals()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
v_count INTEGER;
|
||||
BEGIN
|
||||
UPDATE trading.signals
|
||||
SET status = 'expired'
|
||||
WHERE status = 'active'
|
||||
AND valid_until IS NOT NULL
|
||||
AND valid_until < NOW();
|
||||
|
||||
GET DIAGNOSTICS v_count = ROW_COUNT;
|
||||
RETURN v_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Funcion para actualizar estadisticas del bot cuando se procesa senal
|
||||
CREATE OR REPLACE FUNCTION trading.update_bot_on_signal()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.bot_id IS NOT NULL THEN
|
||||
UPDATE trading.bots
|
||||
SET last_signal_at = NOW()
|
||||
WHERE id = NEW.bot_id;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS signal_bot_update ON trading.signals;
|
||||
CREATE TRIGGER signal_bot_update
|
||||
AFTER INSERT ON trading.signals
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.update_bot_on_signal();
|
||||
|
||||
-- Vista de senales activas
|
||||
CREATE OR REPLACE VIEW trading.v_active_signals AS
|
||||
SELECT
|
||||
s.id,
|
||||
s.tenant_id,
|
||||
s.bot_id,
|
||||
b.name AS bot_name,
|
||||
s.symbol,
|
||||
s.timeframe,
|
||||
s.type,
|
||||
s.direction,
|
||||
s.entry_price,
|
||||
s.stop_loss,
|
||||
s.take_profit,
|
||||
s.risk_reward_ratio,
|
||||
s.confidence,
|
||||
s.strength,
|
||||
s.pattern_detected,
|
||||
s.valid_until,
|
||||
s.generated_at
|
||||
FROM trading.signals s
|
||||
LEFT JOIN trading.bots b ON s.bot_id = b.id
|
||||
WHERE s.status = 'active'
|
||||
AND (s.valid_until IS NULL OR s.valid_until > NOW())
|
||||
ORDER BY s.confidence DESC, s.generated_at DESC;
|
||||
|
||||
-- Vista de rendimiento de senales por simbolo
|
||||
CREATE OR REPLACE VIEW trading.v_signal_performance AS
|
||||
SELECT
|
||||
symbol,
|
||||
COUNT(*) AS total_signals,
|
||||
COUNT(*) FILTER (WHERE result = 'win') AS wins,
|
||||
COUNT(*) FILTER (WHERE result = 'loss') AS losses,
|
||||
ROUND(AVG(confidence)::NUMERIC, 2) AS avg_confidence,
|
||||
ROUND(AVG(risk_reward_ratio)::NUMERIC, 2) AS avg_rr,
|
||||
ROUND((COUNT(*) FILTER (WHERE result = 'win')::DECIMAL / NULLIF(COUNT(*) FILTER (WHERE result IN ('win', 'loss')), 0) * 100)::NUMERIC, 2) AS win_rate
|
||||
FROM trading.signals
|
||||
WHERE status IN ('executed', 'expired')
|
||||
AND result IS NOT NULL
|
||||
GROUP BY symbol
|
||||
ORDER BY win_rate DESC NULLS LAST;
|
||||
|
||||
-- RLS Policy para multi-tenancy
|
||||
ALTER TABLE trading.signals ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY signals_tenant_isolation ON trading.signals
|
||||
FOR ALL
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||||
|
||||
-- Grants
|
||||
GRANT SELECT, INSERT, UPDATE ON trading.signals TO trading_app;
|
||||
GRANT SELECT ON trading.signals TO trading_readonly;
|
||||
GRANT SELECT ON trading.v_active_signals TO trading_app;
|
||||
GRANT SELECT ON trading.v_signal_performance TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION trading.expire_old_signals TO trading_app;
|
||||
395
ddl/schemas/trading/tables/005_positions.sql
Normal file
395
ddl/schemas/trading/tables/005_positions.sql
Normal file
@ -0,0 +1,395 @@
|
||||
-- ============================================================================
|
||||
-- SCHEMA: trading
|
||||
-- TABLE: positions
|
||||
-- DESCRIPTION: Posiciones de trading (abiertas y cerradas)
|
||||
-- VERSION: 1.0.0
|
||||
-- CREATED: 2026-01-16
|
||||
-- SPRINT: Sprint 3 - DDL Implementation Roadmap Q1-2026
|
||||
-- ============================================================================
|
||||
|
||||
-- Tabla de Posiciones de Trading
|
||||
CREATE TABLE IF NOT EXISTS trading.positions (
|
||||
-- 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,
|
||||
|
||||
-- Referencias
|
||||
bot_id UUID REFERENCES trading.bots(id),
|
||||
strategy_id UUID REFERENCES trading.strategies(id),
|
||||
signal_id UUID REFERENCES trading.signals(id),
|
||||
symbol_id UUID REFERENCES trading.symbols(id),
|
||||
|
||||
-- Identificadores externos
|
||||
external_ticket VARCHAR(50), -- Ticket del broker/MT4
|
||||
external_order_id VARCHAR(50),
|
||||
|
||||
-- Instrumento
|
||||
symbol VARCHAR(20) NOT NULL,
|
||||
|
||||
-- Direccion y estado
|
||||
direction trading.trade_direction NOT NULL,
|
||||
status trading.position_status NOT NULL DEFAULT 'pending',
|
||||
order_type trading.order_type NOT NULL DEFAULT 'market',
|
||||
|
||||
-- Tamaño
|
||||
lot_size DECIMAL(10, 4) NOT NULL,
|
||||
units INTEGER, -- Unidades/cantidad
|
||||
|
||||
-- Precios de entrada
|
||||
requested_price DECIMAL(15, 8),
|
||||
entry_price DECIMAL(15, 8),
|
||||
slippage DECIMAL(15, 8),
|
||||
|
||||
-- Stop Loss
|
||||
stop_loss DECIMAL(15, 8),
|
||||
stop_loss_pips DECIMAL(10, 4),
|
||||
original_stop_loss DECIMAL(15, 8),
|
||||
trailing_stop_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
trailing_stop_distance DECIMAL(10, 4),
|
||||
|
||||
-- Take Profit
|
||||
take_profit DECIMAL(15, 8),
|
||||
take_profit_pips DECIMAL(10, 4),
|
||||
take_profit_2 DECIMAL(15, 8),
|
||||
take_profit_3 DECIMAL(15, 8),
|
||||
|
||||
-- Breakeven
|
||||
breakeven_activated BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
breakeven_trigger_pips DECIMAL(10, 4),
|
||||
breakeven_offset_pips DECIMAL(10, 4),
|
||||
|
||||
-- Precios de cierre
|
||||
exit_price DECIMAL(15, 8),
|
||||
exit_reason VARCHAR(50), -- 'manual', 'stop_loss', 'take_profit', 'trailing_stop', 'margin_call'
|
||||
|
||||
-- Profit/Loss
|
||||
profit_loss DECIMAL(15, 4), -- P&L en moneda de cuenta
|
||||
profit_loss_pips DECIMAL(10, 4),
|
||||
profit_loss_percent DECIMAL(10, 4),
|
||||
|
||||
-- Comisiones y costos
|
||||
commission DECIMAL(15, 4) DEFAULT 0,
|
||||
swap DECIMAL(15, 4) DEFAULT 0, -- Swap overnight
|
||||
spread_cost DECIMAL(15, 4) DEFAULT 0,
|
||||
total_cost DECIMAL(15, 4) DEFAULT 0,
|
||||
net_profit DECIMAL(15, 4), -- P&L - costos
|
||||
|
||||
-- Margen
|
||||
margin_used DECIMAL(15, 4),
|
||||
leverage_used INTEGER,
|
||||
|
||||
-- Risk management
|
||||
risk_amount DECIMAL(15, 4), -- Monto en riesgo
|
||||
risk_percent DECIMAL(5, 2), -- % de cuenta en riesgo
|
||||
reward_amount DECIMAL(15, 4),
|
||||
risk_reward_ratio DECIMAL(10, 4),
|
||||
|
||||
-- Tiempo
|
||||
duration_seconds INTEGER,
|
||||
duration_formatted VARCHAR(50), -- "2h 30m"
|
||||
|
||||
-- Partial closes
|
||||
partial_closes JSONB DEFAULT '[]'::JSONB, -- [{ "lot_size": 0.05, "price": 1.1234, "pnl": 50, "at": "..." }]
|
||||
remaining_lot_size DECIMAL(10, 4),
|
||||
|
||||
-- Escalado
|
||||
scale_ins JSONB DEFAULT '[]'::JSONB, -- Adiciones a la posicion
|
||||
average_entry_price DECIMAL(15, 8),
|
||||
|
||||
-- Precio actual (para posiciones abiertas)
|
||||
current_price DECIMAL(15, 8),
|
||||
unrealized_pnl DECIMAL(15, 4),
|
||||
unrealized_pnl_pips DECIMAL(10, 4),
|
||||
price_updated_at TIMESTAMPTZ,
|
||||
|
||||
-- Analisis post-trade
|
||||
max_favorable_excursion DECIMAL(15, 4), -- MFE - max ganancia no realizada
|
||||
max_adverse_excursion DECIMAL(15, 4), -- MAE - max perdida no realizada
|
||||
mfe_price DECIMAL(15, 8),
|
||||
mae_price DECIMAL(15, 8),
|
||||
|
||||
-- Screenshots/Evidence
|
||||
entry_screenshot_url TEXT,
|
||||
exit_screenshot_url TEXT,
|
||||
|
||||
-- Notas
|
||||
notes TEXT,
|
||||
tags VARCHAR(50)[],
|
||||
|
||||
-- Metadata
|
||||
metadata JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Timestamps
|
||||
requested_at TIMESTAMPTZ,
|
||||
opened_at TIMESTAMPTZ,
|
||||
closed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE trading.positions IS
|
||||
'Posiciones de trading con tracking completo de entrada, salida y P&L';
|
||||
|
||||
COMMENT ON COLUMN trading.positions.max_favorable_excursion IS
|
||||
'Maximum Favorable Excursion - maxima ganancia no realizada durante el trade';
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_positions_tenant
|
||||
ON trading.positions(tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_positions_user
|
||||
ON trading.positions(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_positions_bot
|
||||
ON trading.positions(bot_id)
|
||||
WHERE bot_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_positions_strategy
|
||||
ON trading.positions(strategy_id)
|
||||
WHERE strategy_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_positions_signal
|
||||
ON trading.positions(signal_id)
|
||||
WHERE signal_id IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_positions_symbol
|
||||
ON trading.positions(symbol);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_positions_status
|
||||
ON trading.positions(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_positions_open
|
||||
ON trading.positions(tenant_id, user_id, status)
|
||||
WHERE status = 'open';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_positions_closed
|
||||
ON trading.positions(tenant_id, closed_at DESC)
|
||||
WHERE status IN ('closed', 'stopped', 'target_hit');
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_positions_external
|
||||
ON trading.positions(external_ticket)
|
||||
WHERE external_ticket IS NOT NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_positions_pnl
|
||||
ON trading.positions(profit_loss DESC)
|
||||
WHERE status IN ('closed', 'stopped', 'target_hit');
|
||||
|
||||
-- Trigger para updated_at
|
||||
DROP TRIGGER IF EXISTS position_updated_at ON trading.positions;
|
||||
CREATE TRIGGER position_updated_at
|
||||
BEFORE UPDATE ON trading.positions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.update_trading_timestamp();
|
||||
|
||||
-- Trigger para calcular campos al abrir posicion
|
||||
CREATE OR REPLACE FUNCTION trading.calculate_position_on_open()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.status = 'open' AND OLD.status = 'pending' THEN
|
||||
NEW.opened_at := NOW();
|
||||
|
||||
-- Calcular slippage
|
||||
IF NEW.requested_price IS NOT NULL AND NEW.entry_price IS NOT NULL THEN
|
||||
NEW.slippage := ABS(NEW.entry_price - NEW.requested_price);
|
||||
END IF;
|
||||
|
||||
-- Calcular pips de SL/TP
|
||||
IF NEW.stop_loss IS NOT NULL AND NEW.entry_price IS NOT NULL THEN
|
||||
IF NEW.direction = 'long' THEN
|
||||
NEW.stop_loss_pips := (NEW.entry_price - NEW.stop_loss) * 10000;
|
||||
ELSE
|
||||
NEW.stop_loss_pips := (NEW.stop_loss - NEW.entry_price) * 10000;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
IF NEW.take_profit IS NOT NULL AND NEW.entry_price IS NOT NULL THEN
|
||||
IF NEW.direction = 'long' THEN
|
||||
NEW.take_profit_pips := (NEW.take_profit - NEW.entry_price) * 10000;
|
||||
ELSE
|
||||
NEW.take_profit_pips := (NEW.entry_price - NEW.take_profit) * 10000;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
-- Calcular risk/reward ratio
|
||||
IF NEW.stop_loss_pips > 0 AND NEW.take_profit_pips IS NOT NULL THEN
|
||||
NEW.risk_reward_ratio := NEW.take_profit_pips / NEW.stop_loss_pips;
|
||||
END IF;
|
||||
|
||||
NEW.remaining_lot_size := NEW.lot_size;
|
||||
NEW.average_entry_price := NEW.entry_price;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS position_on_open ON trading.positions;
|
||||
CREATE TRIGGER position_on_open
|
||||
BEFORE UPDATE OF status ON trading.positions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.calculate_position_on_open();
|
||||
|
||||
-- Trigger para calcular campos al cerrar posicion
|
||||
CREATE OR REPLACE FUNCTION trading.calculate_position_on_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 en pips
|
||||
IF NEW.exit_price IS NOT NULL AND NEW.entry_price IS NOT NULL THEN
|
||||
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;
|
||||
END IF;
|
||||
|
||||
-- Calcular duracion
|
||||
IF NEW.opened_at IS NOT NULL THEN
|
||||
NEW.duration_seconds := EXTRACT(EPOCH FROM (NEW.closed_at - NEW.opened_at))::INTEGER;
|
||||
|
||||
-- Formatear duracion
|
||||
NEW.duration_formatted :=
|
||||
CASE
|
||||
WHEN NEW.duration_seconds < 60 THEN NEW.duration_seconds || 's'
|
||||
WHEN NEW.duration_seconds < 3600 THEN (NEW.duration_seconds / 60) || 'm'
|
||||
WHEN NEW.duration_seconds < 86400 THEN (NEW.duration_seconds / 3600) || 'h ' || ((NEW.duration_seconds % 3600) / 60) || 'm'
|
||||
ELSE (NEW.duration_seconds / 86400) || 'd ' || ((NEW.duration_seconds % 86400) / 3600) || 'h'
|
||||
END;
|
||||
END IF;
|
||||
|
||||
-- Calcular costos totales
|
||||
NEW.total_cost := COALESCE(NEW.commission, 0) + COALESCE(NEW.swap, 0) + COALESCE(NEW.spread_cost, 0);
|
||||
NEW.net_profit := COALESCE(NEW.profit_loss, 0) - NEW.total_cost;
|
||||
|
||||
-- Determinar exit_reason basado en status
|
||||
IF NEW.exit_reason IS NULL THEN
|
||||
NEW.exit_reason := CASE NEW.status
|
||||
WHEN 'stopped' THEN 'stop_loss'
|
||||
WHEN 'target_hit' THEN 'take_profit'
|
||||
ELSE 'manual'
|
||||
END;
|
||||
END IF;
|
||||
|
||||
-- Actualizar senal si existe
|
||||
IF NEW.signal_id IS NOT NULL THEN
|
||||
UPDATE trading.signals
|
||||
SET status = 'executed',
|
||||
executed_at = NEW.closed_at,
|
||||
executed_price = NEW.exit_price,
|
||||
result = CASE
|
||||
WHEN NEW.profit_loss > 0 THEN 'win'
|
||||
WHEN NEW.profit_loss < 0 THEN 'loss'
|
||||
ELSE 'breakeven'
|
||||
END,
|
||||
result_pips = NEW.profit_loss_pips,
|
||||
result_amount = NEW.profit_loss,
|
||||
position_id = NEW.id
|
||||
WHERE id = NEW.signal_id;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS position_on_close ON trading.positions;
|
||||
CREATE TRIGGER position_on_close
|
||||
BEFORE UPDATE OF status ON trading.positions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.calculate_position_on_close();
|
||||
|
||||
-- Trigger para actualizar estadisticas de bot/strategy
|
||||
CREATE OR REPLACE FUNCTION trading.update_stats_on_position_close()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.status IN ('closed', 'stopped', 'target_hit') AND OLD.status = 'open' THEN
|
||||
-- Actualizar bot stats
|
||||
IF NEW.bot_id IS NOT NULL THEN
|
||||
PERFORM trading.recalculate_bot_stats(NEW.bot_id);
|
||||
END IF;
|
||||
|
||||
-- Actualizar strategy stats
|
||||
IF NEW.strategy_id IS NOT NULL THEN
|
||||
PERFORM trading.recalculate_strategy_stats(NEW.strategy_id);
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS position_stats_update ON trading.positions;
|
||||
CREATE TRIGGER position_stats_update
|
||||
AFTER UPDATE OF status ON trading.positions
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.update_stats_on_position_close();
|
||||
|
||||
-- Vista de posiciones abiertas
|
||||
CREATE OR REPLACE VIEW trading.v_open_positions AS
|
||||
SELECT
|
||||
p.id,
|
||||
p.tenant_id,
|
||||
p.user_id,
|
||||
p.bot_id,
|
||||
b.name AS bot_name,
|
||||
p.symbol,
|
||||
p.direction,
|
||||
p.lot_size,
|
||||
p.entry_price,
|
||||
p.stop_loss,
|
||||
p.take_profit,
|
||||
p.current_price,
|
||||
p.unrealized_pnl,
|
||||
p.unrealized_pnl_pips,
|
||||
p.risk_reward_ratio,
|
||||
p.opened_at,
|
||||
NOW() - p.opened_at AS time_open
|
||||
FROM trading.positions p
|
||||
LEFT JOIN trading.bots b ON p.bot_id = b.id
|
||||
WHERE p.status = 'open'
|
||||
ORDER BY p.opened_at DESC;
|
||||
|
||||
-- Vista de historial de trades
|
||||
CREATE OR REPLACE VIEW trading.v_trade_history AS
|
||||
SELECT
|
||||
p.id,
|
||||
p.tenant_id,
|
||||
p.user_id,
|
||||
p.symbol,
|
||||
p.direction,
|
||||
p.lot_size,
|
||||
p.entry_price,
|
||||
p.exit_price,
|
||||
p.stop_loss,
|
||||
p.take_profit,
|
||||
p.profit_loss,
|
||||
p.profit_loss_pips,
|
||||
p.net_profit,
|
||||
p.exit_reason,
|
||||
p.duration_formatted,
|
||||
p.opened_at,
|
||||
p.closed_at
|
||||
FROM trading.positions p
|
||||
WHERE p.status IN ('closed', 'stopped', 'target_hit')
|
||||
ORDER BY p.closed_at DESC;
|
||||
|
||||
-- RLS Policy para multi-tenancy
|
||||
ALTER TABLE trading.positions ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY positions_tenant_isolation ON trading.positions
|
||||
FOR ALL
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||||
|
||||
-- Los usuarios solo ven sus propias posiciones
|
||||
CREATE POLICY positions_user_isolation ON trading.positions
|
||||
FOR SELECT
|
||||
USING (user_id = current_setting('app.current_user_id', true)::UUID);
|
||||
|
||||
-- Grants
|
||||
GRANT SELECT, INSERT, UPDATE ON trading.positions TO trading_app;
|
||||
GRANT SELECT ON trading.positions TO trading_readonly;
|
||||
GRANT SELECT ON trading.v_open_positions TO trading_app;
|
||||
GRANT SELECT ON trading.v_trade_history TO trading_app;
|
||||
260
ddl/schemas/trading/tables/006_price_alerts.sql
Normal file
260
ddl/schemas/trading/tables/006_price_alerts.sql
Normal file
@ -0,0 +1,260 @@
|
||||
-- ============================================================================
|
||||
-- SCHEMA: trading
|
||||
-- TABLE: price_alerts
|
||||
-- DESCRIPTION: Alertas de precio configuradas por usuarios
|
||||
-- VERSION: 1.0.0
|
||||
-- CREATED: 2026-01-16
|
||||
-- SPRINT: Sprint 3 - DDL Implementation Roadmap Q1-2026
|
||||
-- ============================================================================
|
||||
|
||||
-- Enum para tipo de alerta
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE trading.alert_type AS ENUM (
|
||||
'price_above', -- Precio sube por encima de
|
||||
'price_below', -- Precio baja por debajo de
|
||||
'price_cross', -- Precio cruza (cualquier direccion)
|
||||
'percent_change', -- Cambio porcentual
|
||||
'volume_spike', -- Spike de volumen
|
||||
'volatility', -- Alerta de volatilidad
|
||||
'indicator', -- Alerta de indicador tecnico
|
||||
'pattern' -- Patron detectado
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Enum para estado de alerta
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE trading.alert_status AS ENUM (
|
||||
'active', -- Activa y monitoreando
|
||||
'triggered', -- Disparada
|
||||
'expired', -- Expirada sin disparar
|
||||
'paused', -- Pausada por usuario
|
||||
'deleted' -- Eliminada (soft delete)
|
||||
);
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN null;
|
||||
END $$;
|
||||
|
||||
-- Tabla de Alertas de Precio
|
||||
CREATE TABLE IF NOT EXISTS trading.price_alerts (
|
||||
-- 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,
|
||||
symbol_id UUID REFERENCES trading.symbols(id),
|
||||
|
||||
-- Simbolo
|
||||
symbol VARCHAR(20) NOT NULL,
|
||||
|
||||
-- Tipo y estado
|
||||
type trading.alert_type NOT NULL DEFAULT 'price_above',
|
||||
status trading.alert_status NOT NULL DEFAULT 'active',
|
||||
|
||||
-- Condicion
|
||||
target_price DECIMAL(15, 8), -- Precio objetivo
|
||||
trigger_value DECIMAL(15, 8), -- Valor que dispara (precio, %, etc)
|
||||
comparison VARCHAR(10) DEFAULT 'gte', -- 'gt', 'gte', 'lt', 'lte', 'eq', 'cross'
|
||||
|
||||
-- Para alertas de cambio porcentual
|
||||
percent_change DECIMAL(10, 4),
|
||||
time_window_minutes INTEGER, -- Ventana de tiempo para % change
|
||||
base_price DECIMAL(15, 8), -- Precio base para calcular %
|
||||
|
||||
-- Para alertas de indicadores
|
||||
indicator_name VARCHAR(50),
|
||||
indicator_config JSONB,
|
||||
indicator_threshold DECIMAL(15, 8),
|
||||
|
||||
-- Configuracion
|
||||
is_recurring BOOLEAN NOT NULL DEFAULT FALSE, -- Se reactiva despues de disparar
|
||||
cooldown_minutes INTEGER DEFAULT 60, -- Cooldown entre disparos
|
||||
max_triggers INTEGER, -- Max veces que puede disparar (NULL = ilimitado)
|
||||
trigger_count INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Notificacion
|
||||
notify_email BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
notify_push BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
notify_sms BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
notification_message TEXT, -- Mensaje personalizado
|
||||
|
||||
-- Sonido
|
||||
play_sound BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
sound_name VARCHAR(50) DEFAULT 'default',
|
||||
|
||||
-- Vigencia
|
||||
valid_from TIMESTAMPTZ DEFAULT NOW(),
|
||||
valid_until TIMESTAMPTZ, -- NULL = sin expiracion
|
||||
|
||||
-- Ultimo trigger
|
||||
last_triggered_at TIMESTAMPTZ,
|
||||
last_triggered_price DECIMAL(15, 8),
|
||||
|
||||
-- Precio actual (cache)
|
||||
current_price DECIMAL(15, 8),
|
||||
price_updated_at TIMESTAMPTZ,
|
||||
|
||||
-- Notas
|
||||
name VARCHAR(100), -- Nombre descriptivo opcional
|
||||
notes TEXT,
|
||||
|
||||
-- Metadata
|
||||
metadata JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
COMMENT ON TABLE trading.price_alerts IS
|
||||
'Alertas de precio configuradas por usuarios para monitorear instrumentos';
|
||||
|
||||
COMMENT ON COLUMN trading.price_alerts.is_recurring IS
|
||||
'Si TRUE, la alerta se reactiva automaticamente despues de disparar';
|
||||
|
||||
-- Indices
|
||||
CREATE INDEX IF NOT EXISTS idx_price_alerts_tenant
|
||||
ON trading.price_alerts(tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_price_alerts_user
|
||||
ON trading.price_alerts(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_price_alerts_symbol
|
||||
ON trading.price_alerts(symbol);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_price_alerts_status
|
||||
ON trading.price_alerts(status);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_price_alerts_active
|
||||
ON trading.price_alerts(symbol, status, target_price)
|
||||
WHERE status = 'active';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_price_alerts_user_active
|
||||
ON trading.price_alerts(user_id, status)
|
||||
WHERE status = 'active';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_price_alerts_expiring
|
||||
ON trading.price_alerts(valid_until)
|
||||
WHERE valid_until IS NOT NULL AND status = 'active';
|
||||
|
||||
-- Trigger para updated_at
|
||||
DROP TRIGGER IF EXISTS price_alert_updated_at ON trading.price_alerts;
|
||||
CREATE TRIGGER price_alert_updated_at
|
||||
BEFORE UPDATE ON trading.price_alerts
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.update_trading_timestamp();
|
||||
|
||||
-- Funcion para verificar y disparar alertas
|
||||
CREATE OR REPLACE FUNCTION trading.check_price_alerts(
|
||||
p_symbol VARCHAR(20),
|
||||
p_price DECIMAL(15, 8)
|
||||
)
|
||||
RETURNS TABLE (
|
||||
alert_id UUID,
|
||||
user_id UUID,
|
||||
alert_type trading.alert_type,
|
||||
notification_message TEXT
|
||||
) AS $$
|
||||
DECLARE
|
||||
v_alert RECORD;
|
||||
BEGIN
|
||||
FOR v_alert IN
|
||||
SELECT * FROM trading.price_alerts
|
||||
WHERE symbol = p_symbol
|
||||
AND status = 'active'
|
||||
AND (valid_until IS NULL OR valid_until > NOW())
|
||||
AND (last_triggered_at IS NULL OR last_triggered_at + (cooldown_minutes || ' minutes')::INTERVAL < NOW())
|
||||
LOOP
|
||||
-- Verificar condicion segun tipo
|
||||
IF (v_alert.type = 'price_above' AND p_price >= v_alert.target_price) OR
|
||||
(v_alert.type = 'price_below' AND p_price <= v_alert.target_price) OR
|
||||
(v_alert.type = 'price_cross' AND
|
||||
((v_alert.current_price < v_alert.target_price AND p_price >= v_alert.target_price) OR
|
||||
(v_alert.current_price > v_alert.target_price AND p_price <= v_alert.target_price)))
|
||||
THEN
|
||||
-- Disparar alerta
|
||||
UPDATE trading.price_alerts
|
||||
SET status = CASE WHEN is_recurring THEN 'active' ELSE 'triggered' END,
|
||||
last_triggered_at = NOW(),
|
||||
last_triggered_price = p_price,
|
||||
trigger_count = trigger_count + 1
|
||||
WHERE id = v_alert.id;
|
||||
|
||||
-- Verificar max_triggers
|
||||
IF v_alert.max_triggers IS NOT NULL AND v_alert.trigger_count + 1 >= v_alert.max_triggers THEN
|
||||
UPDATE trading.price_alerts SET status = 'triggered' WHERE id = v_alert.id;
|
||||
END IF;
|
||||
|
||||
RETURN QUERY SELECT
|
||||
v_alert.id,
|
||||
v_alert.user_id,
|
||||
v_alert.type,
|
||||
COALESCE(v_alert.notification_message,
|
||||
v_alert.symbol || ' alcanzó ' || p_price::TEXT);
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- Actualizar precio actual en todas las alertas del simbolo
|
||||
UPDATE trading.price_alerts
|
||||
SET current_price = p_price,
|
||||
price_updated_at = NOW()
|
||||
WHERE symbol = p_symbol
|
||||
AND status = 'active';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Funcion para expirar alertas
|
||||
CREATE OR REPLACE FUNCTION trading.expire_old_alerts()
|
||||
RETURNS INTEGER AS $$
|
||||
DECLARE
|
||||
v_count INTEGER;
|
||||
BEGIN
|
||||
UPDATE trading.price_alerts
|
||||
SET status = 'expired'
|
||||
WHERE status = 'active'
|
||||
AND valid_until IS NOT NULL
|
||||
AND valid_until < NOW();
|
||||
|
||||
GET DIAGNOSTICS v_count = ROW_COUNT;
|
||||
RETURN v_count;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Vista de alertas activas del usuario
|
||||
CREATE OR REPLACE VIEW trading.v_my_price_alerts AS
|
||||
SELECT
|
||||
id,
|
||||
symbol,
|
||||
type,
|
||||
status,
|
||||
target_price,
|
||||
current_price,
|
||||
name,
|
||||
notify_email,
|
||||
notify_push,
|
||||
is_recurring,
|
||||
trigger_count,
|
||||
last_triggered_at,
|
||||
valid_until,
|
||||
created_at
|
||||
FROM trading.price_alerts
|
||||
WHERE status IN ('active', 'paused')
|
||||
ORDER BY created_at DESC;
|
||||
|
||||
-- RLS Policy para multi-tenancy
|
||||
ALTER TABLE trading.price_alerts ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY price_alerts_tenant_isolation ON trading.price_alerts
|
||||
FOR ALL
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||||
|
||||
CREATE POLICY price_alerts_user_isolation ON trading.price_alerts
|
||||
FOR ALL
|
||||
USING (user_id = current_setting('app.current_user_id', true)::UUID);
|
||||
|
||||
-- Grants
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON trading.price_alerts TO trading_app;
|
||||
GRANT SELECT ON trading.price_alerts TO trading_readonly;
|
||||
GRANT SELECT ON trading.v_my_price_alerts TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION trading.check_price_alerts TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION trading.expire_old_alerts TO trading_app;
|
||||
334
ddl/schemas/trading/tables/007_watchlists.sql
Normal file
334
ddl/schemas/trading/tables/007_watchlists.sql
Normal file
@ -0,0 +1,334 @@
|
||||
-- ============================================================================
|
||||
-- SCHEMA: trading
|
||||
-- TABLE: watchlists, watchlist_items
|
||||
-- DESCRIPTION: Listas de seguimiento de simbolos
|
||||
-- VERSION: 1.0.0
|
||||
-- CREATED: 2026-01-16
|
||||
-- SPRINT: Sprint 3 - DDL Implementation Roadmap Q1-2026
|
||||
-- ============================================================================
|
||||
|
||||
-- Tabla de Watchlists
|
||||
CREATE TABLE IF NOT EXISTS trading.watchlists (
|
||||
-- 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
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
color VARCHAR(7), -- Color hex para identificacion
|
||||
icon VARCHAR(50),
|
||||
|
||||
-- Configuracion
|
||||
is_default BOOLEAN NOT NULL DEFAULT FALSE, -- Watchlist por defecto
|
||||
is_public BOOLEAN NOT NULL DEFAULT FALSE, -- Compartida publicamente
|
||||
is_pinned BOOLEAN NOT NULL DEFAULT FALSE, -- Fijada en UI
|
||||
|
||||
-- Ordenamiento
|
||||
display_order INTEGER NOT NULL DEFAULT 0,
|
||||
sort_by VARCHAR(50) DEFAULT 'symbol', -- 'symbol', 'change', 'volume', 'custom'
|
||||
sort_direction VARCHAR(4) DEFAULT 'asc',
|
||||
|
||||
-- Columnas visibles
|
||||
visible_columns JSONB DEFAULT '["symbol", "price", "change", "change_percent"]'::JSONB,
|
||||
|
||||
-- Estadisticas
|
||||
item_count INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Compartir
|
||||
share_code VARCHAR(20), -- Codigo para compartir
|
||||
shared_at TIMESTAMPTZ,
|
||||
view_count INTEGER NOT NULL DEFAULT 0,
|
||||
copy_count INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Metadata
|
||||
metadata JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Timestamps
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT watchlists_unique_name UNIQUE (user_id, name)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE trading.watchlists IS
|
||||
'Listas de seguimiento de simbolos creadas por usuarios';
|
||||
|
||||
-- Tabla de Items de Watchlist
|
||||
CREATE TABLE IF NOT EXISTS trading.watchlist_items (
|
||||
-- Identificadores
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
watchlist_id UUID NOT NULL REFERENCES trading.watchlists(id) ON DELETE CASCADE,
|
||||
symbol_id UUID REFERENCES trading.symbols(id),
|
||||
ticker_id UUID REFERENCES market_data.tickers(id),
|
||||
|
||||
-- Simbolo
|
||||
symbol VARCHAR(20) NOT NULL,
|
||||
|
||||
-- Orden personalizado
|
||||
display_order INTEGER NOT NULL DEFAULT 0,
|
||||
|
||||
-- Notas del usuario
|
||||
notes TEXT,
|
||||
tags VARCHAR(50)[],
|
||||
|
||||
-- Alertas rapidas
|
||||
alert_price_above DECIMAL(15, 8),
|
||||
alert_price_below DECIMAL(15, 8),
|
||||
alert_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
-- Precios de referencia del usuario
|
||||
entry_price DECIMAL(15, 8), -- Precio de entrada planeado
|
||||
target_price DECIMAL(15, 8), -- Precio objetivo
|
||||
stop_price DECIMAL(15, 8), -- Stop loss planeado
|
||||
|
||||
-- Precio actual (cache)
|
||||
current_price DECIMAL(15, 8),
|
||||
price_change DECIMAL(15, 8),
|
||||
price_change_percent DECIMAL(10, 4),
|
||||
price_updated_at TIMESTAMPTZ,
|
||||
|
||||
-- Metadata
|
||||
metadata JSONB DEFAULT '{}'::JSONB,
|
||||
|
||||
-- Timestamps
|
||||
added_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
|
||||
-- Constraints
|
||||
CONSTRAINT watchlist_items_unique UNIQUE (watchlist_id, symbol)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE trading.watchlist_items IS
|
||||
'Simbolos individuales dentro de una watchlist';
|
||||
|
||||
-- Indices para watchlists
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlists_tenant
|
||||
ON trading.watchlists(tenant_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlists_user
|
||||
ON trading.watchlists(user_id);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlists_default
|
||||
ON trading.watchlists(user_id, is_default)
|
||||
WHERE is_default = TRUE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlists_public
|
||||
ON trading.watchlists(is_public, view_count DESC)
|
||||
WHERE is_public = TRUE;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlists_share_code
|
||||
ON trading.watchlists(share_code)
|
||||
WHERE share_code IS NOT NULL;
|
||||
|
||||
-- Indices para watchlist_items
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlist_items_watchlist
|
||||
ON trading.watchlist_items(watchlist_id, display_order);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlist_items_symbol
|
||||
ON trading.watchlist_items(symbol);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_watchlist_items_alerts
|
||||
ON trading.watchlist_items(alert_enabled, symbol)
|
||||
WHERE alert_enabled = TRUE;
|
||||
|
||||
-- Trigger para updated_at en watchlists
|
||||
DROP TRIGGER IF EXISTS watchlist_updated_at ON trading.watchlists;
|
||||
CREATE TRIGGER watchlist_updated_at
|
||||
BEFORE UPDATE ON trading.watchlists
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.update_trading_timestamp();
|
||||
|
||||
-- Trigger para updated_at en watchlist_items
|
||||
DROP TRIGGER IF EXISTS watchlist_item_updated_at ON trading.watchlist_items;
|
||||
CREATE TRIGGER watchlist_item_updated_at
|
||||
BEFORE UPDATE ON trading.watchlist_items
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.update_trading_timestamp();
|
||||
|
||||
-- Trigger para actualizar item_count en watchlist
|
||||
CREATE OR REPLACE FUNCTION trading.update_watchlist_count()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF TG_OP = 'INSERT' THEN
|
||||
UPDATE trading.watchlists
|
||||
SET item_count = item_count + 1
|
||||
WHERE id = NEW.watchlist_id;
|
||||
ELSIF TG_OP = 'DELETE' THEN
|
||||
UPDATE trading.watchlists
|
||||
SET item_count = item_count - 1
|
||||
WHERE id = OLD.watchlist_id;
|
||||
END IF;
|
||||
|
||||
RETURN COALESCE(NEW, OLD);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
DROP TRIGGER IF EXISTS watchlist_item_count ON trading.watchlist_items;
|
||||
CREATE TRIGGER watchlist_item_count
|
||||
AFTER INSERT OR DELETE ON trading.watchlist_items
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION trading.update_watchlist_count();
|
||||
|
||||
-- Trigger para asegurar solo una watchlist default por usuario
|
||||
CREATE OR REPLACE FUNCTION trading.ensure_single_default_watchlist()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.is_default = TRUE THEN
|
||||
UPDATE trading.watchlists
|
||||
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 watchlist_single_default ON trading.watchlists;
|
||||
CREATE TRIGGER watchlist_single_default
|
||||
BEFORE INSERT OR UPDATE OF is_default ON trading.watchlists
|
||||
FOR EACH ROW
|
||||
WHEN (NEW.is_default = TRUE)
|
||||
EXECUTE FUNCTION trading.ensure_single_default_watchlist();
|
||||
|
||||
-- Funcion para copiar watchlist
|
||||
CREATE OR REPLACE FUNCTION trading.copy_watchlist(
|
||||
p_watchlist_id UUID,
|
||||
p_new_user_id UUID,
|
||||
p_new_name VARCHAR(100) DEFAULT NULL
|
||||
)
|
||||
RETURNS UUID AS $$
|
||||
DECLARE
|
||||
v_source RECORD;
|
||||
v_new_id UUID;
|
||||
v_tenant_id UUID;
|
||||
BEGIN
|
||||
-- Obtener watchlist origen
|
||||
SELECT * INTO v_source FROM trading.watchlists WHERE id = p_watchlist_id;
|
||||
|
||||
IF v_source IS NULL THEN
|
||||
RAISE EXCEPTION 'Watchlist not found';
|
||||
END IF;
|
||||
|
||||
-- Obtener tenant del nuevo usuario
|
||||
SELECT tenant_id INTO v_tenant_id FROM users.users WHERE id = p_new_user_id;
|
||||
|
||||
-- Crear nueva watchlist
|
||||
INSERT INTO trading.watchlists (
|
||||
tenant_id, user_id, name, description, color, icon,
|
||||
sort_by, sort_direction, visible_columns
|
||||
) VALUES (
|
||||
v_tenant_id,
|
||||
p_new_user_id,
|
||||
COALESCE(p_new_name, v_source.name || ' (Copy)'),
|
||||
v_source.description,
|
||||
v_source.color,
|
||||
v_source.icon,
|
||||
v_source.sort_by,
|
||||
v_source.sort_direction,
|
||||
v_source.visible_columns
|
||||
)
|
||||
RETURNING id INTO v_new_id;
|
||||
|
||||
-- Copiar items
|
||||
INSERT INTO trading.watchlist_items (
|
||||
watchlist_id, symbol_id, ticker_id, symbol,
|
||||
display_order, notes, tags,
|
||||
entry_price, target_price, stop_price
|
||||
)
|
||||
SELECT
|
||||
v_new_id, symbol_id, ticker_id, symbol,
|
||||
display_order, notes, tags,
|
||||
entry_price, target_price, stop_price
|
||||
FROM trading.watchlist_items
|
||||
WHERE watchlist_id = p_watchlist_id;
|
||||
|
||||
-- Incrementar contador de copias
|
||||
UPDATE trading.watchlists
|
||||
SET copy_count = copy_count + 1
|
||||
WHERE id = p_watchlist_id;
|
||||
|
||||
RETURN v_new_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Vista de watchlists con items
|
||||
CREATE OR REPLACE VIEW trading.v_watchlists_with_items AS
|
||||
SELECT
|
||||
w.id,
|
||||
w.user_id,
|
||||
w.name,
|
||||
w.description,
|
||||
w.color,
|
||||
w.is_default,
|
||||
w.is_pinned,
|
||||
w.item_count,
|
||||
w.display_order,
|
||||
(
|
||||
SELECT jsonb_agg(jsonb_build_object(
|
||||
'symbol', wi.symbol,
|
||||
'current_price', wi.current_price,
|
||||
'price_change_percent', wi.price_change_percent,
|
||||
'notes', wi.notes
|
||||
) ORDER BY wi.display_order)
|
||||
FROM trading.watchlist_items wi
|
||||
WHERE wi.watchlist_id = w.id
|
||||
) AS items
|
||||
FROM trading.watchlists w
|
||||
ORDER BY w.is_pinned DESC, w.display_order, w.name;
|
||||
|
||||
-- Vista de items con precios actualizados
|
||||
CREATE OR REPLACE VIEW trading.v_watchlist_items_live AS
|
||||
SELECT
|
||||
wi.id,
|
||||
wi.watchlist_id,
|
||||
wi.symbol,
|
||||
t.name AS symbol_name,
|
||||
t.type AS symbol_type,
|
||||
t.current_bid,
|
||||
t.current_ask,
|
||||
wi.current_price,
|
||||
wi.price_change_percent,
|
||||
wi.entry_price,
|
||||
wi.target_price,
|
||||
wi.stop_price,
|
||||
wi.notes,
|
||||
wi.display_order
|
||||
FROM trading.watchlist_items wi
|
||||
LEFT JOIN market_data.tickers t ON wi.symbol = t.symbol
|
||||
ORDER BY wi.watchlist_id, wi.display_order;
|
||||
|
||||
-- RLS Policy para multi-tenancy en watchlists
|
||||
ALTER TABLE trading.watchlists ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY watchlists_tenant_isolation ON trading.watchlists
|
||||
FOR ALL
|
||||
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
|
||||
|
||||
CREATE POLICY watchlists_user_isolation ON trading.watchlists
|
||||
FOR ALL
|
||||
USING (user_id = current_setting('app.current_user_id', true)::UUID);
|
||||
|
||||
-- RLS para watchlist_items (hereda de watchlist via JOIN)
|
||||
ALTER TABLE trading.watchlist_items ENABLE ROW LEVEL SECURITY;
|
||||
|
||||
CREATE POLICY watchlist_items_via_watchlist ON trading.watchlist_items
|
||||
FOR ALL
|
||||
USING (
|
||||
watchlist_id IN (
|
||||
SELECT id FROM trading.watchlists
|
||||
WHERE user_id = current_setting('app.current_user_id', true)::UUID
|
||||
)
|
||||
);
|
||||
|
||||
-- Grants
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON trading.watchlists TO trading_app;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON trading.watchlist_items TO trading_app;
|
||||
GRANT SELECT ON trading.watchlists TO trading_readonly;
|
||||
GRANT SELECT ON trading.watchlist_items TO trading_readonly;
|
||||
GRANT SELECT ON trading.v_watchlists_with_items TO trading_app;
|
||||
GRANT SELECT ON trading.v_watchlist_items_live TO trading_app;
|
||||
GRANT EXECUTE ON FUNCTION trading.copy_watchlist TO trading_app;
|
||||
464
ddl/schemas/trading/tables/008_paper_trading.sql
Normal file
464
ddl/schemas/trading/tables/008_paper_trading.sql
Normal file
@ -0,0 +1,464 @@
|
||||
-- ============================================================================
|
||||
-- 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;
|
||||
Loading…
Reference in New Issue
Block a user