-- ================================================================================================
-- Migration 044: Advanced Performance Metrics for Paper Trading
-- Adds Sortino ratio, Calmar ratio, VaR, CVaR, maximum drawdown calculations
-- ================================================================================================

-- ================================================================================================
-- Drop existing functions with conflicting names
-- ================================================================================================
DROP FUNCTION IF EXISTS calculate_sharpe_ratio(VARCHAR, INTEGER);
DROP FUNCTION IF EXISTS calculate_max_drawdown(VARCHAR, INTEGER);
DROP FUNCTION IF EXISTS calculate_sortino_ratio(VARCHAR, VARCHAR, INTEGER, DOUBLE PRECISION);
DROP FUNCTION IF EXISTS calculate_calmar_ratio(VARCHAR, VARCHAR, INTEGER);
DROP FUNCTION IF EXISTS calculate_var_95(VARCHAR, VARCHAR, INTEGER);
DROP FUNCTION IF EXISTS calculate_cvar_95(VARCHAR, VARCHAR, INTEGER);
DROP FUNCTION IF EXISTS get_comprehensive_performance_metrics(VARCHAR, INTEGER);

-- ================================================================================================
-- Function: Calculate Sortino Ratio (Downside Risk-Adjusted Returns)
-- Similar to Sharpe but only considers downside volatility
-- ================================================================================================

CREATE OR REPLACE FUNCTION calculate_sortino_ratio(
    p_model_id VARCHAR(50),
    p_symbol VARCHAR(20) DEFAULT NULL,
    p_window_hours INTEGER DEFAULT 24,
    p_risk_free_rate DOUBLE PRECISION DEFAULT 0.0
)
RETURNS DOUBLE PRECISION AS $$
DECLARE
    v_avg_return DOUBLE PRECISION;
    v_downside_std DOUBLE PRECISION;
    v_sortino_ratio DOUBLE PRECISION;
BEGIN
    -- Calculate average return and downside standard deviation
    SELECT
        AVG(pnl),
        STDDEV(CASE WHEN pnl < 0 THEN pnl ELSE NULL END)
    INTO v_avg_return, v_downside_std
    FROM ensemble_predictions
    WHERE
        prediction_timestamp >= NOW() - (p_window_hours || ' hours')::INTERVAL
        AND (p_symbol IS NULL OR symbol = p_symbol)
        AND actual_outcome IS NOT NULL
        AND (
            (p_model_id = 'DQN' AND dqn_vote IS NOT NULL) OR
            (p_model_id = 'PPO' AND ppo_vote IS NOT NULL) OR
            (p_model_id = 'MAMBA2' AND mamba2_vote IS NOT NULL) OR
            (p_model_id = 'TFT' AND tft_vote IS NOT NULL)
        );

    -- Calculate Sortino ratio (annualized)
    IF v_downside_std IS NOT NULL AND v_downside_std > 0 THEN
        v_sortino_ratio := ((v_avg_return - p_risk_free_rate) / v_downside_std) * SQRT(252);
    ELSE
        v_sortino_ratio := NULL;
    END IF;

    RETURN v_sortino_ratio;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION calculate_sortino_ratio IS 'Calculate Sortino ratio (downside risk-adjusted returns) for a model';

-- ================================================================================================
-- Function: Calculate Maximum Drawdown (Peak-to-Trough Decline)
-- ================================================================================================
CREATE OR REPLACE FUNCTION calculate_max_drawdown(
    p_model_id VARCHAR(50),
    p_symbol VARCHAR(20) DEFAULT NULL,
    p_window_hours INTEGER DEFAULT 24
)
RETURNS DOUBLE PRECISION AS $$
DECLARE
    v_max_drawdown DOUBLE PRECISION;
