-- ============================================================================ -- SCHEMA: ml -- TABLE: range_predictions -- DESCRIPTION: Predicciones de rango diario/semanal (High/Low esperados) -- VERSION: 1.0.0 -- CREATED: 2026-01-16 -- SPRINT: Sprint 4 - DDL Implementation Roadmap Q1-2026 -- NOTE: Tabla particionada por fecha para alto volumen -- ============================================================================ -- Enum para tipo de rango DO $$ BEGIN CREATE TYPE ml.range_type AS ENUM ( 'daily', -- Rango diario 'weekly', -- Rango semanal 'session_asia', -- Sesion asiatica 'session_london', -- Sesion Londres 'session_ny', -- Sesion NY 'kill_zone', -- Kill zones ICT 'custom' -- Personalizado ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Enum para resultado de rango DO $$ BEGIN CREATE TYPE ml.range_result AS ENUM ( 'pending', -- Pendiente de validacion 'hit_high', -- Alcanzo high predicho 'hit_low', -- Alcanzo low predicho 'hit_both', -- Alcanzo ambos 'missed', -- No alcanzo ninguno 'partial_high', -- Parcialmente hacia high 'partial_low', -- Parcialmente hacia low 'invalidated' -- Prediccion invalidada ); EXCEPTION WHEN duplicate_object THEN null; END $$; -- Tabla de Predicciones de Rango (particionada) CREATE TABLE IF NOT EXISTS ml.range_predictions ( -- Identificadores id UUID DEFAULT gen_random_uuid(), tenant_id UUID NOT NULL, -- Simbolo y timeframe symbol VARCHAR(20) NOT NULL, asset_class ml.asset_class NOT NULL DEFAULT 'FOREX', range_type ml.range_type NOT NULL DEFAULT 'daily', -- Fecha del rango prediction_date DATE NOT NULL, -- Fecha de la prediccion valid_from TIMESTAMPTZ NOT NULL, -- Inicio de validez valid_until TIMESTAMPTZ NOT NULL, -- Fin de validez -- Prediccion de rango predicted_high DECIMAL(15, 8) NOT NULL, -- High esperado predicted_low DECIMAL(15, 8) NOT NULL, -- Low esperado predicted_open DECIMAL(15, 8), -- Open esperado predicted_close DECIMAL(15, 8), -- Close esperado -- Rangos de confianza high_confidence_upper DECIMAL(15, 8), -- High upper bound high_confidence_lower DECIMAL(15, 8), -- High lower bound low_confidence_upper DECIMAL(15, 8), -- Low upper bound low_confidence_lower DECIMAL(15, 8), -- Low lower bound -- Pips esperados expected_range_pips DECIMAL(10, 2), -- |High - Low| expected_atr_pips DECIMAL(10, 2), -- ATR esperado -- Niveles clave pivot_point DECIMAL(15, 8), resistance_1 DECIMAL(15, 8), resistance_2 DECIMAL(15, 8), support_1 DECIMAL(15, 8), support_2 DECIMAL(15, 8), -- Confianza del modelo confidence_score DECIMAL(5, 4) NOT NULL DEFAULT 0.5 CHECK (confidence_score BETWEEN 0 AND 1), model_version VARCHAR(50) NOT NULL, model_id VARCHAR(100), -- Features usadas features_used JSONB DEFAULT '{}'::JSONB, -- Features del modelo market_context JSONB DEFAULT '{}'::JSONB, -- Contexto de mercado -- Resultado actual result ml.range_result NOT NULL DEFAULT 'pending', actual_high DECIMAL(15, 8), actual_low DECIMAL(15, 8), actual_open DECIMAL(15, 8), actual_close DECIMAL(15, 8), -- Metricas de precision high_error_pips DECIMAL(10, 2), -- Error en prediccion de high low_error_pips DECIMAL(10, 2), -- Error en prediccion de low range_error_percent DECIMAL(5, 2), -- Error porcentual en rango -- Timing time_to_high_minutes INTEGER, -- Tiempo hasta alcanzar high time_to_low_minutes INTEGER, -- Tiempo hasta alcanzar low high_hit_first BOOLEAN, -- Si high se alcanzo primero -- Estadisticas max_drawdown_pips DECIMAL(10, 2), -- Max drawdown desde open volatility_actual DECIMAL(10, 4), -- Volatilidad real -- Metadata notes TEXT, metadata JSONB DEFAULT '{}'::JSONB, -- Timestamps created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), validated_at TIMESTAMPTZ, -- Primary Key incluye columna de particion PRIMARY KEY (id, prediction_date), -- Constraints CONSTRAINT range_pred_high_gt_low CHECK (predicted_high > predicted_low) ) PARTITION BY RANGE (prediction_date); COMMENT ON TABLE ml.range_predictions IS 'Predicciones de rango (high/low) diario/semanal. Particionada por fecha para alto volumen'; -- Crear particiones para 2026 CREATE TABLE IF NOT EXISTS ml.range_predictions_2026_q1 PARTITION OF ml.range_predictions FOR VALUES FROM ('2026-01-01') TO ('2026-04-01'); CREATE TABLE IF NOT EXISTS ml.range_predictions_2026_q2 PARTITION OF ml.range_predictions FOR VALUES FROM ('2026-04-01') TO ('2026-07-01'); CREATE TABLE IF NOT EXISTS ml.range_predictions_2026_q3 PARTITION OF ml.range_predictions FOR VALUES FROM ('2026-07-01') TO ('2026-10-01'); CREATE TABLE IF NOT EXISTS ml.range_predictions_2026_q4 PARTITION OF ml.range_predictions FOR VALUES FROM ('2026-10-01') TO ('2027-01-01'); -- Indices en la tabla padre (se propagan a particiones) CREATE INDEX IF NOT EXISTS idx_range_pred_tenant ON ml.range_predictions(tenant_id); CREATE INDEX IF NOT EXISTS idx_range_pred_symbol ON ml.range_predictions(symbol, prediction_date DESC); CREATE INDEX IF NOT EXISTS idx_range_pred_date ON ml.range_predictions(prediction_date DESC); CREATE INDEX IF NOT EXISTS idx_range_pred_type ON ml.range_predictions(range_type, prediction_date DESC); CREATE INDEX IF NOT EXISTS idx_range_pred_pending ON ml.range_predictions(result, valid_until) WHERE result = 'pending'; CREATE INDEX IF NOT EXISTS idx_range_pred_model ON ml.range_predictions(model_version, prediction_date DESC); -- Funcion de timestamp (si no existe ya) CREATE OR REPLACE FUNCTION ml.update_ml_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at := NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; -- Trigger para updated_at DROP TRIGGER IF EXISTS range_pred_updated_at ON ml.range_predictions; CREATE TRIGGER range_pred_updated_at BEFORE UPDATE ON ml.range_predictions FOR EACH ROW EXECUTE FUNCTION ml.update_ml_timestamp(); -- Funcion para validar prediccion de rango CREATE OR REPLACE FUNCTION ml.validate_range_prediction( p_prediction_id UUID, p_prediction_date DATE, p_actual_high DECIMAL, p_actual_low DECIMAL, p_actual_open DECIMAL DEFAULT NULL, p_actual_close DECIMAL DEFAULT NULL ) RETURNS ml.range_result AS $$ DECLARE v_pred ml.range_predictions; v_result ml.range_result; v_tolerance DECIMAL := 0.0005; -- 5 pips tolerance BEGIN SELECT * INTO v_pred FROM ml.range_predictions WHERE id = p_prediction_id AND prediction_date = p_prediction_date; IF NOT FOUND THEN RETURN NULL; END IF; -- Determinar resultado IF p_actual_high >= v_pred.predicted_high * (1 - v_tolerance) AND p_actual_low <= v_pred.predicted_low * (1 + v_tolerance) THEN v_result := 'hit_both'; ELSIF p_actual_high >= v_pred.predicted_high * (1 - v_tolerance) THEN v_result := 'hit_high'; ELSIF p_actual_low <= v_pred.predicted_low * (1 + v_tolerance) THEN v_result := 'hit_low'; ELSIF p_actual_high >= v_pred.predicted_high * 0.9 THEN v_result := 'partial_high'; ELSIF p_actual_low <= v_pred.predicted_low * 1.1 THEN v_result := 'partial_low'; ELSE v_result := 'missed'; END IF; -- Actualizar prediccion UPDATE ml.range_predictions SET result = v_result, actual_high = p_actual_high, actual_low = p_actual_low, actual_open = p_actual_open, actual_close = p_actual_close, high_error_pips = ABS(p_actual_high - predicted_high) * 10000, low_error_pips = ABS(p_actual_low - predicted_low) * 10000, range_error_percent = ABS((p_actual_high - p_actual_low) - (predicted_high - predicted_low)) / (predicted_high - predicted_low) * 100, validated_at = NOW() WHERE id = p_prediction_id AND prediction_date = p_prediction_date; RETURN v_result; END; $$ LANGUAGE plpgsql; -- Vista de rendimiento de predicciones de rango CREATE OR REPLACE VIEW ml.v_range_prediction_performance AS SELECT symbol, range_type, model_version, COUNT(*) AS total_predictions, COUNT(*) FILTER (WHERE result IN ('hit_both', 'hit_high', 'hit_low')) AS successful, COUNT(*) FILTER (WHERE result = 'missed') AS missed, ROUND((COUNT(*) FILTER (WHERE result IN ('hit_both', 'hit_high', 'hit_low'))::DECIMAL / NULLIF(COUNT(*) FILTER (WHERE result != 'pending'), 0) * 100), 2) AS accuracy_percent, ROUND(AVG(confidence_score)::NUMERIC, 4) AS avg_confidence, ROUND(AVG(high_error_pips)::NUMERIC, 2) AS avg_high_error_pips, ROUND(AVG(low_error_pips)::NUMERIC, 2) AS avg_low_error_pips, MAX(prediction_date) AS last_prediction_date FROM ml.range_predictions WHERE result != 'pending' GROUP BY symbol, range_type, model_version ORDER BY accuracy_percent DESC NULLS LAST; -- Vista de predicciones activas CREATE OR REPLACE VIEW ml.v_active_range_predictions AS SELECT id, tenant_id, symbol, range_type, prediction_date, predicted_high, predicted_low, expected_range_pips, pivot_point, resistance_1, support_1, confidence_score, model_version, valid_until FROM ml.range_predictions WHERE result = 'pending' AND valid_until > NOW() ORDER BY prediction_date DESC, confidence_score DESC; -- RLS Policies ALTER TABLE ml.range_predictions ENABLE ROW LEVEL SECURITY; CREATE POLICY range_pred_tenant ON ml.range_predictions FOR ALL USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID); -- Grants GRANT SELECT, INSERT, UPDATE ON ml.range_predictions TO trading_app; GRANT SELECT ON ml.range_predictions TO trading_readonly; GRANT SELECT ON ml.v_range_prediction_performance TO trading_app; GRANT SELECT ON ml.v_active_range_predictions TO trading_app; GRANT EXECUTE ON FUNCTION ml.validate_range_prediction TO trading_app;