# pyxva v1.0 — Design & Roadmap
Generated by /plan-ceo-review on 2026-03-18
Mode: SELECTIVE EXPANSION

## Vision

The 10x version of this library is one a quant developer at a regional bank deploys on
Monday morning: `pip install pyxva`, drops a YAML config describing their portfolio,
and gets ISDA-compliant EE/PFE/CVA numbers against a live yield curve by afternoon.
Zero-copy parallel execution at 100k+ paths, multi-factor rate dynamics for realistic
swaption calibration, SA-CCR regulatory capital from the same run, and a CHANGELOG that
tells consumers exactly what changed on upgrade.

---

## Accepted Scope (v1.0 implementation targets)

### 1. SharedMemory in `_run_parallel()` _(P1 — memory ceiling fix)_

**Problem:** `_run_parallel()` pickles full `simulation_results` per worker. At realistic
scale (1M paths × 200 nodes × 5 models ≈ 4GB), this blows up RAM proportional to worker count.

**Solution:** Wire `SimulationSharedMemory` (already built) into `_run_parallel()`.
Workers receive a lightweight descriptor dict (~100 bytes); arrays are zero-copy numpy views.

**Key decisions:**
- Use `try/finally` context manager wrapping `_run_parallel()` for cleanup on exceptions
- Name blocks with `f"ra_{os.getpid()}_{uuid4().hex[:8]}"` to prevent collisions

```
Before: worker = pickle(simulation_results)   # O(workers × data)
After:  worker = descriptor_dict              # O(1), zero-copy

SimulationSharedMemory.from_results(results)  # allocate once
  └── descriptor = shm.descriptor()           # 100-byte dict
        ├── worker_1.attach(descriptor)       # zero-copy view
        └── worker_N.attach(descriptor)       # zero-copy view
finally: shm.cleanup()                        # always runs
```

---

### 2. Example YAML configs in `examples/`

**Problem:** Zero runnable examples. New users must read the test suite to understand config format.

**Deliverables:**
- `examples/single_swap.yaml` — HW1F rates model, 5y payer swap, 10k paths
- `examples/multi_asset.yaml` — HW1F + GBM, IRS + equity option in one netting set, correlated
- `examples/stress_test.yaml` — same multi-asset, 100k paths, 3 agreements (exercises parallel path)
- All three runnable via `uv run pyxva-demo` (wire into existing `demo.py` or new runner)

---

### 3. HullWhite2F model

**Problem:** HW1F produces a single mean-reverting short rate. Real swaption markets show richer
term structure of volatility that HW1F systematically mis-prices. HW2F adds a second factor.

**Design:** Standalone class (not inheriting HW1F — the 2F discount factor math diverges).

**Exact 2-factor Gaussian discretisation:**
```
Short rate:  x(t) = r(t) + u(t)
Factor r:    dr = (θ(t) - ar)dt + σ dW₁
Factor u:    du = -bu dt + η dW₂
Correlation: E[dW₁ dW₂] = ρ dt

Discrete step (exact):
  r(t+Δ) = r(t)·exp(-aΔ) + (θ/a)·(1-exp(-aΔ)) + σ·ε₁·√((1-exp(-2aΔ))/(2a))
  u(t+Δ) = u(t)·exp(-bΔ) + η·ε₂·√((1-exp(-2bΔ))/(2b))
```

**2F affine discount factor:** P(t,T|r,u) = A₂(t,T)·exp(-B₁(t,T)·r - B₂(t,T)·u)

**Interface:** `n_factors=2`, `interpolation_space=["linear","linear"]`,
`calibrate({"tenors":..., "zero_rates":..., "time_grid":...})` — same as HW1F.

**Pipeline registration:** add `"HullWhite2F"` to `MODEL_REGISTRY` in `pipeline/engine.py`.

---

### 4. AsianOption (StatefulPricer)

**Problem:** No arithmetic average option pricer. Also: only one StatefulPricer example (BarrierOption)
makes the pattern harder to learn from.

**Design:** PathState holds running sum and step count. Initial state: `count=1`, `avg_sum=S(t=0)`.

```python
@dataclass
class AsianState(PathState):
    avg_sum: np.ndarray   # shape (n_paths,) — running sum of S values
    count: int            # steps elapsed (starts at 1)

def step(t_idx, result, state):
    S_t = result.at(t)[..., 0]          # current spot
    new_avg = state.avg_sum + S_t
    new_count = state.count + 1
    if t_idx == expiry_idx:
        mtm = np.maximum(new_avg / new_count - K, 0)
    else:
        mtm = np.zeros(n_paths)         # interim: 0 (pre-expiry placeholder)
    return mtm, AsianState(new_avg, new_count)
```

