-- Paper Trading Database Schema -- Created: 2025-10-14 -- Purpose: Track ensemble predictions and simulated trades during Phase 1 paper trading -- ============================================================================ -- Table 1: paper_trading_predictions -- ============================================================================ -- Stores every ensemble prediction with per-model votes and simulated execution CREATE TABLE IF NOT EXISTS paper_trading_predictions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), symbol VARCHAR(20) NOT NULL, -- Ensemble decision ensemble_action VARCHAR(10) NOT NULL, -- BUY, SELL, HOLD ensemble_signal DOUBLE PRECISION NOT NULL, -- -1.0 to 1.0 (bearish to bullish) ensemble_confidence DOUBLE PRECISION NOT NULL, -- 0.0 to 1.0 disagreement_rate DOUBLE PRECISION NOT NULL, -- 0.0 to 1.0 (% models disagree) -- Per-model votes (DQN) dqn_signal DOUBLE PRECISION, dqn_confidence DOUBLE PRECISION, dqn_weight DOUBLE PRECISION, -- Per-model votes (PPO) ppo_signal DOUBLE PRECISION, ppo_confidence DOUBLE PRECISION, ppo_weight DOUBLE PRECISION, -- Per-model votes (TFT) - Reserved for Phase 2 tft_signal DOUBLE PRECISION, tft_confidence DOUBLE PRECISION, tft_weight DOUBLE PRECISION, -- Per-model votes (MAMBA-2) - Reserved for Phase 2 mamba2_signal DOUBLE PRECISION, mamba2_confidence DOUBLE PRECISION, mamba2_weight DOUBLE PRECISION, -- Simulated execution (paper trading) executed BOOLEAN DEFAULT FALSE, execution_price DOUBLE PRECISION, -- Price at which trade was executed position_size DOUBLE PRECISION, -- Number of contracts/shares position_value DOUBLE PRECISION, -- USD value of position -- Position tracking entry_price DOUBLE PRECISION, -- Entry price for open positions exit_price DOUBLE PRECISION, -- Exit price when position closed position_duration_seconds INTEGER, -- How long position was held -- P&L tracking pnl DOUBLE PRECISION, -- Realized P&L (USD) pnl_percentage DOUBLE PRECISION, -- Realized P&L (%) commission_fees DOUBLE PRECISION DEFAULT 0.0, -- Simulated commission slippage_cost DOUBLE PRECISION DEFAULT 0.0, -- Simulated slippage -- Baseline comparison (current production strategy) baseline_action VARCHAR(10), -- What current production would have done baseline_pnl DOUBLE PRECISION, -- What current production would have made -- Metadata prediction_latency_us BIGINT, -- Time to generate prediction (microseconds) aggregation_method VARCHAR(50) DEFAULT 'weighted_average', trading_mode VARCHAR(20) DEFAULT 'paper' -- paper, live ); COMMENT ON TABLE paper_trading_predictions IS 'Tracks all ensemble predictions and simulated trades during paper trading validation'; COMMENT ON COLUMN paper_trading_predictions.disagreement_rate IS 'Percentage of models disagreeing with ensemble decision (0.0-1.0)'; COMMENT ON COLUMN paper_trading_predictions.executed IS 'Whether this prediction resulted in a simulated trade'; -- Create indexes separately CREATE INDEX IF NOT EXISTS idx_paper_trading_timestamp ON paper_trading_predictions (timestamp DESC); CREATE INDEX IF NOT EXISTS idx_paper_trading_symbol_timestamp ON paper_trading_predictions (symbol, timestamp DESC); CREATE INDEX IF NOT EXISTS idx_paper_trading_ensemble_action ON paper_trading_predictions (ensemble_action); CREATE INDEX IF NOT EXISTS idx_paper_trading_executed ON paper_trading_predictions (executed); CREATE INDEX IF NOT EXISTS idx_paper_trading_pnl ON paper_trading_predictions (pnl DESC); -- TimescaleDB hypertable for time-series optimization (if TimescaleDB extension available) DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') THEN PERFORM create_hypertable('paper_trading_predictions', 'timestamp', if_not_exists => TRUE); END IF; END $$; -- ============================================================================ -- Table 2: model_performance_attribution -- ============================================================================ -- Rolling window performance metrics per model and symbol CREATE TABLE IF NOT EXISTS model_performance_attribution ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), model_id VARCHAR(50) NOT NULL, -- DQN, PPO, TFT, MAMBA2 symbol VARCHAR(20) NOT NULL, -- Performance metrics total_predictions INTEGER NOT NULL DEFAULT 0, correct_predictions INTEGER NOT NULL DEFAULT 0, accuracy DOUBLE PRECISION NOT NULL DEFAULT 0.0, -- correct / total total_pnl DOUBLE PRECISION NOT NULL DEFAULT 0.0, sharpe_ratio DOUBLE PRECISION, max_drawdown DOUBLE PRECISION, -- Contribution to ensemble avg_weight DOUBLE PRECISION NOT NULL, avg_confidence DOUBLE PRECISION NOT NULL, avg_signal DOUBLE PRECISION, -- Rolling window window_hours INTEGER NOT NULL DEFAULT 24, -- 1, 24, 168 (1h, 1d, 1w) -- Constraints CONSTRAINT valid_model_id CHECK (model_id IN ('DQN', 'PPO', 'TFT', 'MAMBA2', 'TLOB', 'Liquid')), CONSTRAINT valid_accuracy CHECK (accuracy >= 0.0 AND accuracy <= 1.0), CONSTRAINT valid_window CHECK (window_hours IN (1, 24, 168)) ); COMMENT ON TABLE model_performance_attribution IS 'Rolling window performance metrics for each model in the ensemble'; -- Create indexes separately CREATE INDEX IF NOT EXISTS idx_model_perf_model_timestamp ON model_performance_attribution (model_id, timestamp DESC); CREATE INDEX IF NOT EXISTS idx_model_perf_symbol_timestamp ON model_performance_attribution (symbol, timestamp DESC); CREATE INDEX IF NOT EXISTS idx_model_perf_window ON model_performance_attribution (window_hours); -- TimescaleDB hypertable DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'timescaledb') THEN PERFORM create_hypertable('model_performance_attribution', 'timestamp', if_not_exists => TRUE); END IF; END $$; -- ============================================================================ -- Materialized View 1: paper_trading_daily_performance -- ============================================================================ -- Daily aggregated performance summary (refreshed nightly) CREATE MATERIALIZED VIEW IF NOT EXISTS paper_trading_daily_performance AS SELECT DATE(timestamp) AS date, symbol, COUNT(*) AS total_trades, SUM(CASE WHEN pnl > 0 THEN 1 ELSE 0 END) AS winning_trades, SUM(CASE WHEN pnl < 0 THEN 1 ELSE 0 END) AS losing_trades, SUM(CASE WHEN pnl = 0 THEN 1 ELSE 0 END) AS breakeven_trades, -- P&L metrics SUM(pnl) AS total_pnl, AVG(pnl) AS avg_pnl, STDDEV(pnl) AS pnl_stddev, MAX(pnl) AS max_win, MIN(pnl) AS max_loss, -- Ensemble metrics AVG(ensemble_confidence) AS avg_confidence, AVG(disagreement_rate) AS avg_disagreement, MAX(disagreement_rate) AS max_disagreement, -- Model weights AVG(dqn_weight) AS avg_dqn_weight, AVG(ppo_weight) AS avg_ppo_weight, AVG(tft_weight) AS avg_tft_weight, AVG(mamba2_weight) AS avg_mamba2_weight, -- Performance latency AVG(prediction_latency_us) AS avg_prediction_latency_us, PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY prediction_latency_us) AS p99_prediction_latency_us FROM paper_trading_predictions WHERE executed = TRUE GROUP BY DATE(timestamp), symbol ORDER BY date DESC, total_pnl DESC; CREATE INDEX IF NOT EXISTS idx_daily_perf_date_symbol ON paper_trading_daily_performance (date DESC, symbol); COMMENT ON MATERIALIZED VIEW paper_trading_daily_performance IS 'Daily aggregated performance metrics for paper trading validation'; -- ============================================================================ -- Materialized View 2: paper_trading_weekly_summary -- ============================================================================ -- Weekly performance summary for Phase 1 completion report CREATE MATERIALIZED VIEW IF NOT EXISTS paper_trading_weekly_summary AS SELECT DATE_TRUNC('week', timestamp) AS week_start, symbol, COUNT(*) AS total_trades, SUM(CASE WHEN pnl > 0 THEN 1 ELSE 0 END) AS winning_trades, (SUM(CASE WHEN pnl > 0 THEN 1 ELSE 0 END)::FLOAT / NULLIF(COUNT(*), 0) * 100)::NUMERIC(5,2) AS win_rate_pct, -- P&L SUM(pnl) AS total_pnl, AVG(pnl) AS avg_pnl_per_trade, -- Risk metrics MAX(pnl) - MIN(pnl) AS pnl_range, STDDEV(pnl) / NULLIF(AVG(pnl), 0) AS coefficient_of_variation, -- Sharpe ratio (annualized) CASE WHEN STDDEV(pnl) > 0 THEN (AVG(pnl) / STDDEV(pnl)) * SQRT(252) -- 252 trading days ELSE NULL END AS sharpe_ratio, -- Ensemble health AVG(ensemble_confidence) AS avg_confidence, AVG(disagreement_rate) AS avg_disagreement, -- Model contribution AVG(dqn_weight) AS avg_dqn_weight, AVG(ppo_weight) AS avg_ppo_weight, -- Baseline comparison SUM(baseline_pnl) AS baseline_total_pnl, (SUM(pnl) - SUM(baseline_pnl))::NUMERIC(12,2) AS pnl_vs_baseline, ((SUM(pnl) - SUM(baseline_pnl)) / NULLIF(ABS(SUM(baseline_pnl)), 0) * 100)::NUMERIC(5,2) AS pnl_vs_baseline_pct FROM paper_trading_predictions WHERE executed = TRUE GROUP BY DATE_TRUNC('week', timestamp), symbol ORDER BY week_start DESC, total_pnl DESC; CREATE INDEX IF NOT EXISTS idx_weekly_summary_week_symbol ON paper_trading_weekly_summary (week_start DESC, symbol); COMMENT ON MATERIALIZED VIEW paper_trading_weekly_summary IS '7-day rolling summary for Phase 1 completion report'; -- ============================================================================ -- View 1: high_disagreement_events -- ============================================================================ -- Real-time view of high disagreement predictions (>50%) CREATE OR REPLACE VIEW high_disagreement_events AS SELECT timestamp, symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate, dqn_signal, ppo_signal, tft_signal, mamba2_signal, CASE WHEN disagreement_rate > 0.7 THEN 'CRITICAL' WHEN disagreement_rate > 0.5 THEN 'HIGH' ELSE 'NORMAL' END AS disagreement_severity FROM paper_trading_predictions WHERE disagreement_rate > 0.5 ORDER BY timestamp DESC; COMMENT ON VIEW high_disagreement_events IS 'Real-time view of predictions with high model disagreement (>50%)'; -- ============================================================================ -- View 2: model_performance_comparison -- ============================================================================ -- Compare per-model performance for attribution analysis CREATE OR REPLACE VIEW model_performance_comparison AS SELECT symbol, -- DQN metrics AVG(dqn_weight) AS dqn_avg_weight, SUM(CASE WHEN dqn_signal * pnl > 0 THEN pnl ELSE 0 END) AS dqn_pnl_contribution, -- PPO metrics AVG(ppo_weight) AS ppo_avg_weight, SUM(CASE WHEN ppo_signal * pnl > 0 THEN pnl ELSE 0 END) AS ppo_pnl_contribution, -- TFT metrics (Phase 2) AVG(tft_weight) AS tft_avg_weight, SUM(CASE WHEN tft_signal * pnl > 0 THEN pnl ELSE 0 END) AS tft_pnl_contribution, -- MAMBA-2 metrics (Phase 2) AVG(mamba2_weight) AS mamba2_avg_weight, SUM(CASE WHEN mamba2_signal * pnl > 0 THEN pnl ELSE 0 END) AS mamba2_pnl_contribution, -- Totals COUNT(*) AS total_predictions, SUM(pnl) AS total_ensemble_pnl FROM paper_trading_predictions WHERE executed = TRUE AND timestamp >= NOW() - INTERVAL '7 days' GROUP BY symbol ORDER BY total_ensemble_pnl DESC; COMMENT ON VIEW model_performance_comparison IS 'Per-model P&L contribution analysis for 7-day rolling window'; -- ============================================================================ -- Function 1: refresh_paper_trading_views -- ============================================================================ -- Refresh materialized views (call nightly via cron) CREATE OR REPLACE FUNCTION refresh_paper_trading_views() RETURNS void AS $$ BEGIN REFRESH MATERIALIZED VIEW CONCURRENTLY paper_trading_daily_performance; REFRESH MATERIALIZED VIEW CONCURRENTLY paper_trading_weekly_summary; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION refresh_paper_trading_views() IS 'Refresh materialized views for paper trading metrics (run nightly)'; -- ============================================================================ -- Function 2: calculate_sharpe_ratio -- ============================================================================ -- Calculate Sharpe ratio for a given symbol and time window CREATE OR REPLACE FUNCTION calculate_sharpe_ratio( p_symbol VARCHAR(20), p_days INTEGER DEFAULT 7 ) RETURNS NUMERIC AS $$ DECLARE v_avg_return DOUBLE PRECISION; v_stddev DOUBLE PRECISION; v_sharpe NUMERIC; BEGIN SELECT AVG(pnl), STDDEV(pnl) INTO v_avg_return, v_stddev FROM paper_trading_predictions WHERE symbol = p_symbol AND executed = TRUE AND timestamp >= NOW() - (p_days || ' days')::INTERVAL; IF v_stddev IS NULL OR v_stddev = 0 THEN RETURN NULL; END IF; -- Annualized Sharpe ratio (252 trading days) v_sharpe := (v_avg_return / v_stddev) * SQRT(252); RETURN v_sharpe::NUMERIC(10,4); END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION calculate_sharpe_ratio(VARCHAR, INTEGER) IS 'Calculate annualized Sharpe ratio for a symbol over N days'; -- ============================================================================ -- Function 3: calculate_max_drawdown -- ============================================================================ -- Calculate maximum drawdown for a given symbol CREATE OR REPLACE FUNCTION calculate_max_drawdown( p_symbol VARCHAR(20), p_days INTEGER DEFAULT 7 ) RETURNS NUMERIC AS $$ DECLARE v_max_drawdown NUMERIC; BEGIN WITH cumulative_pnl AS ( SELECT timestamp, SUM(pnl) OVER (ORDER BY timestamp) AS cum_pnl FROM paper_trading_predictions WHERE symbol = p_symbol AND executed = TRUE AND timestamp >= NOW() - (p_days || ' days')::INTERVAL ), running_max AS ( SELECT timestamp, cum_pnl, MAX(cum_pnl) OVER (ORDER BY timestamp) AS peak FROM cumulative_pnl ) SELECT MIN((cum_pnl - peak) / NULLIF(peak, 0) * 100) AS max_drawdown_pct INTO v_max_drawdown FROM running_max WHERE peak > 0; RETURN COALESCE(v_max_drawdown, 0)::NUMERIC(10,4); END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION calculate_max_drawdown(VARCHAR, INTEGER) IS 'Calculate maximum drawdown percentage for a symbol over N days'; -- ============================================================================ -- Table 3: paper_trading_circuit_breaker_log -- ============================================================================ -- Log of circuit breaker activations CREATE TABLE IF NOT EXISTS paper_trading_circuit_breaker_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW(), trigger_type VARCHAR(50) NOT NULL, -- max_daily_loss, consecutive_losses, high_disagreement trigger_value DOUBLE PRECISION NOT NULL, threshold_value DOUBLE PRECISION NOT NULL, symbol VARCHAR(20), action_taken VARCHAR(100) NOT NULL, -- halt_trading, reduce_position_size, alert_only resolved_at TIMESTAMPTZ, resolution_notes TEXT ); COMMENT ON TABLE paper_trading_circuit_breaker_log IS 'Log of circuit breaker activations and resolutions'; -- Create indexes separately CREATE INDEX IF NOT EXISTS idx_circuit_breaker_timestamp ON paper_trading_circuit_breaker_log (timestamp DESC); CREATE INDEX IF NOT EXISTS idx_circuit_breaker_trigger_type ON paper_trading_circuit_breaker_log (trigger_type); -- ============================================================================ -- Insert Initial Data (Optional) -- ============================================================================ -- Insert sample data for testing (remove in production) -- Example: Successful trade INSERT INTO paper_trading_predictions ( symbol, ensemble_action, ensemble_signal, ensemble_confidence, disagreement_rate, dqn_signal, dqn_confidence, dqn_weight, ppo_signal, ppo_confidence, ppo_weight, executed, execution_price, position_size, position_value, entry_price, exit_price, pnl, pnl_percentage, baseline_action, baseline_pnl, prediction_latency_us ) VALUES ( 'ES.FUT', 'BUY', 0.75, 0.85, 0.25, 0.8, 0.9, 0.5, 0.7, 0.8, 0.5, TRUE, 4500.00, 2, 9000.00, 4500.00, 4515.00, 30.00, 0.33, 'HOLD', 0.00, 42 ); -- Grant permissions (adjust users as needed) -- GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO foxhunt_app; -- GRANT SELECT ON ALL MATERIALIZED VIEWS IN SCHEMA public TO foxhunt_app; -- GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO foxhunt_app; -- ============================================================================ -- Schema Validation Queries -- ============================================================================ -- Verify tables created SELECT table_name, table_type FROM information_schema.tables WHERE table_schema = 'public' AND table_name LIKE 'paper_trading%' ORDER BY table_name; -- Verify indexes created SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = 'public' AND tablename LIKE 'paper_trading%' ORDER BY tablename, indexname; -- Verify functions created SELECT routine_name, routine_type FROM information_schema.routines WHERE routine_schema = 'public' AND (routine_name LIKE 'calculate_%' OR routine_name LIKE 'refresh_%') ORDER BY routine_name; COMMENT ON SCHEMA public IS 'Paper trading schema created: 2025-10-14'; -- ============================================================================ -- End of Schema -- ============================================================================