-- ================================================================================================ -- Migration 002: Risk Events Schema -- Comprehensive risk management event storage with real-time monitoring -- Production-ready with compliance, stress testing, and alert capabilities -- ================================================================================================ -- ================================================================================================ -- RISK EVENT TYPES AND ENUMS -- Comprehensive classification for all risk-related events -- ================================================================================================ CREATE TYPE risk_event_type AS ENUM ( 'var_breach', 'exposure_limit_breach', 'position_limit_breach', 'concentration_risk', 'leverage_excess', 'margin_call', 'drawdown_limit', 'volatility_spike', 'correlation_breakdown', 'liquidity_shortage', 'stress_test_failure', 'compliance_violation', 'model_validation_error', 'circuit_breaker_triggered', 'emergency_shutdown', 'risk_limit_update', 'model_recalibration', 'backtest_failure' ); CREATE TYPE risk_severity AS ENUM ( 'info', -- Information only 'low', -- Minor risk, monitoring required 'medium', -- Elevated risk, caution advised 'high', -- Significant risk, action may be required 'critical', -- Immediate action required 'emergency' -- System shutdown level risk ); CREATE TYPE risk_action_type AS ENUM ( 'alert_only', 'reduce_position', 'close_position', 'halt_trading', 'reduce_leverage', 'increase_margin', 'manual_intervention', 'system_shutdown', 'compliance_review' ); CREATE TYPE risk_metric_type AS ENUM ( 'var_1d', 'var_10d', 'cvar_1d', 'cvar_10d', 'exposure_gross', 'exposure_net', 'leverage_ratio', 'concentration_single', 'concentration_sector', 'beta_portfolio', 'sharpe_ratio', 'max_drawdown', 'volatility_realized', 'volatility_implied', 'correlation_matrix', 'margin_excess', 'margin_requirement', 'liquidity_score' ); -- ================================================================================================ -- RISK EVENTS TABLE -- Immutable event store for all risk-related activities -- ================================================================================================ CREATE TABLE risk_events ( -- Primary identifiers id UUID DEFAULT uuid_generate_v4(), event_id BIGSERIAL NOT NULL, correlation_id UUID NOT NULL, -- Timing with nanosecond precision event_timestamp ns_timestamp NOT NULL, detected_timestamp ns_timestamp NOT NULL, acknowledged_timestamp ns_timestamp, resolved_timestamp ns_timestamp, -- Risk event classification event_type risk_event_type NOT NULL, severity risk_severity NOT NULL, risk_metric risk_metric_type, -- Risk context symbol VARCHAR(32), account_id VARCHAR(64), strategy_id VARCHAR(100), portfolio_id VARCHAR(100), -- Risk values and thresholds threshold_value DECIMAL(20, 8), actual_value DECIMAL(20, 8), breach_percentage DECIMAL(8, 4), -- How much threshold was exceeded by risk_score DECIMAL(10, 6), -- Normalized risk score 0-1 -- Event details description TEXT NOT NULL, risk_model VARCHAR(100), -- Which risk model detected this model_version VARCHAR(50), -- Actions and responses recommended_action risk_action_type, action_taken risk_action_type, action_details JSONB, automated_response BOOLEAN DEFAULT FALSE, -- System context source_system VARCHAR(100) NOT NULL, node_id VARCHAR(50) NOT NULL, process_id INTEGER NOT NULL, -- Audit and compliance acknowledged_by VARCHAR(64), resolved_by VARCHAR(64), escalated_to VARCHAR(64), compliance_notification_sent BOOLEAN DEFAULT FALSE, -- Additional data event_data JSONB NOT NULL, -- Complete risk event payload metadata JSONB, -- Partition key event_date DATE NOT NULL, -- Constraints PRIMARY KEY (id, event_date), CONSTRAINT chk_risk_timestamps CHECK ( detected_timestamp >= event_timestamp AND (acknowledged_timestamp IS NULL OR acknowledged_timestamp >= detected_timestamp) AND (resolved_timestamp IS NULL OR resolved_timestamp >= COALESCE(acknowledged_timestamp, detected_timestamp)) ), CONSTRAINT chk_breach_percentage CHECK ( breach_percentage IS NULL OR breach_percentage >= 0 ) ) PARTITION BY RANGE (event_date); -- ================================================================================================ -- RISK METRICS TABLE -- Current and historical risk metric values -- ================================================================================================ CREATE TABLE risk_metrics ( -- Primary identifiers id UUID DEFAULT uuid_generate_v4(), metric_name risk_metric_type NOT NULL, -- Scope identifiers symbol VARCHAR(32), -- NULL for portfolio-level metrics account_id VARCHAR(64), strategy_id VARCHAR(100), portfolio_id VARCHAR(100), -- Metric values value DECIMAL(20, 8) NOT NULL, confidence_interval_lower DECIMAL(20, 8), confidence_interval_upper DECIMAL(20, 8), confidence_level DECIMAL(5, 4) DEFAULT 0.95, -- 95% confidence by default -- Thresholds and limits warning_threshold DECIMAL(20, 8), breach_threshold DECIMAL(20, 8), emergency_threshold DECIMAL(20, 8), -- Calculation context calculation_timestamp ns_timestamp NOT NULL, data_timestamp ns_timestamp NOT NULL, -- Timestamp of underlying data model_name VARCHAR(100) NOT NULL, model_version VARCHAR(50) NOT NULL, calculation_method VARCHAR(200), -- Time horizon and parameters time_horizon_days INTEGER, lookback_days INTEGER, confidence_level_pct DECIMAL(5, 2), -- Status and validation is_valid BOOLEAN DEFAULT TRUE, validation_errors TEXT[], last_updated ns_timestamp NOT NULL, -- Additional context market_conditions JSONB, -- Market state when calculated calculation_details JSONB, -- Model parameters and inputs -- Partition key metric_date DATE NOT NULL, -- Constraints PRIMARY KEY (id, metric_date), CONSTRAINT chk_confidence_level CHECK (confidence_level > 0 AND confidence_level <= 1), CONSTRAINT chk_time_horizons CHECK ( time_horizon_days IS NULL OR time_horizon_days > 0 ), CONSTRAINT chk_calculation_timestamps CHECK ( calculation_timestamp >= data_timestamp ) ) PARTITION BY RANGE (metric_date); -- ================================================================================================ -- RISK LIMITS TABLE -- Configurable risk limits and thresholds -- ================================================================================================ CREATE TABLE risk_limits ( -- Primary identifiers id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), limit_name VARCHAR(200) NOT NULL, limit_type risk_metric_type NOT NULL, -- Scope (hierarchy: global -> account -> strategy -> symbol) scope_level VARCHAR(20) NOT NULL CHECK (scope_level IN ('global', 'account', 'strategy', 'symbol')), account_id VARCHAR(64), strategy_id VARCHAR(100), symbol VARCHAR(32), -- Limit values warning_threshold DECIMAL(20, 8), breach_threshold DECIMAL(20, 8) NOT NULL, emergency_threshold DECIMAL(20, 8), -- Time-based limits intraday_limit DECIMAL(20, 8), daily_limit DECIMAL(20, 8), weekly_limit DECIMAL(20, 8), monthly_limit DECIMAL(20, 8), -- Limit behavior is_active BOOLEAN DEFAULT TRUE, is_hard_limit BOOLEAN DEFAULT FALSE, -- If true, system enforces automatically breach_action risk_action_type DEFAULT 'alert_only', -- Timing and validity effective_from TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), effective_to TIMESTAMP WITH TIME ZONE, time_zone VARCHAR(50) DEFAULT 'UTC', -- Approval and audit approved_by VARCHAR(64) NOT NULL, approval_timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), created_by VARCHAR(64) NOT NULL, last_modified_by VARCHAR(64), -- Change tracking created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), version INTEGER NOT NULL DEFAULT 1, -- Additional configuration limit_details JSONB, -- Additional limit parameters override_permissions TEXT[], -- Who can override this limit -- Constraints (unique constraint moved to expression index below) CONSTRAINT chk_threshold_order CHECK ( warning_threshold IS NULL OR breach_threshold IS NULL OR warning_threshold <= breach_threshold ), CONSTRAINT chk_scope_consistency CHECK ( (scope_level = 'global' AND account_id IS NULL AND strategy_id IS NULL AND symbol IS NULL) OR (scope_level = 'account' AND account_id IS NOT NULL AND strategy_id IS NULL AND symbol IS NULL) OR (scope_level = 'strategy' AND account_id IS NOT NULL AND strategy_id IS NOT NULL AND symbol IS NULL) OR (scope_level = 'symbol' AND symbol IS NOT NULL) ) ); -- Expression index for risk_limits uniqueness CREATE UNIQUE INDEX uk_risk_limits_unique ON risk_limits ( limit_type, scope_level, COALESCE(account_id, ''), COALESCE(strategy_id, ''), COALESCE(symbol, '') ); -- ================================================================================================ -- STRESS TEST SCENARIOS TABLE -- Predefined stress test scenarios and results -- ================================================================================================ CREATE TABLE stress_test_scenarios ( -- Primary identifiers id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), scenario_name VARCHAR(200) NOT NULL UNIQUE, scenario_type VARCHAR(100) NOT NULL, -- 'historical', 'hypothetical', 'monte_carlo' -- Scenario definition description TEXT NOT NULL, stress_parameters JSONB NOT NULL, -- Market movements, shocks, etc. test_duration_days INTEGER DEFAULT 1, -- Execution details is_active BOOLEAN DEFAULT TRUE, frequency_hours INTEGER DEFAULT 24, -- How often to run this scenario last_executed TIMESTAMP WITH TIME ZONE, next_scheduled TIMESTAMP WITH TIME ZONE, -- Validation and approval created_by VARCHAR(64) NOT NULL, approved_by VARCHAR(64), approval_date TIMESTAMP WITH TIME ZONE, -- Change tracking created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), version INTEGER NOT NULL DEFAULT 1 ); -- ================================================================================================ -- STRESS TEST RESULTS TABLE -- Results from stress test executions -- ================================================================================================ CREATE TABLE stress_test_results ( -- Primary identifiers id UUID DEFAULT uuid_generate_v4(), scenario_id UUID NOT NULL REFERENCES stress_test_scenarios(id), execution_id UUID NOT NULL, -- Groups results from same execution -- Execution context execution_timestamp ns_timestamp NOT NULL, portfolio_snapshot_id UUID, -- Reference to portfolio state at test time market_data_timestamp ns_timestamp, -- Scope of test account_id VARCHAR(64), strategy_id VARCHAR(100), symbol VARCHAR(32), -- Results base_value DECIMAL(20, 8) NOT NULL, -- Portfolio value before stress stressed_value DECIMAL(20, 8) NOT NULL, -- Portfolio value after stress pnl_impact DECIMAL(20, 8) NOT NULL, -- Profit/Loss impact percentage_impact DECIMAL(8, 4) NOT NULL, -- Percentage change -- Risk metrics under stress stressed_var DECIMAL(20, 8), stressed_volatility DECIMAL(10, 6), stressed_correlation DECIMAL(6, 4), max_drawdown DECIMAL(8, 4), -- Test verdict test_passed BOOLEAN NOT NULL, failure_reason TEXT, risk_score DECIMAL(10, 6), -- Overall risk score after stress -- Additional details detailed_results JSONB, -- Breakdown by position, factor, etc. calculation_time_ms INTEGER, -- How long the calculation took -- Partition key execution_date DATE NOT NULL, -- Constraints PRIMARY KEY (id, execution_date) ) PARTITION BY RANGE (execution_date); -- ================================================================================================ -- RISK DASHBOARD MATERIALIZED VIEW -- Real-time risk monitoring dashboard -- ================================================================================================ CREATE MATERIALIZED VIEW mv_risk_dashboard AS SELECT -- Scope identifiers COALESCE(rm.account_id, 'ALL') as account_id, COALESCE(rm.strategy_id, 'ALL') as strategy_id, COALESCE(rm.symbol, 'ALL') as symbol, -- Current risk metrics rm.metric_name, rm.value as current_value, rm.warning_threshold, rm.breach_threshold, rm.emergency_threshold, -- Risk status CASE WHEN rm.value > COALESCE(rm.emergency_threshold, rm.breach_threshold) THEN 'emergency' WHEN rm.value > rm.breach_threshold THEN 'critical' WHEN rm.value > COALESCE(rm.warning_threshold, rm.breach_threshold * 0.8) THEN 'warning' ELSE 'normal' END as risk_status, -- Utilization percentages CASE WHEN rm.breach_threshold > 0 THEN (rm.value / rm.breach_threshold * 100) ELSE 0 END as threshold_utilization_pct, -- Timing rm.calculation_timestamp, rm.last_updated, -- Recent events (SELECT COUNT(*) FROM risk_events re WHERE re.event_timestamp >= EXTRACT(EPOCH FROM (NOW() - INTERVAL '1 hour')) * 1000000000 AND re.severity IN ('high', 'critical', 'emergency') AND (re.account_id = rm.account_id OR rm.account_id IS NULL) AND (re.strategy_id = rm.strategy_id OR rm.strategy_id IS NULL) AND (re.symbol = rm.symbol OR rm.symbol IS NULL) ) as recent_high_severity_events, -- Model information rm.model_name, rm.model_version, rm.is_valid FROM risk_metrics rm WHERE rm.calculation_timestamp >= EXTRACT(EPOCH FROM (NOW() - INTERVAL '4 hours')) * 1000000000 AND rm.is_valid = TRUE -- Get the most recent metric for each combination AND rm.calculation_timestamp = ( SELECT MAX(rm2.calculation_timestamp) FROM risk_metrics rm2 WHERE rm2.metric_name = rm.metric_name AND COALESCE(rm2.account_id, '') = COALESCE(rm.account_id, '') AND COALESCE(rm2.strategy_id, '') = COALESCE(rm.strategy_id, '') AND COALESCE(rm2.symbol, '') = COALESCE(rm.symbol, '') AND rm2.calculation_timestamp >= EXTRACT(EPOCH FROM (NOW() - INTERVAL '4 hours')) * 1000000000 AND rm2.is_valid = TRUE ); -- ================================================================================================ -- HIGH-PERFORMANCE INDEXES -- ================================================================================================ -- Risk events indexes CREATE INDEX idx_risk_events_timestamp ON risk_events USING BTREE (event_timestamp); CREATE INDEX idx_risk_events_severity_timestamp ON risk_events USING BTREE (severity, event_timestamp); CREATE INDEX idx_risk_events_type_timestamp ON risk_events USING BTREE (event_type, event_timestamp); CREATE INDEX idx_risk_events_account ON risk_events USING BTREE (account_id, event_timestamp) WHERE account_id IS NOT NULL; CREATE INDEX idx_risk_events_symbol ON risk_events USING BTREE (symbol, event_timestamp) WHERE symbol IS NOT NULL; CREATE INDEX idx_risk_events_unresolved ON risk_events USING BTREE (severity, event_timestamp) WHERE resolved_timestamp IS NULL; CREATE INDEX idx_risk_events_correlation ON risk_events USING HASH (correlation_id); -- GIN indexes for JSONB fields CREATE INDEX idx_risk_events_data_gin ON risk_events USING GIN (event_data); CREATE INDEX idx_risk_events_action_details_gin ON risk_events USING GIN (action_details); -- Risk metrics indexes CREATE INDEX idx_risk_metrics_timestamp ON risk_metrics USING BTREE (calculation_timestamp); CREATE INDEX idx_risk_metrics_name_scope ON risk_metrics USING BTREE (metric_name, account_id, strategy_id, symbol); CREATE INDEX idx_risk_metrics_account_timestamp ON risk_metrics USING BTREE (account_id, calculation_timestamp) WHERE account_id IS NOT NULL; CREATE INDEX idx_risk_metrics_symbol_timestamp ON risk_metrics USING BTREE (symbol, calculation_timestamp) WHERE symbol IS NOT NULL; CREATE INDEX idx_risk_metrics_valid ON risk_metrics USING BTREE (metric_name, calculation_timestamp) WHERE is_valid = TRUE; -- Risk limits indexes CREATE INDEX idx_risk_limits_scope ON risk_limits USING BTREE (limit_type, scope_level); CREATE INDEX idx_risk_limits_account ON risk_limits USING BTREE (account_id) WHERE account_id IS NOT NULL; CREATE INDEX idx_risk_limits_active ON risk_limits USING BTREE (limit_type, is_active) WHERE is_active = TRUE; CREATE INDEX idx_risk_limits_effective ON risk_limits USING BTREE (effective_from, effective_to); -- Stress test indexes CREATE INDEX idx_stress_test_results_execution ON stress_test_results USING BTREE (execution_id, execution_timestamp); CREATE INDEX idx_stress_test_results_scenario ON stress_test_results USING BTREE (scenario_id, execution_timestamp); CREATE INDEX idx_stress_test_results_account ON stress_test_results USING BTREE (account_id, execution_timestamp) WHERE account_id IS NOT NULL; -- ================================================================================================ -- TRIGGER FUNCTIONS FOR GENERATED COLUMNS (converted from GENERATED ALWAYS) -- ================================================================================================ -- Set event_date for risk_events CREATE OR REPLACE FUNCTION set_risk_event_date() RETURNS TRIGGER AS $$ BEGIN NEW.event_date := DATE(TO_TIMESTAMP(NEW.event_timestamp / 1000000000.0)); RETURN NEW; END; $$ LANGUAGE plpgsql IMMUTABLE; CREATE TRIGGER tg_set_risk_event_date BEFORE INSERT OR UPDATE ON risk_events FOR EACH ROW EXECUTE FUNCTION set_risk_event_date(); -- Set metric_date for risk_metrics CREATE OR REPLACE FUNCTION set_risk_metric_date() RETURNS TRIGGER AS $$ BEGIN NEW.metric_date := DATE(TO_TIMESTAMP(NEW.calculation_timestamp / 1000000000.0)); RETURN NEW; END; $$ LANGUAGE plpgsql IMMUTABLE; CREATE TRIGGER tg_set_risk_metric_date BEFORE INSERT OR UPDATE ON risk_metrics FOR EACH ROW EXECUTE FUNCTION set_risk_metric_date(); -- Set execution_date for stress_test_results CREATE OR REPLACE FUNCTION set_stress_test_execution_date() RETURNS TRIGGER AS $$ BEGIN NEW.execution_date := DATE(TO_TIMESTAMP(NEW.execution_timestamp / 1000000000.0)); RETURN NEW; END; $$ LANGUAGE plpgsql IMMUTABLE; CREATE TRIGGER tg_set_stress_test_execution_date BEFORE INSERT OR UPDATE ON stress_test_results FOR EACH ROW EXECUTE FUNCTION set_stress_test_execution_date(); -- ================================================================================================ -- AUTOMATIC PARTITIONING -- ================================================================================================ -- Function to create daily partitions for risk events CREATE OR REPLACE FUNCTION create_risk_events_partition(target_date DATE) RETURNS VOID AS $$ DECLARE partition_name TEXT; start_date DATE; end_date DATE; BEGIN start_date := target_date; end_date := target_date + INTERVAL '1 day'; partition_name := 'risk_events_' || to_char(start_date, 'YYYY_MM_DD'); IF NOT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_name = partition_name ) THEN EXECUTE format('CREATE TABLE %I PARTITION OF risk_events FOR VALUES FROM (%L) TO (%L)', partition_name, start_date, end_date); -- Add partition-specific indexes EXECUTE format('CREATE INDEX %I ON %I USING BTREE (event_timestamp)', 'idx_' || partition_name || '_timestamp', partition_name); EXECUTE format('CREATE INDEX %I ON %I USING BTREE (severity, event_timestamp)', 'idx_' || partition_name || '_severity_ts', partition_name); END IF; END; $$ LANGUAGE plpgsql; -- Function to create monthly partitions for risk metrics CREATE OR REPLACE FUNCTION create_risk_metrics_partition(target_date DATE) RETURNS VOID AS $$ DECLARE partition_name TEXT; start_date DATE; end_date DATE; BEGIN start_date := date_trunc('month', target_date); end_date := start_date + INTERVAL '1 month'; partition_name := 'risk_metrics_' || to_char(start_date, 'YYYY_MM'); IF NOT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_name = partition_name ) THEN EXECUTE format('CREATE TABLE %I PARTITION OF risk_metrics FOR VALUES FROM (%L) TO (%L)', partition_name, start_date, end_date); -- Add partition-specific indexes EXECUTE format('CREATE INDEX %I ON %I USING BTREE (calculation_timestamp)', 'idx_' || partition_name || '_timestamp', partition_name); EXECUTE format('CREATE INDEX %I ON %I USING BTREE (metric_name, calculation_timestamp)', 'idx_' || partition_name || '_name_ts', partition_name); END IF; END; $$ LANGUAGE plpgsql; -- Create initial partitions DO $$ DECLARE i INTEGER; BEGIN -- Create risk_events partitions for current and next 7 days FOR i IN 0..7 LOOP PERFORM create_risk_events_partition(CURRENT_DATE + i); END LOOP; -- Create risk_metrics partitions for current and next 2 months FOR i IN 0..2 LOOP PERFORM create_risk_metrics_partition((CURRENT_DATE + (i || ' months')::INTERVAL)::DATE); END LOOP; -- Create stress_test_results partitions (daily, same as risk_events) FOR i IN 0..7 LOOP -- Create stress_test_results partitions using similar logic DECLARE partition_name TEXT; start_date DATE; end_date DATE; BEGIN start_date := CURRENT_DATE + i; end_date := start_date + INTERVAL '1 day'; partition_name := 'stress_test_results_' || to_char(start_date, 'YYYY_MM_DD'); IF NOT EXISTS ( SELECT 1 FROM information_schema.tables WHERE table_name = partition_name ) THEN EXECUTE format('CREATE TABLE %I PARTITION OF stress_test_results FOR VALUES FROM (%L) TO (%L)', partition_name, start_date, end_date); END IF; END; END LOOP; END $$; -- ================================================================================================ -- TRIGGER FUNCTIONS FOR AUTOMATION -- ================================================================================================ -- Function to automatically update risk limits timestamp CREATE OR REPLACE FUNCTION update_risk_limits_timestamp() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at := NOW(); NEW.version := OLD.version + 1; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Function to validate risk limit hierarchy CREATE OR REPLACE FUNCTION validate_risk_limit_hierarchy() RETURNS TRIGGER AS $$ DECLARE parent_limit DECIMAL(20, 8); BEGIN -- Check that child limits don't exceed parent limits IF NEW.scope_level = 'account' THEN SELECT breach_threshold INTO parent_limit FROM risk_limits WHERE limit_type = NEW.limit_type AND scope_level = 'global' AND is_active = TRUE; IF parent_limit IS NOT NULL AND NEW.breach_threshold > parent_limit THEN RAISE EXCEPTION 'Account limit cannot exceed global limit for %', NEW.limit_type; END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Function to generate risk events from metric breaches CREATE OR REPLACE FUNCTION check_risk_metric_breach() RETURNS TRIGGER AS $$ DECLARE applicable_limit RECORD; breach_detected BOOLEAN := FALSE; severity_level risk_severity; event_type_val risk_event_type; BEGIN -- Find applicable risk limit (most specific first) SELECT * INTO applicable_limit FROM risk_limits rl WHERE rl.limit_type = NEW.metric_name AND rl.is_active = TRUE AND NOW() BETWEEN rl.effective_from AND COALESCE(rl.effective_to, 'infinity'::TIMESTAMP WITH TIME ZONE) AND ( (rl.scope_level = 'symbol' AND rl.symbol = NEW.symbol) OR (rl.scope_level = 'strategy' AND rl.strategy_id = NEW.strategy_id) OR (rl.scope_level = 'account' AND rl.account_id = NEW.account_id) OR (rl.scope_level = 'global') ) ORDER BY CASE rl.scope_level WHEN 'symbol' THEN 1 WHEN 'strategy' THEN 2 WHEN 'account' THEN 3 WHEN 'global' THEN 4 END LIMIT 1; -- Check for breaches IF applicable_limit.id IS NOT NULL THEN IF NEW.value > COALESCE(applicable_limit.emergency_threshold, applicable_limit.breach_threshold) THEN breach_detected := TRUE; severity_level := 'emergency'; event_type_val := CASE WHEN NEW.metric_name IN ('var_1d', 'var_10d') THEN 'var_breach' WHEN NEW.metric_name IN ('exposure_gross', 'exposure_net') THEN 'exposure_limit_breach' WHEN NEW.metric_name = 'leverage_ratio' THEN 'leverage_excess' WHEN NEW.metric_name IN ('concentration_single', 'concentration_sector') THEN 'concentration_risk' ELSE 'stress_test_failure' END; ELSIF NEW.value > applicable_limit.breach_threshold THEN breach_detected := TRUE; severity_level := 'critical'; event_type_val := CASE WHEN NEW.metric_name IN ('var_1d', 'var_10d') THEN 'var_breach' WHEN NEW.metric_name IN ('exposure_gross', 'exposure_net') THEN 'exposure_limit_breach' WHEN NEW.metric_name = 'leverage_ratio' THEN 'leverage_excess' WHEN NEW.metric_name IN ('concentration_single', 'concentration_sector') THEN 'concentration_risk' ELSE 'stress_test_failure' END; ELSIF NEW.value > COALESCE(applicable_limit.warning_threshold, applicable_limit.breach_threshold * 0.8) THEN breach_detected := TRUE; severity_level := 'medium'; event_type_val := CASE WHEN NEW.metric_name IN ('var_1d', 'var_10d') THEN 'var_breach' WHEN NEW.metric_name IN ('exposure_gross', 'exposure_net') THEN 'exposure_limit_breach' WHEN NEW.metric_name = 'leverage_ratio' THEN 'leverage_excess' WHEN NEW.metric_name IN ('concentration_single', 'concentration_sector') THEN 'concentration_risk' ELSE 'stress_test_failure' END; END IF; -- Generate risk event if breach detected IF breach_detected THEN INSERT INTO risk_events ( correlation_id, event_timestamp, detected_timestamp, event_type, severity, risk_metric, symbol, account_id, strategy_id, threshold_value, actual_value, breach_percentage, description, risk_model, model_version, recommended_action, source_system, node_id, process_id, event_data ) VALUES ( NEW.id, NEW.calculation_timestamp, EXTRACT(EPOCH FROM NOW()) * 1000000000, event_type_val, severity_level, NEW.metric_name, NEW.symbol, NEW.account_id, NEW.strategy_id, applicable_limit.breach_threshold, NEW.value, ((NEW.value - applicable_limit.breach_threshold) / applicable_limit.breach_threshold * 100), format('Risk metric %s breached: %s > %s', NEW.metric_name, NEW.value, applicable_limit.breach_threshold), NEW.model_name, NEW.model_version, applicable_limit.breach_action, 'risk_engine', 'risk-node-01', pg_backend_pid(), jsonb_build_object( 'metric_id', NEW.id, 'limit_id', applicable_limit.id, 'calculation_details', NEW.calculation_details, 'confidence_level', NEW.confidence_level ) ); END IF; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- ================================================================================================ -- CREATE TRIGGERS -- ================================================================================================ -- Risk limits triggers CREATE TRIGGER tg_update_risk_limits_timestamp BEFORE UPDATE ON risk_limits FOR EACH ROW EXECUTE FUNCTION update_risk_limits_timestamp(); CREATE TRIGGER tg_validate_risk_limit_hierarchy BEFORE INSERT OR UPDATE ON risk_limits FOR EACH ROW EXECUTE FUNCTION validate_risk_limit_hierarchy(); -- Risk metrics breach detection CREATE TRIGGER tg_check_risk_metric_breach AFTER INSERT OR UPDATE ON risk_metrics FOR EACH ROW WHEN (NEW.is_valid = TRUE) EXECUTE FUNCTION check_risk_metric_breach(); -- Update stress test scenario timestamp CREATE TRIGGER tg_update_stress_scenarios_timestamp BEFORE UPDATE ON stress_test_scenarios FOR EACH ROW EXECUTE FUNCTION update_risk_limits_timestamp(); -- Reuse same function -- ================================================================================================ -- RISK MANAGEMENT FUNCTIONS -- ================================================================================================ -- Function to calculate portfolio VaR CREATE OR REPLACE FUNCTION calculate_portfolio_var( p_account_id VARCHAR(64) DEFAULT NULL, p_strategy_id VARCHAR(100) DEFAULT NULL, p_confidence_level DECIMAL(5,4) DEFAULT 0.95, p_time_horizon_days INTEGER DEFAULT 1 ) RETURNS DECIMAL(20,8) AS $$ DECLARE portfolio_var DECIMAL(20,8) := 0; position_count INTEGER; BEGIN -- Simple VaR calculation based on current positions -- In production, this would use more sophisticated models SELECT COUNT(*) INTO position_count FROM positions p WHERE (p_account_id IS NULL OR p.account_id = p_account_id) AND (p_strategy_id IS NULL OR p.strategy_id = p_strategy_id) AND p.quantity != 0; IF position_count = 0 THEN RETURN 0; END IF; -- Placeholder calculation - implement actual VaR model SELECT COALESCE(SUM(ABS(p.market_value) * 0.02), 0) -- 2% daily volatility assumption INTO portfolio_var FROM positions p WHERE (p_account_id IS NULL OR p.account_id = p_account_id) AND (p_strategy_id IS NULL OR p.strategy_id = p_strategy_id) AND p.quantity != 0; -- Adjust for confidence level and time horizon portfolio_var := portfolio_var * SQRT(p_time_horizon_days) * (CASE WHEN p_confidence_level >= 0.99 THEN 2.33 WHEN p_confidence_level >= 0.95 THEN 1.65 ELSE 1.28 END); RETURN portfolio_var; END; $$ LANGUAGE plpgsql; -- Function to refresh risk dashboard CREATE OR REPLACE FUNCTION refresh_risk_dashboard() RETURNS VOID AS $$ BEGIN REFRESH MATERIALIZED VIEW CONCURRENTLY mv_risk_dashboard; END; $$ LANGUAGE plpgsql; -- Function to get active risk alerts CREATE OR REPLACE FUNCTION get_active_risk_alerts( p_severity risk_severity[] DEFAULT ARRAY['high'::risk_severity, 'critical'::risk_severity, 'emergency'::risk_severity] ) RETURNS TABLE ( event_id UUID, event_type risk_event_type, severity risk_severity, symbol VARCHAR(32), account_id VARCHAR(64), description TEXT, event_timestamp ns_timestamp, age_minutes INTEGER ) AS $$ BEGIN RETURN QUERY SELECT re.id, re.event_type, re.severity, re.symbol, re.account_id, re.description, re.event_timestamp, EXTRACT(EPOCH FROM (NOW() - TO_TIMESTAMP(re.event_timestamp / 1000000000.0))) / 60 AS age_minutes FROM risk_events re WHERE re.resolved_timestamp IS NULL AND re.severity = ANY(p_severity) AND re.event_timestamp >= EXTRACT(EPOCH FROM (NOW() - INTERVAL '24 hours')) * 1000000000 ORDER BY re.severity DESC, re.event_timestamp DESC; END; $$ LANGUAGE plpgsql; -- ================================================================================================ -- REPORTING VIEWS -- ================================================================================================ -- Active risk alerts view CREATE VIEW v_active_risk_alerts AS SELECT re.id, re.event_type, re.severity, re.symbol, re.account_id, re.strategy_id, re.description, re.actual_value, re.threshold_value, re.breach_percentage, TO_TIMESTAMP(re.event_timestamp / 1000000000.0) as event_time, TO_TIMESTAMP(re.detected_timestamp / 1000000000.0) as detected_time, EXTRACT(EPOCH FROM (NOW() - TO_TIMESTAMP(re.event_timestamp / 1000000000.0))) / 60 as age_minutes, re.recommended_action, re.acknowledged_by IS NOT NULL as is_acknowledged FROM risk_events re WHERE re.resolved_timestamp IS NULL AND re.severity IN ('medium', 'high', 'critical', 'emergency') AND re.event_timestamp >= EXTRACT(EPOCH FROM (NOW() - INTERVAL '7 days')) * 1000000000 ORDER BY CASE re.severity WHEN 'emergency' THEN 1 WHEN 'critical' THEN 2 WHEN 'high' THEN 3 WHEN 'medium' THEN 4 ELSE 5 END, re.event_timestamp DESC; -- Risk metrics summary view CREATE VIEW v_risk_metrics_summary AS SELECT rm.metric_name, rm.account_id, rm.strategy_id, rm.symbol, rm.value as current_value, rl.warning_threshold, rl.breach_threshold, rl.emergency_threshold, CASE WHEN rm.value > COALESCE(rl.emergency_threshold, rl.breach_threshold) THEN 'EMERGENCY' WHEN rm.value > rl.breach_threshold THEN 'CRITICAL' WHEN rm.value > COALESCE(rl.warning_threshold, rl.breach_threshold * 0.8) THEN 'WARNING' ELSE 'NORMAL' END as status, TO_TIMESTAMP(rm.calculation_timestamp / 1000000000.0) as calculated_at, rm.model_name, rm.is_valid FROM risk_metrics rm LEFT JOIN risk_limits rl ON ( rl.limit_type = rm.metric_name AND rl.is_active = TRUE AND NOW() BETWEEN rl.effective_from AND COALESCE(rl.effective_to, 'infinity'::TIMESTAMP WITH TIME ZONE) AND ( (rl.scope_level = 'symbol' AND rl.symbol = rm.symbol) OR (rl.scope_level = 'strategy' AND rl.strategy_id = rm.strategy_id) OR (rl.scope_level = 'account' AND rl.account_id = rm.account_id) OR (rl.scope_level = 'global' AND rl.account_id IS NULL AND rl.strategy_id IS NULL AND rl.symbol IS NULL) ) ) WHERE rm.calculation_timestamp >= EXTRACT(EPOCH FROM (NOW() - INTERVAL '4 hours')) * 1000000000 AND rm.is_valid = TRUE -- Get most recent calculation for each metric/scope combination AND rm.calculation_timestamp = ( SELECT MAX(rm2.calculation_timestamp) FROM risk_metrics rm2 WHERE rm2.metric_name = rm.metric_name AND COALESCE(rm2.account_id, '') = COALESCE(rm.account_id, '') AND COALESCE(rm2.strategy_id, '') = COALESCE(rm.strategy_id, '') AND COALESCE(rm2.symbol, '') = COALESCE(rm.symbol, '') AND rm2.is_valid = TRUE ); -- ================================================================================================ -- COMMENTS AND DOCUMENTATION -- ================================================================================================ COMMENT ON TABLE risk_events IS 'Immutable event store for all risk management events including breaches, alerts, and stress test results. Critical for compliance and risk monitoring.'; COMMENT ON TABLE risk_metrics IS 'Historical and current risk metric calculations with confidence intervals. Partitioned by date for performance.'; COMMENT ON TABLE risk_limits IS 'Configurable risk limits with hierarchical scope (global > account > strategy > symbol). Supports time-based limits and automatic enforcement.'; COMMENT ON TABLE stress_test_scenarios IS 'Predefined stress test scenarios including historical events, hypothetical shocks, and Monte Carlo simulations.'; COMMENT ON TABLE stress_test_results IS 'Results from stress test executions showing portfolio impact under various scenarios. Critical for regulatory reporting.'; COMMENT ON MATERIALIZED VIEW mv_risk_dashboard IS 'Real-time risk monitoring dashboard with current metrics, thresholds, and alert counts. Refresh every 5 minutes in production.'; COMMENT ON FUNCTION calculate_portfolio_var IS 'Calculate portfolio Value at Risk using specified confidence level and time horizon. Implement with actual risk models in production.'; COMMENT ON FUNCTION get_active_risk_alerts IS 'Get currently active risk alerts filtered by severity. Used by monitoring systems and dashboards.';