trading-platform-database-v2/ddl/schemas/ml/004_entry_signals.sql
rckrdmrd 9f8bbb7494 [DDL] feat: Sprint 4 - Add data_sources and ml_predictions schemas
- data_sources schema:
  - api_providers: Proveedores de datos de mercado
  - ticker_mapping: Mapeo de simbolos entre proveedores
  - data_sync_status: Estado de sincronizacion de datos

- ml schema additions:
  - range_predictions: Predicciones de rango (particionada)
  - entry_signals: Señales de entrada con metodologias ICT/SMC/AMD
  - market_analysis: Analisis de mercado (sesgo, estructura, volatilidad)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 20:08:51 -06:00

409 lines
14 KiB
PL/PgSQL

-- ============================================================================
-- SCHEMA: ml
-- TABLE: entry_signals
-- DESCRIPTION: Senales de entrada generadas por modelos ML
-- VERSION: 1.0.0
-- CREATED: 2026-01-16
-- SPRINT: Sprint 4 - DDL Implementation Roadmap Q1-2026
-- ============================================================================
-- Enum para metodologia de senal
DO $$ BEGIN
CREATE TYPE ml.signal_methodology AS ENUM (
'amd', -- Accumulation, Manipulation, Distribution
'ict_smc', -- ICT Smart Money Concepts
'order_blocks', -- Order Blocks
'fair_value_gap', -- Fair Value Gaps
'liquidity_sweep', -- Liquidity Sweeps
'break_of_structure', -- Break of Structure
'supply_demand', -- Supply & Demand zones
'technical', -- Indicadores tecnicos clasicos
'pattern', -- Patrones de precio
'ensemble' -- Combinacion de metodologias
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Enum para fase AMD
DO $$ BEGIN
CREATE TYPE ml.amd_phase AS ENUM (
'accumulation', -- Fase de acumulacion
'manipulation', -- Fase de manipulacion
'distribution', -- Fase de distribucion
'expansion', -- Fase de expansion
'retracement', -- Fase de retroceso
'consolidation', -- Consolidacion
'unknown' -- Fase no determinada
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Enum para estado de senal de entrada
DO $$ BEGIN
CREATE TYPE ml.entry_signal_status AS ENUM (
'generated', -- Generada por modelo
'pending', -- Pendiente de activacion
'active', -- Activa (precio llego a zona)
'triggered', -- Disparada (orden ejecutada)
'expired', -- Expirada sin activar
'invalidated', -- Invalidada por condiciones
'cancelled' -- Cancelada
);
EXCEPTION
WHEN duplicate_object THEN null;
END $$;
-- Tabla de Senales de Entrada ML
CREATE TABLE IF NOT EXISTS ml.entry_signals (
-- Identificadores
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL,
user_id UUID, -- NULL si es senal publica
-- Referencias
range_prediction_id UUID, -- Prediccion de rango asociada
ticker_id UUID REFERENCES market_data.tickers(id),
-- Simbolo y timeframe
symbol VARCHAR(20) NOT NULL,
asset_class ml.asset_class NOT NULL DEFAULT 'FOREX',
timeframe VARCHAR(10) NOT NULL DEFAULT 'H1',
session VARCHAR(20), -- 'asia', 'london', 'ny'
-- Metodologia
methodology ml.signal_methodology NOT NULL,
amd_phase ml.amd_phase,
-- Direccion
direction VARCHAR(10) NOT NULL CHECK (direction IN ('LONG', 'SHORT')),
-- Estado
status ml.entry_signal_status NOT NULL DEFAULT 'generated',
-- Zona de entrada
entry_zone_high DECIMAL(15, 8) NOT NULL, -- Limite superior de entrada
entry_zone_low DECIMAL(15, 8) NOT NULL, -- Limite inferior de entrada
optimal_entry DECIMAL(15, 8), -- Precio optimo de entrada
-- Stop Loss
stop_loss DECIMAL(15, 8) NOT NULL,
stop_loss_pips DECIMAL(10, 2),
invalidation_price DECIMAL(15, 8), -- Precio que invalida la senal
-- Take Profits
take_profit_1 DECIMAL(15, 8),
take_profit_2 DECIMAL(15, 8),
take_profit_3 DECIMAL(15, 8),
tp1_pips DECIMAL(10, 2),
tp2_pips DECIMAL(10, 2),
tp3_pips DECIMAL(10, 2),
-- Risk/Reward
risk_reward_1 DECIMAL(5, 2), -- RR a TP1
risk_reward_2 DECIMAL(5, 2), -- RR a TP2
risk_reward_3 DECIMAL(5, 2), -- RR a TP3
-- Confianza
confidence_score DECIMAL(5, 4) NOT NULL DEFAULT 0.5
CHECK (confidence_score BETWEEN 0 AND 1),
strength VARCHAR(20), -- 'weak', 'moderate', 'strong', 'very_strong'
quality_grade VARCHAR(2), -- 'A+', 'A', 'B', 'C', 'D'
-- Modelo
model_id VARCHAR(100),
model_version VARCHAR(50) NOT NULL,
-- Contexto tecnico
market_structure JSONB DEFAULT '{}'::JSONB, -- BOS, CHoCH, etc.
key_levels JSONB DEFAULT '[]'::JSONB, -- Niveles clave cercanos
order_blocks JSONB DEFAULT '[]'::JSONB, -- Order blocks detectados
fair_value_gaps JSONB DEFAULT '[]'::JSONB, -- FVGs detectados
liquidity_pools JSONB DEFAULT '[]'::JSONB, -- Pools de liquidez
-- Indicadores al momento
rsi_14 DECIMAL(5, 2),
macd_histogram DECIMAL(15, 8),
atr_14 DECIMAL(15, 8),
volume_ratio DECIMAL(5, 2),
-- Contexto de mercado
market_bias VARCHAR(20), -- 'bullish', 'bearish', 'neutral'
volatility_regime VARCHAR(20), -- 'low', 'medium', 'high', 'extreme'
trend_direction VARCHAR(20), -- 'up', 'down', 'ranging'
session_bias VARCHAR(20), -- Sesgo de la sesion
-- Validez
valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(),
valid_until TIMESTAMPTZ NOT NULL,
-- Ejecucion
activated_at TIMESTAMPTZ, -- Cuando precio llego a zona
triggered_at TIMESTAMPTZ, -- Cuando se ejecuto
triggered_price DECIMAL(15, 8),
slippage DECIMAL(10, 4),
-- Resultado
result VARCHAR(20), -- 'win', 'loss', 'breakeven', 'partial'
exit_price DECIMAL(15, 8),
exit_reason VARCHAR(50),
pnl_pips DECIMAL(10, 2),
pnl_rr DECIMAL(5, 2), -- Resultado en multiplos de RR
-- Trazabilidad
position_id UUID, -- Posicion resultante
-- Features del modelo
features JSONB DEFAULT '{}'::JSONB,
explanation TEXT, -- Explicacion de la senal
-- Notificacion
notified_at TIMESTAMPTZ,
notification_channels VARCHAR(20)[],
-- 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(),
-- Constraints
CONSTRAINT entry_signals_zone_valid CHECK (entry_zone_high >= entry_zone_low)
);
COMMENT ON TABLE ml.entry_signals IS
'Senales de entrada generadas por modelos ML con metodologias ICT/SMC/AMD';
COMMENT ON COLUMN ml.entry_signals.amd_phase IS
'Fase AMD detectada: Accumulation, Manipulation, Distribution';
COMMENT ON COLUMN ml.entry_signals.quality_grade IS
'Calificacion de calidad de la senal: A+, A, B, C, D';
-- Indices
CREATE INDEX IF NOT EXISTS idx_entry_signals_tenant
ON ml.entry_signals(tenant_id);
CREATE INDEX IF NOT EXISTS idx_entry_signals_user
ON ml.entry_signals(user_id)
WHERE user_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_entry_signals_symbol
ON ml.entry_signals(symbol, generated_at DESC);
CREATE INDEX IF NOT EXISTS idx_entry_signals_status
ON ml.entry_signals(status);
CREATE INDEX IF NOT EXISTS idx_entry_signals_active
ON ml.entry_signals(status, valid_until)
WHERE status IN ('pending', 'active');
CREATE INDEX IF NOT EXISTS idx_entry_signals_methodology
ON ml.entry_signals(methodology, direction);
CREATE INDEX IF NOT EXISTS idx_entry_signals_amd
ON ml.entry_signals(amd_phase, generated_at DESC)
WHERE amd_phase IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_entry_signals_confidence
ON ml.entry_signals(confidence_score DESC)
WHERE status IN ('pending', 'active');
CREATE INDEX IF NOT EXISTS idx_entry_signals_generated
ON ml.entry_signals(generated_at DESC);
CREATE INDEX IF NOT EXISTS idx_entry_signals_result
ON ml.entry_signals(result, triggered_at DESC)
WHERE result IS NOT NULL;
-- GIN indices para JSONB
CREATE INDEX IF NOT EXISTS idx_entry_signals_keys_gin
ON ml.entry_signals USING GIN (key_levels);
CREATE INDEX IF NOT EXISTS idx_entry_signals_ob_gin
ON ml.entry_signals USING GIN (order_blocks);
-- Trigger para updated_at
DROP TRIGGER IF EXISTS entry_signal_updated_at ON ml.entry_signals;
CREATE TRIGGER entry_signal_updated_at
BEFORE UPDATE ON ml.entry_signals
FOR EACH ROW
EXECUTE FUNCTION ml.update_ml_timestamp();
-- Trigger para calcular strength basado en confidence
CREATE OR REPLACE FUNCTION ml.calculate_signal_strength()
RETURNS TRIGGER AS $$
BEGIN
NEW.strength := CASE
WHEN NEW.confidence_score >= 0.85 THEN 'very_strong'
WHEN NEW.confidence_score >= 0.70 THEN 'strong'
WHEN NEW.confidence_score >= 0.55 THEN 'moderate'
ELSE 'weak'
END;
-- Calcular pips si hay precios
IF NEW.optimal_entry IS NOT NULL AND NEW.stop_loss IS NOT NULL THEN
NEW.stop_loss_pips := ABS(NEW.optimal_entry - NEW.stop_loss) * 10000;
IF NEW.take_profit_1 IS NOT NULL THEN
NEW.tp1_pips := ABS(NEW.take_profit_1 - NEW.optimal_entry) * 10000;
NEW.risk_reward_1 := NEW.tp1_pips / NULLIF(NEW.stop_loss_pips, 0);
END IF;
IF NEW.take_profit_2 IS NOT NULL THEN
NEW.tp2_pips := ABS(NEW.take_profit_2 - NEW.optimal_entry) * 10000;
NEW.risk_reward_2 := NEW.tp2_pips / NULLIF(NEW.stop_loss_pips, 0);
END IF;
IF NEW.take_profit_3 IS NOT NULL THEN
NEW.tp3_pips := ABS(NEW.take_profit_3 - NEW.optimal_entry) * 10000;
NEW.risk_reward_3 := NEW.tp3_pips / NULLIF(NEW.stop_loss_pips, 0);
END IF;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DROP TRIGGER IF EXISTS entry_signal_calc_strength ON ml.entry_signals;
CREATE TRIGGER entry_signal_calc_strength
BEFORE INSERT OR UPDATE OF confidence_score, optimal_entry, stop_loss, take_profit_1, take_profit_2, take_profit_3
ON ml.entry_signals
FOR EACH ROW
EXECUTE FUNCTION ml.calculate_signal_strength();
-- Funcion para activar senal (precio llego a zona)
CREATE OR REPLACE FUNCTION ml.activate_entry_signal(
p_signal_id UUID,
p_current_price DECIMAL
)
RETURNS BOOLEAN AS $$
DECLARE
v_signal ml.entry_signals;
BEGIN
SELECT * INTO v_signal FROM ml.entry_signals WHERE id = p_signal_id;
IF NOT FOUND OR v_signal.status != 'pending' THEN
RETURN FALSE;
END IF;
-- Verificar si precio esta en zona
IF p_current_price BETWEEN v_signal.entry_zone_low AND v_signal.entry_zone_high THEN
UPDATE ml.entry_signals
SET status = 'active',
activated_at = NOW()
WHERE id = p_signal_id;
RETURN TRUE;
END IF;
RETURN FALSE;
END;
$$ LANGUAGE plpgsql;
-- Funcion para registrar resultado de senal
CREATE OR REPLACE FUNCTION ml.record_signal_result(
p_signal_id UUID,
p_result VARCHAR,
p_exit_price DECIMAL,
p_exit_reason VARCHAR DEFAULT 'manual',
p_position_id UUID DEFAULT NULL
)
RETURNS VOID AS $$
DECLARE
v_signal ml.entry_signals;
BEGIN
SELECT * INTO v_signal FROM ml.entry_signals WHERE id = p_signal_id;
IF NOT FOUND THEN
RETURN;
END IF;
UPDATE ml.entry_signals
SET result = p_result,
exit_price = p_exit_price,
exit_reason = p_exit_reason,
position_id = p_position_id,
pnl_pips = CASE v_signal.direction
WHEN 'LONG' THEN (p_exit_price - v_signal.triggered_price) * 10000
ELSE (v_signal.triggered_price - p_exit_price) * 10000
END,
pnl_rr = CASE
WHEN v_signal.stop_loss_pips > 0 THEN
CASE v_signal.direction
WHEN 'LONG' THEN (p_exit_price - v_signal.triggered_price) * 10000 / v_signal.stop_loss_pips
ELSE (v_signal.triggered_price - p_exit_price) * 10000 / v_signal.stop_loss_pips
END
ELSE NULL
END
WHERE id = p_signal_id;
END;
$$ LANGUAGE plpgsql;
-- Vista de senales activas
CREATE OR REPLACE VIEW ml.v_active_entry_signals AS
SELECT
id,
tenant_id,
symbol,
direction,
methodology,
amd_phase,
status,
entry_zone_high,
entry_zone_low,
optimal_entry,
stop_loss,
take_profit_1,
risk_reward_1,
confidence_score,
strength,
quality_grade,
market_bias,
volatility_regime,
valid_until,
generated_at
FROM ml.entry_signals
WHERE status IN ('pending', 'active')
AND valid_until > NOW()
ORDER BY confidence_score DESC, generated_at DESC;
-- Vista de rendimiento por metodologia
CREATE OR REPLACE VIEW ml.v_signal_methodology_performance AS
SELECT
methodology,
amd_phase,
direction,
COUNT(*) AS total_signals,
COUNT(*) FILTER (WHERE result = 'win') AS wins,
COUNT(*) FILTER (WHERE result = 'loss') AS losses,
COUNT(*) FILTER (WHERE result = 'breakeven') AS breakeven,
ROUND((COUNT(*) FILTER (WHERE result = 'win')::DECIMAL
/ NULLIF(COUNT(*) FILTER (WHERE result IS NOT NULL), 0) * 100), 2) AS win_rate,
ROUND(AVG(confidence_score)::NUMERIC, 4) AS avg_confidence,
ROUND(AVG(pnl_rr) FILTER (WHERE pnl_rr IS NOT NULL)::NUMERIC, 2) AS avg_rr,
ROUND(SUM(pnl_pips) FILTER (WHERE pnl_pips IS NOT NULL)::NUMERIC, 2) AS total_pips
FROM ml.entry_signals
WHERE result IS NOT NULL
GROUP BY methodology, amd_phase, direction
ORDER BY win_rate DESC NULLS LAST;
-- RLS Policies
ALTER TABLE ml.entry_signals ENABLE ROW LEVEL SECURITY;
CREATE POLICY entry_signals_tenant ON ml.entry_signals
FOR ALL
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- Grants
GRANT SELECT, INSERT, UPDATE ON ml.entry_signals TO trading_app;
GRANT SELECT ON ml.entry_signals TO trading_readonly;
GRANT SELECT ON ml.v_active_entry_signals TO trading_app;
GRANT SELECT ON ml.v_signal_methodology_performance TO trading_app;
GRANT EXECUTE ON FUNCTION ml.activate_entry_signal TO trading_app;
GRANT EXECUTE ON FUNCTION ml.record_signal_result TO trading_app;