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