BEGIN
    -- Calculate maximum drawdown using running cumulative P&L
    WITH cumulative_pnl AS (
        SELECT
            prediction_timestamp,
            SUM(pnl) OVER (ORDER BY prediction_timestamp) AS running_pnl
        FROM ensemble_predictions
        WHERE
            prediction_timestamp >= NOW() - (p_window_hours || ' hours')::INTERVAL
            AND (p_symbol IS NULL OR symbol = p_symbol)
            AND actual_outcome IS NOT NULL
            AND (
                (p_model_id = 'DQN' AND dqn_vote IS NOT NULL) OR
                (p_model_id = 'PPO' AND ppo_vote IS NOT NULL) OR
                (p_model_id = 'MAMBA2' AND mamba2_vote IS NOT NULL) OR
                (p_model_id = 'TFT' AND tft_vote IS NOT NULL)
            )
    ),
    running_max AS (
        SELECT
            prediction_timestamp,
            running_pnl,
            MAX(running_pnl) OVER (ORDER BY prediction_timestamp) AS peak_pnl
        FROM cumulative_pnl
    ),
    drawdowns AS (
        SELECT
            (peak_pnl - running_pnl) / NULLIF(ABS(peak_pnl), 0) AS drawdown_pct
        FROM running_max
        WHERE peak_pnl > 0
    )
    SELECT MAX(drawdown_pct)
    INTO v_max_drawdown
    FROM drawdowns;

    RETURN COALESCE(v_max_drawdown, 0.0);
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION calculate_max_drawdown IS 'Calculate maximum peak-to-trough decline for a model';

-- ================================================================================================
-- Function: Calculate Calmar Ratio (Return / Max Drawdown)
-- ================================================================================================
CREATE OR REPLACE FUNCTION calculate_calmar_ratio(
    p_model_id VARCHAR(50),
    p_symbol VARCHAR(20) DEFAULT NULL,
    p_window_hours INTEGER DEFAULT 24
)
RETURNS DOUBLE PRECISION AS $$
DECLARE
    v_annualized_return DOUBLE PRECISION;
    v_max_drawdown DOUBLE PRECISION;
    v_calmar_ratio DOUBLE PRECISION;
BEGIN
    -- Calculate annualized return (sum of P&L over period, annualized)
    SELECT
        (SUM(pnl) / COUNT(*)) * 252  -- Annualize assuming 252 trading days
    INTO v_annualized_return
    FROM ensemble_predictions
    WHERE
        prediction_timestamp >= NOW() - (p_window_hours || ' hours')::INTERVAL
        AND (p_symbol IS NULL OR symbol = p_symbol)
        AND actual_outcome IS NOT NULL
        AND (
            (p_model_id = 'DQN' AND dqn_vote IS NOT NULL) OR
            (p_model_id = 'PPO' AND ppo_vote IS NOT NULL) OR
            (p_model_id = 'MAMBA2' AND mamba2_vote IS NOT NULL) OR
            (p_model_id = 'TFT' AND tft_vote IS NOT NULL)
        );

    -- Get max drawdown
    v_max_drawdown := calculate_max_drawdown(p_model_id, p_symbol, p_window_hours);

    -- Calculate Calmar ratio
    IF v_max_drawdown IS NOT NULL AND v_max_drawdown > 0 THEN
        v_calmar_ratio := v_annualized_return / v_max_drawdown;
    ELSE
        v_calmar_ratio := NULL;
    END IF;

    RETURN v_calmar_ratio;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION calculate_calmar_ratio IS 'Calculate Calmar ratio (annualized return / max drawdown)';

-- ================================================================================================
-- Function: Calculate Value at Risk (VaR) - 95th Percentile Loss
-- ================================================================================================
CREATE OR REPLACE FUNCTION calculate_var_95(
    p_model_id VARCHAR(50),
    p_symbol VARCHAR(20) DEFAULT NULL,
    p_window_hours INTEGER DEFAULT 24
)
RETURNS DOUBLE PRECISION AS $$
DECLARE
    v_var_95 DOUBLE PRECISION;
BEGIN
    -- Calculate 95th percentile of losses (5th percentile of returns)
    SELECT
        PERCENTILE_CONT(0.05) WITHIN GROUP (ORDER BY pnl)
    INTO v_var_95
    FROM ensemble_predictions
    WHERE
        prediction_timestamp >= NOW() - (p_window_hours || ' hours')::INTERVAL
        AND (p_symbol IS NULL OR symbol = p_symbol)
        AND actual_outcome IS NOT NULL
        AND (
            (p_model_id = 'DQN' AND dqn_vote IS NOT NULL) OR
            (p_model_id = 'PPO' AND ppo_vote IS NOT NULL) OR
            (p_model_id = 'MAMBA2' AND mamba2_vote IS NOT NULL) OR
            (p_model_id = 'TFT' AND tft_vote IS NOT NULL)
        );

    RETURN COALESCE(v_var_95, 0.0);
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION calculate_var_95 IS 'Calculate Value at Risk (95th percentile loss)';