**Edge case:** count=1 at t=0 (treating initial spot as first observation) — avoids divide-by-zero.

---

### 5. SA-CCR Calculator

**Problem:** Library computes CVA/DVA/EE but not SA-CCR regulatory capital (Basel III requirement).
Banks need both outputs from the same simulation run.

**Placement:** `exposure/saccr.py` — standalone calculator alongside `metrics.py` and `bilateral.py`.

**Architecture:**
```
SACCRCalculator.compute(netting_set, current_mtm_vector)
  ├── RC = max(V_agg(t=0), 0)               # replacement cost at current date
  ├── Per asset class, per hedging set:
  │     IR:     adjusted_notional × maturity_factor × supervisory_delta
  │     Equity: notional × supervisory_factor
  │     FX:     notional × supervisory_factor
  ├── Aggregate via square-root formula across hedging sets
  └── EAD = 1.4 × (RC + PFE_add_on)
→ SACCRResult(rc, pfe_addon, ead, breakdown_by_class)
```

**Error handling:** raises `ValueError` for unknown asset class or missing required trade params.
Logging: per-netting-set EAD at DEBUG level.

---

### 6. CHANGELOG + PyPI 1.0.0

**Steps:**
1. Write `CHANGELOG.md` with sections for all features shipped (Added, Fixed, Changed)
2. Run public API surface audit — review all exported class/method names before committing to stability
3. Update `pyproject.toml`: `version = "1.0.0"`, add `[project]` metadata (description, authors, license, classifiers)
4. `git tag v1.0.0`
5. `uv build && uv publish`

**API stability commitment:** semver. Breaking changes require 2.0.0.

---

### 7. TradeFactory auto-register built-in exotics _(P3 — pulled in)_

**Problem:** `BarrierOption` and `AsianOption` require manual `@TradeFactory.register()` before
pipeline use. Built-in instruments should work without ceremony.

**Fix:** In `pipeline/config.py`, add `_auto_register_builtins()` that imports and registers
`BarrierOption` and `AsianOption` once on module load. Users only call `register()` for custom
instruments.

---

### 8. BarrierOption pre-expiry analytical MTM _(P2 — pulled in)_

**Problem:** `BarrierOption.step()` returns 0 for `t < expiry`. EE profile is flat zero until
maturity — not useful for pre-expiry exposure analysis.

**Fix:** Implement closed-form Black-Scholes barrier option price for `t < expiry`:
```
For down-and-out call:
  MTM(t) = BS_call(S, K, σ, r, T-t) - (S/H)^(2λ) · BS_call(H²/S, K, σ, r, T-t)
  where λ = (r/σ² + 0.5), H = barrier level
```
Returns smooth, positive pre-expiry MTM profile.

---

### 9. Streaming vs batch EE benchmark test _(P3 — pulled in)_

**Problem:** No test verifies that `StreamingExposureEngine` and `ISDAExposureCalculator` produce
the same EE profiles for vanilla swaps with zero threshold/MTA. Future divergence would be silent.

**Fix:** Add `test_streaming_batch_ee_parity()` that runs both engines on the same HW1F simulation
and asserts EE profiles agree within 1bp tolerance.

---

## Not in scope (deferred)

| Item | Rationale |
|------|-----------|
| HullWhite3F | Ocean, not lake — limited incremental value over HW2F for most use cases |
| Multi-currency CSA | Complex FX netting rules; separate PR |
| Credit models (CDS, JtD) | Wrong-way risk and credit curve infrastructure needed first |
| SABR/LocalVol | Local vol surface infrastructure is a multi-PR effort |
| Lazy RunResult loading | Useful but blocked on downstream use case clarity |
| Multi-model netting in streaming | Known limitation; separate PR |

---

## Error & Architecture Decisions

| Decision | Choice |
|----------|--------|
| SharedMemory crash safety | `try/finally` context manager in `_run_parallel()` |
| SharedMemory naming | `f"ra_{os.getpid()}_{uuid4().hex[:8]}"` — no collisions |
| HW2F inheritance | Standalone class (not extending HW1F) |
| SA-CCR module placement | `exposure/saccr.py` — standalone calculator |
| PyPI version | 1.0.0 — commit to API stability |
| API surface audit | Run before tagging 1.0.0 |
| AsianOption t=0 | Initialize `count=1` in PathState |
