# SDK-004 — Pydantic Models & Exception Hierarchy

**Status:** Complete
**Date:** 2026-03-05
**Branch:** `sdk-004/models-exceptions` (off `main`)
**Location:** `hyperscaled-sdk/`

---

## Summary

All data types and errors the SDK uses, defined up front so every feature ticket in Sprints 04–06 can import and use them immediately. Includes 13 Pydantic models across 6 domain modules and 8 exception classes forming a structured hierarchy rooted in `HyperscaledError`.

---

## What Was Built

### New Files

```
hyperscaled-sdk/
├── hyperscaled/
│   └── models/
│       ├── miner.py            # PricingTier, ProfitSplit, EntityMiner
│       ├── trading.py          # Order, Position, ClosedPosition
│       ├── account.py          # LeverageLimits, AccountInfo
│       ├── payout.py           # Payout
│       ├── registration.py     # RegistrationStatus
│       └── rules.py            # Rule, RuleViolation, TradeValidation
└── tests/
    ├── test_models.py          # 32 tests
    └── test_exceptions.py      # 18 tests
```

### Modified Files

| File | Change |
|------|--------|
| `hyperscaled/models/__init__.py` | Re-exports all 13 models (replaced stub) |
| `hyperscaled/exceptions.py` | Full exception hierarchy — 8 new classes (extended from base-only stub) |
| `tests/test_imports.py` | Added 6 new model submodules to import checks |

### Models

| Module | Models | Key Fields |
|--------|--------|------------|
| `models/miner.py` | `PricingTier`, `ProfitSplit`, `EntityMiner` | account_size, cost, trader/miner pct, slug, pricing_tiers, supported_pairs, leverage_limits |
| `models/trading.py` | `Order`, `Position`, `ClosedPosition` | hl_order_id, pair, side (long/short), size, funded_equivalent_size, scaling_ratio, unrealized/realized_pnl |
| `models/account.py` | `LeverageLimits`, `AccountInfo` | status (active/suspended/pending_kyc/breached), drawdown, leverage_limits, kyc_status |
| `models/payout.py` | `Payout` | date, amount, token, network, tx_hash, status (completed/pending/processing/failed) |
| `models/registration.py` | `RegistrationStatus` | status (pending/registered/failed), registration_id, funded_account_id, account_size |
| `models/rules.py` | `Rule`, `RuleViolation`, `TradeValidation` | rule_id, category, violations list, valid flag |

All models:
- Use `Literal` types for constrained string fields (rejects invalid values at construction)
- Default optional fields to `None`
- Serialize to/from JSON via `model_dump_json()` / `model_validate()`
- Use `from __future__ import annotations` for Python 3.10 compatibility

### Exception Hierarchy

```
Exception
└── HyperscaledError(message)
    ├── RuleViolationError(message, rule_id, limit, actual_value)
    │   ├── UnsupportedPairError(+ pair, supported_pairs)
    │   ├── LeverageLimitError(+ requested_leverage, max_leverage)
    │   ├── InsufficientBalanceError(+ balance, minimum_required)
    │   ├── ExposureLimitError(+ current_exposure, max_exposure)
    │   ├── DrawdownBreachError(+ current_drawdown, max_drawdown)
    │   └── OrderFrequencyError(+ requests_per_minute, limit_per_minute)
    └── AccountSuspendedError(message, reason, suspended_at)
```

Every error exposes `.message` and contextual fields. `RuleViolationError` subclasses all share `.rule_id`, `.limit`, and `.actual_value`. `AccountSuspendedError` extends `HyperscaledError` directly (not `RuleViolationError`) since suspension is an account state, not a per-trade rule check.

---

## Acceptance Criteria — All Met

| Criterion | Status |
|-----------|--------|
| All models import cleanly and validate with sample data | Pass |
| Exception hierarchy is tested: `isinstance(UnsupportedPairError(...), RuleViolationError)` is True | Pass |
| Every error type exposes `.message`, `.rule_id` (where applicable), and contextual fields | Pass |
| Models serialize to/from JSON (for CLI `--json` output) | Pass |
| At least one unit test per model and per exception type | Pass |

### Verification Results

```
ruff check .          → All checks passed!
ruff format --check . → 38 files already formatted
mypy hyperscaled      → Success: no issues found in 32 source files
pytest -v             → 105 passed in 0.97s
```

### Test Coverage

| Test File | Test Group | Count | What's Covered |
|-----------|------------|-------|----------------|
| `test_models.py` | PricingTier | 2 | Construction, JSON roundtrip |
| | ProfitSplit | 2 | Construction, JSON roundtrip |
| | EntityMiner | 3 | Construction with nested models, optional URL defaults to None, JSON roundtrip |
| | Order | 3 | Valid market order, invalid side rejected by Literal, JSON roundtrip |
| | Position | 2 | Construction with optional fields, JSON roundtrip |
| | ClosedPosition | 2 | Extends Position (`isinstance` check), JSON roundtrip |
| | LeverageLimits | 2 | Construction, JSON roundtrip |
| | AccountInfo | 3 | Construction, invalid status rejected by Literal, JSON roundtrip |
| | Payout | 3 | Construction, optional tx_hash, JSON roundtrip |
| | RegistrationStatus | 3 | Pending (no funded_account_id), registered (with funded_account_id), JSON roundtrip |
| | Rule | 3 | Construction, invalid category rejected, JSON roundtrip |
| | RuleViolation | 1 | Construction with nested Rule |
| | TradeValidation | 3 | Valid trade (empty violations), invalid trade (with violations), JSON roundtrip |
| `test_exceptions.py` | HyperscaledError | 2 | Message attribute, is subclass of Exception |
| | RuleViolationError | 2 | Inherits HyperscaledError, exposes rule_id/limit/actual_value |
| | UnsupportedPairError | 2 | Inherits RuleViolationError, exposes pair/supported_pairs |
| | LeverageLimitError | 2 | Inherits RuleViolationError, exposes requested/max leverage |
| | InsufficientBalanceError | 2 | Inherits RuleViolationError, exposes balance/minimum_required |
| | ExposureLimitError | 2 | Inherits RuleViolationError, exposes current/max exposure |
| | DrawdownBreachError | 2 | Inherits RuleViolationError, exposes current/max drawdown |
| | OrderFrequencyError | 2 | Inherits RuleViolationError, exposes requests/limit per minute |
| | AccountSuspendedError | 2 | Inherits HyperscaledError (not RuleViolationError), exposes reason/suspended_at |

---

## What's Next

This unblocks **SDK-005** (entity miner discovery) — the last ticket in Sprint 04 and the first user-facing feature. All four prerequisites are now complete.

```
SDK-001 ✅ ──┬── SDK-002 ✅ ── SDK-003 ✅ ──┐
             │                              ├── SDK-005
             └── SDK-004 ✅ ────────────────┘
```
