-- ============================================================
-- Stock Signal SQLite Schema
-- Consolidated schema
--
-- This replaces the older schema.sql + phase2/phase3/phase4
-- migration chain for NEW database initialization.
--
-- Canonical lifecycle:
--   prediction -> resolution -> metrics -> retrain flag
-- ============================================================


-- ============================================================
-- 1. PREDICTIONS
-- ============================================================

CREATE TABLE IF NOT EXISTS predictions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,

    -- Model / feature provenance
    model_version TEXT NOT NULL,
    feature_version TEXT NOT NULL,

    -- Prediction timing
    prediction_date DATE NOT NULL,
    prediction_timestamp TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),

    -- Stock
    symbol TEXT NOT NULL,

    -- Model output
    predicted_probability REAL NOT NULL,

    "rank" INTEGER NOT NULL,

    -- Target contract
    target_threshold REAL NOT NULL DEFAULT 0.03,
    target_horizon_days INTEGER NOT NULL DEFAULT 5,

    -- Price at prediction time
    prediction_close REAL,

    -- Resolution / forward evaluation
    entry_date DATE,
    entry_open REAL,
    max_close_5d REAL,
    evaluation_end_date DATE,

    actual_return REAL,
    actual_label INTEGER,

    resolved_at TIMESTAMP,

    created_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),

    -- One prediction per symbol/model/date.
    CONSTRAINT uq_predictions_date_symbol_model
        UNIQUE (prediction_date, symbol, model_version),

    CONSTRAINT predictions_probability_check
        CHECK (
            predicted_probability >= 0
            AND predicted_probability <= 1
        ),

    -- Binary classification target.
    CONSTRAINT predictions_actual_label_check
        CHECK (actual_label IS NULL OR actual_label IN (0, 1)),

    -- Target contract validation.
    CONSTRAINT predictions_target_threshold_check
        CHECK (target_threshold >= 0),

    CONSTRAINT predictions_target_horizon_check
        CHECK (target_horizon_days > 0),

    -- Rank must be positive.
    CONSTRAINT predictions_rank_check
        CHECK ("rank" > 0)
);


CREATE INDEX IF NOT EXISTS idx_predictions_date
    ON predictions (prediction_date);

CREATE INDEX IF NOT EXISTS idx_predictions_symbol
    ON predictions (symbol);

CREATE INDEX IF NOT EXISTS idx_predictions_unresolved
    ON predictions (prediction_date, symbol)
    WHERE actual_label IS NULL;

CREATE INDEX IF NOT EXISTS idx_predictions_resolution
    ON predictions (prediction_date, symbol, actual_label);

CREATE INDEX IF NOT EXISTS idx_predictions_model_date
    ON predictions (model_version, prediction_date);


-- ============================================================
-- 2. JOB RUNS
-- ============================================================

CREATE TABLE IF NOT EXISTS job_runs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,

    job_name TEXT NOT NULL,
    run_date DATE NOT NULL,

    started_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),
    finished_at TIMESTAMP,

    status TEXT NOT NULL
    CHECK (
        status IN (
            'RUNNING',
            'SUCCESS',
            'FAILED',
            'SKIPPED'
        )
    ),

    rows_processed INTEGER NOT NULL DEFAULT 0,
    total_rows INTEGER NOT NULL DEFAULT 0,
    resolved_rows INTEGER NOT NULL DEFAULT 0,
    failed_rows INTEGER NOT NULL DEFAULT 0,
    last_updated_at TIMESTAMP,

    error_message TEXT
);


CREATE INDEX IF NOT EXISTS idx_job_runs_name_date
    ON job_runs (job_name, run_date);

-- Cross-process lease locks for singleton SQLite jobs. The token prevents an
-- expired owner from releasing a newer owner's lease.
CREATE TABLE IF NOT EXISTS job_locks (
    lock_name TEXT PRIMARY KEY,
    owner_token TEXT NOT NULL,
    locked_until TIMESTAMP NOT NULL
);


-- ============================================================
-- 3. PREDICTION METRICS
-- ============================================================

