# VS-06: Stress Testing — Data Contract **Version:** 1.0 **Compliance:** Append-Only (immutable test results) **Migration:** `0035_stress_testing.sql` (DbUp) --- ## Schema Design ### 1. `stress_scenarios` (Configuration — Immutable) Pre-defined scenario templates. New scenarios versioned; active scenarios = latest revision. ```sql CREATE TABLE risk_management.stress_scenarios ( scenario_id VARCHAR(50) PRIMARY KEY, -- Metadata scenario_name VARCHAR(255) NOT NULL, description TEXT, scenario_type VARCHAR(50), -- 'Predefined', 'Custom' -- Shock parameters (JSON-encoded for flexibility) shocks JSONB NOT NULL, -- { "equityShock": -0.20, "bondYieldShock": 0.015, ... } -- Version control (for scenario evolution) version INT NOT NULL DEFAULT 1, effective_date DATE, deprecated_date DATE NULL, -- Audit created_by VARCHAR(100), created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE(scenario_id, version), CHECK (deprecated_date IS NULL OR deprecated_date >= effective_date) ); ``` ### 2. `stress_test_results` (Append-Only — Immutable Results) Immutable record of each stress test execution. ```sql CREATE TABLE risk_management.stress_test_results ( stress_test_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), portfolio_id UUID NOT NULL REFERENCES risk_management.portfolios(portfolio_id), -- Scenario scenario_id VARCHAR(50) NOT NULL REFERENCES risk_management.stress_scenarios(scenario_id), scenario_version INT NOT NULL, run_date DATE NOT NULL, -- Baseline (from portfolio snapshot) baseline_portfolio_value DECIMAL(20, 2), baseline_var_95 DECIMAL(20, 2), baseline_sharpe DECIMAL(5, 3), -- Stressed (after shock application) stressed_portfolio_value DECIMAL(20, 2), stressed_var_95 DECIMAL(20, 2), stressed_sharpe DECIMAL(5, 3), -- Impact metrics portfolio_loss_amount DECIMAL(20, 2), portfolio_loss_percent DECIMAL(5, 2), var_increase_amount DECIMAL(20, 2), var_increase_percent DECIMAL(5, 2), -- Asset class breakdown stress_results_by_class JSONB, -- Array of {assetClass, baselineValue, stressedValue, loss} worst_position JSONB, -- {symbol, loss} -- Status status VARCHAR(50) NOT NULL DEFAULT 'Completed', -- Queued, Running, Completed, Failed started_at TIMESTAMP NULL, completed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, duration_seconds INT NULL, -- Quality quality_flags JSONB, -- Array of strings (e.g., ["missing_price_data"]) -- Audit correlation_id UUID NOT NULL, job_run_id UUID NOT NULL, triggered_by VARCHAR(100), -- 'Manual', 'Scheduler' -- Idempotency UNIQUE(portfolio_id, scenario_id, run_date, correlation_id) ); ``` ### 3. `stress_test_jobs` (Append-Only — Execution Log) Immutable log of job executions. ```sql CREATE TABLE risk_management.stress_test_jobs ( job_id UUID PRIMARY KEY, stress_test_id UUID NOT NULL REFERENCES risk_management.stress_test_results(stress_test_id), -- Execution status VARCHAR(50) NOT NULL DEFAULT 'Queued', started_at TIMESTAMP NULL, completed_at TIMESTAMP NULL, duration_seconds INT NULL, -- Error handling error_message TEXT NULL, retry_count INT DEFAULT 0, -- Audit correlation_id UUID NOT NULL, job_run_id UUID NOT NULL, -- Metadata portfolio_id UUID NOT NULL, scenario_id VARCHAR(50) NOT NULL, run_date DATE NOT NULL, UNIQUE(portfolio_id, scenario_id, run_date, correlation_id) ); ``` ### 4. `stress_test_events` (Append-Only — Published Events) Published to `shared.outbox`. **Schema (JSONB in outbox.payload):** ```json { "eventId": "550e8400-e29b-41d4-a716-446655440007", "eventType": "PortfolioStressTestCompleted", "portfolioId": "550e8400-e29b-41d4-a716-446655440001", "scenarioId": "bear", "stressedVAR95": 42800.00, "portfolioLossPercent": -20.0, "completedAt": "2026-08-05T10:05:00Z", "correlationId": "stress-2026-08-05-001" } ``` --- ## Query Patterns ### Current Stress Test Results ```sql SELECT scenario_id, baseline_portfolio_value, stressed_portfolio_value, portfolio_loss_percent, var_increase_percent, completed_at FROM risk_management.stress_test_results WHERE portfolio_id = @portfolioId AND run_date = CURRENT_DATE ORDER BY portfolio_loss_percent DESC; ``` ### Worst-Case Scenario (Most Loss) ```sql SELECT TOP 1 scenario_id, portfolio_loss_amount, portfolio_loss_percent FROM risk_management.stress_test_results WHERE portfolio_id = @portfolioId AND run_date = @date ORDER BY portfolio_loss_percent ASC; ``` ### Scenario Trend (Historical) ```sql SELECT run_date, scenario_id, portfolio_loss_percent FROM risk_management.stress_test_results WHERE portfolio_id = @portfolioId AND scenario_id = @scenarioId ORDER BY run_date DESC LIMIT 30; ``` ### Idempotency Check ```sql SELECT stress_test_id FROM risk_management.stress_test_results WHERE portfolio_id = @portfolioId AND scenario_id = @scenarioId AND run_date = @date AND correlation_id = @correlationId AND status = 'Completed' LIMIT 1; ``` --- ## Indexes | Table | Columns | Reason | |-------|---------|--------| | stress_scenarios | (scenario_id, version) | Fast scenario lookup | | stress_test_results | (portfolio_id, run_date) | Fast daily result queries | | stress_test_results | (scenario_id) | Fast scenario trend analysis | | stress_test_results | (portfolio_id, scenario_id, run_date, correlation_id) | Fast idempotency check | | stress_test_jobs | (portfolio_id, status) | Fast pending job lookup | --- ## Upsert Strategy **On new stress test request:** ```sql INSERT INTO risk_management.stress_test_results (stress_test_id, portfolio_id, scenario_id, run_date, correlation_id, status) VALUES (@testId, @portfolioId, @scenarioId, @date, @correlationId, 'Queued') ON CONFLICT (portfolio_id, scenario_id, run_date, correlation_id) DO UPDATE SET status = 'Queued' WHERE EXCLUDED.status = 'Completed'; ``` **Idempotency:** Same portfolio_id + scenario_id + run_date + correlation_id → no duplicate test --- ## Pre-loaded Scenarios On fresh install, load 4 predefined scenarios: ```sql INSERT INTO risk_management.stress_scenarios VALUES ('bull', 'Bull Market Scenario', '+15% equities, -50 bps yields', 'Predefined', '{"equityShock": 0.15, "bondYieldShock": -0.005, "volatilityMultiplier": 0.8}', 1, CURRENT_DATE, NULL), ('bear', 'Bear Market Scenario', '-20% equities, +150 bps yields', 'Predefined', '{"equityShock": -0.20, "bondYieldShock": 0.015, "volatilityMultiplier": 1.5}', 1, CURRENT_DATE, NULL), ('rateShock', 'Interest Rate Shock', '+200 bps all yields', 'Predefined', '{"bondYieldShock": 0.02, "volatilityMultiplier": 1.2}', 1, CURRENT_DATE, NULL), ('volSpike', 'Volatility Spike', '5x implied vol', 'Predefined', '{"volatilityMultiplier": 5.0}', 1, CURRENT_DATE, NULL); ``` --- ## Compliance ✅ **AGENTS.md v16.0:** - Append-only results (stress_test_results immutable) - Correlation ID tracing (correlation_id + job_run_id) - Idempotency key (portfolio_id + scenario_id + run_date + correlation_id) - Quality flags recorded (quality_flags JSONB) - Deterministic results (same input → same output) ✅ **Auditability:** - Full execution history preserved (stress_test_jobs) - All shocks recorded (shocks JSONB) - Baseline + stressed values stored - Event published for downstream consumption --- ## Test Scenarios | Test | Data Setup | Assertion | |------|-----------|-----------| | Bear scenario | Portfolio + bear shocks | Portfolio loss ~20% | | Bull scenario | Portfolio + bull shocks | Portfolio gain ~12% | | Asset class impact | Mixed portfolio | Equities impacted more than bonds | | Idempotency | Same test twice | Result retrieved, not recalculated | | Worst position | Mixed holdings | Worst-case position identified correctly | | Quality flags | Missing price data | quality_flags includes "missing_price_data" |