-- ================================================================================================
-- Function: Calculate Conditional VaR (CVaR) - Expected Loss Beyond VaR
-- ================================================================================================
CREATE OR REPLACE FUNCTION calculate_cvar_95(
    p_model_id VARCHAR(50),
    p_symbol VARCHAR(20) DEFAULT NULL,
    p_window_hours INTEGER DEFAULT 24
)
RETURNS DOUBLE PRECISION AS $$
DECLARE
    v_var_95 DOUBLE PRECISION;
    v_cvar_95 DOUBLE PRECISION;
BEGIN
    -- Get VaR (5th percentile)
    v_var_95 := calculate_var_95(p_model_id, p_symbol, p_window_hours);

    -- Calculate expected loss beyond VaR (conditional expectation)
    SELECT
        AVG(pnl)
    INTO v_cvar_95
    FROM ensemble_predictions
    WHERE
        prediction_timestamp >= NOW() - (p_window_hours || ' hours')::INTERVAL
        AND (p_symbol IS NULL OR symbol = p_symbol)
        AND actual_outcome IS NOT NULL
        AND pnl <= v_var_95
        AND (
            (p_model_id = 'DQN' AND dqn_vote IS NOT NULL) OR
            (p_model_id = 'PPO' AND ppo_vote IS NOT NULL) OR
            (p_model_id = 'MAMBA2' AND mamba2_vote IS NOT NULL) OR
            (p_model_id = 'TFT' AND tft_vote IS NOT NULL)
        );

    RETURN COALESCE(v_cvar_95, 0.0);
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION calculate_cvar_95 IS 'Calculate Conditional VaR (expected loss beyond VaR threshold)';

-- ================================================================================================
-- Enhanced Performance Metrics Function (with all metrics)
-- Replaces get_real_performance_metrics with comprehensive metrics
-- ================================================================================================
CREATE OR REPLACE FUNCTION get_comprehensive_performance_metrics(
    p_symbol VARCHAR(20) DEFAULT NULL,
    p_window_hours INTEGER DEFAULT 24
)
RETURNS TABLE (
    model_id VARCHAR(50),
    total_predictions INTEGER,
    win_rate DOUBLE PRECISION,
    sharpe_ratio DOUBLE PRECISION,
    sortino_ratio DOUBLE PRECISION,
    calmar_ratio DOUBLE PRECISION,
    max_drawdown DOUBLE PRECISION,
    var_95 DOUBLE PRECISION,
    cvar_95 DOUBLE PRECISION,
    avg_pnl DOUBLE PRECISION,
    total_pnl BIGINT,
    total_trades INTEGER,
    avg_confidence DOUBLE PRECISION
) AS $$
DECLARE
    v_model_ids VARCHAR[] := ARRAY['DQN', 'PPO', 'MAMBA2', 'TFT'];
    v_model_id VARCHAR(50);
BEGIN
    -- Loop through each model and return comprehensive metrics
    FOREACH v_model_id IN ARRAY v_model_ids
    LOOP
        RETURN QUERY
        SELECT
            v_model_id AS model_id,
            COUNT(*)::INTEGER AS total_predictions,
            (COUNT(CASE WHEN actual_outcome = 'WIN' THEN 1 END)::DOUBLE PRECISION / NULLIF(COUNT(*), 0)) AS win_rate,

            -- Sharpe ratio (from existing calculation)
            (AVG(ep.pnl) / NULLIF(STDDEV(ep.pnl), 0)) * SQRT(252) AS sharpe_ratio,

            -- Sortino ratio (call function)
            calculate_sortino_ratio(v_model_id, p_symbol, p_window_hours) AS sortino_ratio,

            -- Calmar ratio (call function)
            calculate_calmar_ratio(v_model_id, p_symbol, p_window_hours) AS calmar_ratio,

            -- Maximum drawdown (call function)
            calculate_max_drawdown(v_model_id, p_symbol, p_window_hours) AS max_drawdown,

            -- VaR 95% (call function)
            calculate_var_95(v_model_id, p_symbol, p_window_hours) AS var_95,

            -- CVaR 95% (call function)
            calculate_cvar_95(v_model_id, p_symbol, p_window_hours) AS cvar_95,

            AVG(ep.pnl) AS avg_pnl,
            SUM(ep.pnl) AS total_pnl,
            COUNT(CASE WHEN actual_outcome IN ('WIN', 'LOSS', 'BREAKEVEN') THEN 1 END)::INTEGER AS total_trades,
            AVG(ep.ensemble_confidence) AS avg_confidence

        FROM ensemble_predictions ep
        WHERE
            ep.prediction_timestamp >= NOW() - (p_window_hours || ' hours')::INTERVAL
            AND (p_symbol IS NULL OR ep.symbol = p_symbol)
            AND ep.actual_outcome IS NOT NULL
            AND (
                (v_model_id = 'DQN' AND ep.dqn_vote IS NOT NULL) OR
                (v_model_id = 'PPO' AND ep.ppo_vote IS NOT NULL) OR
                (v_model_id = 'MAMBA2' AND ep.mamba2_vote IS NOT NULL) OR
                (v_model_id = 'TFT' AND ep.tft_vote IS NOT NULL)
            )
        GROUP BY v_model_id
        HAVING COUNT(*) > 0;  -- Only return models with predictions

    END LOOP;

    RETURN;