CREATE TABLE IF NOT EXISTS prediction_metrics (
    id INTEGER PRIMARY KEY AUTOINCREMENT,

    window_start DATE NOT NULL,
    window_end DATE NOT NULL,

    prediction_days INTEGER NOT NULL,
    resolved_predictions INTEGER NOT NULL,

    precision REAL,
    pr_auc REAL,
    hit_rate REAL,
    positive_rate REAL,
    average_return REAL,

    metrics_json TEXT NOT NULL DEFAULT '{}',

    calculated_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),

    model_version TEXT NOT NULL,

    CONSTRAINT prediction_metrics_window_uq
        UNIQUE (model_version, window_start, window_end),

    CONSTRAINT prediction_metrics_window_ck
        CHECK (window_start <= window_end),

    CONSTRAINT prediction_metrics_prediction_days_ck
        CHECK (prediction_days > 0),

    CONSTRAINT prediction_metrics_resolved_ck
        CHECK (resolved_predictions >= 0)
);


CREATE INDEX IF NOT EXISTS idx_prediction_metrics_window_end
    ON prediction_metrics (window_end DESC);


-- ============================================================
-- 4. RETRAIN FLAGS
-- ============================================================

CREATE TABLE IF NOT EXISTS retrain_flags (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    model_version TEXT NOT NULL,
    triggered_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),

    metric_window_start DATE NOT NULL,
    metric_window_end DATE NOT NULL,

    baseline_precision REAL NOT NULL,
    current_precision REAL NOT NULL,
    deterioration REAL NOT NULL,

    deterioration_threshold REAL NOT NULL DEFAULT 0.15,

    status TEXT NOT NULL DEFAULT 'PENDING'
        CHECK (
            status IN (
                'PENDING',
                'ACKNOWLEDGED',
                'RESOLVED',
                'IGNORED'
            )
        ),

    reason TEXT NOT NULL,

    resolved_at TIMESTAMP,
    notes TEXT,

    CONSTRAINT retrain_flags_window_uq
        UNIQUE (model_version, metric_window_start, metric_window_end)
);


CREATE INDEX IF NOT EXISTS idx_retrain_flags_status
    ON retrain_flags (status, triggered_at DESC);


-- ============================================================
-- END OF CONSOLIDATED SCHEMA
-- ============================================================

-- ============================================================
-- 5. MARKET DATA REGISTRY AND YFINANCE OHLCV
-- ============================================================

CREATE TABLE IF NOT EXISTS market_tickers (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    symbol TEXT NOT NULL UNIQUE,
    yfinance_symbol TEXT NOT NULL UNIQUE,
    market TEXT NOT NULL CHECK (market IN ('NSE', 'US', 'CONTEXT')),
    name TEXT,
    industry TEXT,
    active BOOLEAN NOT NULL DEFAULT TRUE,
    created_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),
    updated_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now'))
);

CREATE TABLE IF NOT EXISTS market_ohlcv (
    ticker_id INTEGER NOT NULL REFERENCES market_tickers(id) ON DELETE CASCADE,
    trading_date DATE NOT NULL,
    open REAL NOT NULL,
    high REAL NOT NULL,
    low REAL NOT NULL,
    close REAL NOT NULL,
    volume INTEGER,
    source TEXT NOT NULL DEFAULT 'yfinance',
    fetched_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),
    PRIMARY KEY (ticker_id, trading_date)
);

CREATE INDEX IF NOT EXISTS idx_market_ohlcv_ticker_date
    ON market_ohlcv (ticker_id, trading_date DESC);

-- ============================================================
-- 6. MARKET DATA BY DATASET
--
-- The legacy market_ohlcv table remains only as a migration source for an
-- existing installation.  New reads and writes use these dataset tables.
-- ============================================================

CREATE TABLE IF NOT EXISTS ohlcv (
    ticker_id INTEGER NOT NULL REFERENCES market_tickers(id) ON DELETE CASCADE,
    trading_date DATE NOT NULL,
    open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL,
    volume INTEGER, source TEXT NOT NULL DEFAULT 'yfinance',
    fetched_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),
    PRIMARY KEY (ticker_id, trading_date)
);

