trading-platform-database-v2/ddl/schemas/trading/tables/004_signals.sql

324 lines
10 KiB
PL/PgSQL

-- ============================================================================
-- 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;