END;
$$ LANGUAGE plpgsql;

COMMENT ON FUNCTION get_comprehensive_performance_metrics IS 'Get comprehensive performance metrics including Sharpe, Sortino, Calmar, VaR, CVaR for all models';

-- ================================================================================================
-- Update model_performance_attribution with new fields
-- ================================================================================================
ALTER TABLE model_performance_attribution
ADD COLUMN IF NOT EXISTS var_95 DOUBLE PRECISION,
ADD COLUMN IF NOT EXISTS cvar_95 DOUBLE PRECISION,
ADD COLUMN IF NOT EXISTS calmar_ratio DOUBLE PRECISION;

COMMENT ON COLUMN model_performance_attribution.var_95 IS 'Value at Risk (95th percentile loss)';
COMMENT ON COLUMN model_performance_attribution.cvar_95 IS 'Conditional VaR (expected loss beyond VaR)';
COMMENT ON COLUMN model_performance_attribution.calmar_ratio IS 'Calmar ratio (annualized return / max drawdown)';

-- ================================================================================================
-- Update trigger function to calculate additional metrics
-- ================================================================================================
CREATE OR REPLACE FUNCTION update_model_performance_metrics()
RETURNS TRIGGER AS $$
DECLARE
    v_model_ids VARCHAR[] := ARRAY['DQN', 'PPO', 'MAMBA2', 'TFT'];
    v_model_id VARCHAR(50);
    v_window_hours INTEGER[] := ARRAY[1, 24, 168]; -- 1h, 24h, 1 week
    v_window INTEGER;
    v_total_predictions INTEGER;
    v_correct_predictions INTEGER;
    v_total_pnl BIGINT;
    v_total_trades INTEGER;
    v_winning_trades INTEGER;
    v_avg_pnl DOUBLE PRECISION;
    v_stddev_pnl DOUBLE PRECISION;
    v_sharpe_ratio DOUBLE PRECISION;
    v_sortino_ratio DOUBLE PRECISION;
    v_calmar_ratio DOUBLE PRECISION;
    v_max_drawdown DOUBLE PRECISION;
    v_var_95 DOUBLE PRECISION;
    v_cvar_95 DOUBLE PRECISION;
    v_win_rate DOUBLE PRECISION;
