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