# 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" |