CREATE TABLE IF NOT EXISTS nifty_ohlcv (
    ticker_id INTEGER NOT NULL REFERENCES market_tickers(id) ON DELETE CASCADE,
    trading_date DATE NOT NULL,
    open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL,
    volume INTEGER, source TEXT NOT NULL DEFAULT 'yfinance',
    fetched_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),
    PRIMARY KEY (ticker_id, trading_date)
);

CREATE TABLE IF NOT EXISTS india_vix_ohlcv (
    ticker_id INTEGER NOT NULL REFERENCES market_tickers(id) ON DELETE CASCADE,
    trading_date DATE NOT NULL,
    open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL,
    volume INTEGER, source TEXT NOT NULL DEFAULT 'yfinance',
    fetched_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),
    PRIMARY KEY (ticker_id, trading_date)
);

CREATE TABLE IF NOT EXISTS us_stocks_ohlcv (
    ticker_id INTEGER NOT NULL REFERENCES market_tickers(id) ON DELETE CASCADE,
    trading_date DATE NOT NULL,
    open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL,
    volume INTEGER, source TEXT NOT NULL DEFAULT 'yfinance',
    fetched_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),
    PRIMARY KEY (ticker_id, trading_date)
);

CREATE TABLE IF NOT EXISTS macro_ohlcv (
    ticker_id INTEGER NOT NULL REFERENCES market_tickers(id) ON DELETE CASCADE,
    macro_type TEXT NOT NULL CHECK (macro_type IN ('gold', 'brent_crude_oil', 'usd_inr')),
    trading_date DATE NOT NULL,
    open REAL NOT NULL, high REAL NOT NULL, low REAL NOT NULL, close REAL NOT NULL,
    volume INTEGER, source TEXT NOT NULL DEFAULT 'yfinance',
    fetched_at TIMESTAMP NOT NULL DEFAULT (STRFTIME('%Y-%m-%dT%H:%M:%fZ', 'now')),
    PRIMARY KEY (ticker_id, trading_date)
);

CREATE VIEW IF NOT EXISTS all_market_ohlcv AS
SELECT ticker_id, trading_date, open, high, low, close, volume, source, fetched_at FROM ohlcv
UNION ALL SELECT ticker_id, trading_date, open, high, low, close, volume, source, fetched_at FROM nifty_ohlcv
UNION ALL SELECT ticker_id, trading_date, open, high, low, close, volume, source, fetched_at FROM india_vix_ohlcv
UNION ALL SELECT ticker_id, trading_date, open, high, low, close, volume, source, fetched_at FROM us_stocks_ohlcv
UNION ALL SELECT ticker_id, trading_date, open, high, low, close, volume, source, fetched_at FROM macro_ohlcv;

-- Copy legacy data exactly once when upgrading an existing database.
INSERT OR IGNORE INTO ohlcv SELECT o.* FROM market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id WHERE t.market = 'NSE';
INSERT OR IGNORE INTO nifty_ohlcv SELECT o.* FROM market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id WHERE t.symbol = 'NIFTY';
INSERT OR IGNORE INTO india_vix_ohlcv SELECT o.* FROM market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id WHERE t.symbol = 'INDIA_VIX';
INSERT OR IGNORE INTO us_stocks_ohlcv SELECT o.* FROM market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id WHERE t.market = 'US';
INSERT OR IGNORE INTO macro_ohlcv (ticker_id, macro_type, trading_date, open, high, low, close, volume, source, fetched_at)
SELECT o.ticker_id, CASE t.symbol WHEN 'GOLD' THEN 'gold' WHEN 'BRENT' THEN 'brent_crude_oil' WHEN 'USDINR' THEN 'usd_inr' END,
       o.trading_date, o.open, o.high, o.low, o.close, o.volume, o.source, o.fetched_at
FROM market_ohlcv o JOIN market_tickers t ON t.id = o.ticker_id WHERE t.symbol IN ('GOLD', 'BRENT', 'USDINR');

-- Derived feature data lives in Parquet beside this database, under
-- data/db/feature_store (or the equivalent directory for DATABASE_URL).