BEGIN
    -- Only recalculate if outcome was just recorded
    IF (TG_OP = 'UPDATE' AND NEW.actual_outcome IS NOT NULL AND OLD.actual_outcome IS NULL) THEN

        -- Loop through each model
        FOREACH v_model_id IN ARRAY v_model_ids
        LOOP
            -- Loop through each window
            FOREACH v_window IN ARRAY v_window_hours
            LOOP
                -- Calculate basic metrics
                SELECT
                    COUNT(*) AS total_predictions,
                    COUNT(CASE WHEN actual_outcome = 'WIN' THEN 1 END) AS correct_predictions,
                    COALESCE(SUM(pnl), 0) AS total_pnl,
                    COUNT(CASE WHEN actual_outcome IN ('WIN', 'LOSS', 'BREAKEVEN') THEN 1 END) AS total_trades,
                    COUNT(CASE WHEN actual_outcome = 'WIN' THEN 1 END) AS winning_trades,
                    AVG(pnl) AS avg_pnl,
                    STDDEV(pnl) AS stddev_pnl
                INTO
                    v_total_predictions, v_correct_predictions, v_total_pnl,
                    v_total_trades, v_winning_trades, v_avg_pnl, v_stddev_pnl
                FROM ensemble_predictions
                WHERE
                    prediction_timestamp >= NOW() - (v_window || ' hours')::INTERVAL
                    AND symbol = NEW.symbol
                    AND actual_outcome IS NOT NULL
                    AND (
                        (v_model_id = 'DQN' AND dqn_vote IS NOT NULL) OR
                        (v_model_id = 'PPO' AND ppo_vote IS NOT NULL) OR
                        (v_model_id = 'MAMBA2' AND mamba2_vote IS NOT NULL) OR
                        (v_model_id = 'TFT' AND tft_vote IS NOT NULL)
                    );

                -- Calculate Sharpe ratio (annualized)
                IF v_stddev_pnl IS NOT NULL AND v_stddev_pnl > 0 THEN
                    v_sharpe_ratio := (v_avg_pnl / v_stddev_pnl) * SQRT(252);
                ELSE
                    v_sharpe_ratio := NULL;
                END IF;

                -- Calculate win rate
                IF v_total_trades > 0 THEN
                    v_win_rate := v_winning_trades::DOUBLE PRECISION / v_total_trades;
                ELSE
                    v_win_rate := 0.0;
                END IF;

                -- Calculate advanced metrics
                v_sortino_ratio := calculate_sortino_ratio(v_model_id, NEW.symbol, v_window);
                v_calmar_ratio := calculate_calmar_ratio(v_model_id, NEW.symbol, v_window);
                v_max_drawdown := calculate_max_drawdown(v_model_id, NEW.symbol, v_window);
                v_var_95 := calculate_var_95(v_model_id, NEW.symbol, v_window);
                v_cvar_95 := calculate_cvar_95(v_model_id, NEW.symbol, v_window);

                -- Upsert into model_performance_attribution
                INSERT INTO model_performance_attribution (
                    model_id, symbol, window_hours,
                    total_predictions, correct_predictions, accuracy,
                    total_pnl, total_trades, winning_trades,
                    sharpe_ratio, sortino_ratio, calmar_ratio,
                    max_drawdown, var_95, cvar_95, win_rate,
                    prediction_timestamp
                )
                VALUES (
                    v_model_id, NEW.symbol, v_window,
                    v_total_predictions, v_correct_predictions,
                    CASE WHEN v_total_predictions > 0 THEN v_correct_predictions::DOUBLE PRECISION / v_total_predictions ELSE 0.0 END,
                    v_total_pnl, v_total_trades, v_winning_trades,
                    v_sharpe_ratio, v_sortino_ratio, v_calmar_ratio,
                    v_max_drawdown, v_var_95, v_cvar_95, v_win_rate,
                    NOW()
                )
                ON CONFLICT (id, prediction_timestamp) DO NOTHING;

            END LOOP;
        END LOOP;

    END IF;

    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

-- Recreate trigger (drop and recreate to use updated function)
DROP TRIGGER IF EXISTS trg_update_model_performance ON ensemble_predictions;
CREATE TRIGGER trg_update_model_performance
    AFTER UPDATE ON ensemble_predictions
    FOR EACH ROW
    WHEN (NEW.actual_outcome IS NOT NULL AND OLD.actual_outcome IS NULL)
    EXECUTE FUNCTION update_model_performance_metrics();

-- ================================================================================================
-- Grant permissions
-- ================================================================================================
GRANT EXECUTE ON FUNCTION calculate_sortino_ratio TO foxhunt;
GRANT EXECUTE ON FUNCTION calculate_max_drawdown TO foxhunt;
GRANT EXECUTE ON FUNCTION calculate_calmar_ratio TO foxhunt;
GRANT EXECUTE ON FUNCTION calculate_var_95 TO foxhunt;
GRANT EXECUTE ON FUNCTION calculate_cvar_95 TO foxhunt;
GRANT EXECUTE ON FUNCTION get_comprehensive_performance_metrics TO foxhunt;

-- ================================================================================================
-- END MIGRATION 044
-- ================================================================================================
