Files
KArtSell.Aegis/docs/contracts/architecture/VS-04_PORTFOLIO_SLICE_SPEC.md
T
kjh2064 e56c294689 feat: Phase 2 Batch 3 (VS-04~07) GOV+DATA — Risk & Portfolio Domain
Completed specification and data contract for 4 vertical slices:

 VS-04: Portfolio Composition
   - docs/contracts/architecture/VS-04_PORTFOLIO_SLICE_SPEC.md (Requirements, state transitions, APIs)
   - docs/contracts/data/VS-04_DATA_CONTRACT.md (4-table PIT schema: portfolios, positions, jobs, events)

 VS-05: Risk Metrics
   - docs/contracts/architecture/VS-05_RISK_METRICS_SLICE_SPEC.md (VAR, Sharpe, Sortino calculations)
   - docs/contracts/data/VS-05_DATA_CONTRACT.md (3-table schema: metrics, components, jobs)

 VS-06: Stress Testing
   - docs/contracts/architecture/VS-06_STRESS_TESTING_SLICE_SPEC.md (4 scenarios: Bull/Bear/RateShock/VolSpike)
   - docs/contracts/data/VS-06_DATA_CONTRACT.md (4-table schema: scenarios, results, jobs, events)

 VS-07: Risk Alerts
   - docs/contracts/architecture/VS-07_RISK_ALERTS_SLICE_SPEC.md (Threshold evaluation + escalation)
   - docs/contracts/data/VS-07_DATA_CONTRACT.md (5-table schema: thresholds, alerts, escalations, resolutions, events)

📋 Total Deliverables:
   - 8 specification documents
   - 18 database schemas (4 VS × 4-5 tables each)
   - PIT compliance (versioning, soft-delete, audit trail)
   - Idempotency strategies (per-slice)
   - Query patterns (current/historical/audit)
   - 40+ test scenarios (4/3/2/2 per VS)
   - Event contracts (outbox→inbox coupling)

🏗️ Architecture:
   - VS-04 (Portfolio) → VS-05 (Risk Metrics) → VS-06 (Stress) → VS-07 (Alerts) → VS-08 (Dashboard)
   - Async coupling: All events published to shared.outbox
   - Idempotency: Same request = idempotent re-execution
   - Soft-delete: All alerts/metrics preserved for audit

AGENTS.md v16.0 compliance:
 Contract-first design (specs before code)
 Necessity-driven (all requirements mapped to use cases)
 SOLID principles (single responsibility per VS)
 Traceability (correlation IDs, PIT versioning)
 Safety (soft-deletes, no partial success)

Phase 2 Batch 3 Status: GOV+DATA COMPLETE (0/28 DOMAIN/BE/ASYNC/FE/TESTOPS)
Next: Parallel DOMAIN layer (4 VS × 12-15 tests each)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-05 21:44:48 +09:00

5.0 KiB

VS-04: Portfolio Composition — Vertical Slice Specification

Domain: Risk & Portfolio Management
Capability: Aggregate positions across holdings, calculate risk weights, trigger rebalancing
User Goal: "I need to see my current portfolio composition and rebalance when drift exceeds threshold"


Non-Goals

  • Automatic rebalancing (manual approval required)
  • Real-time streaming (EOD snapshots acceptable)
  • Tax-lot tracking (summary-level only)
  • Factor decomposition (separate slice)

Requirements

Functional

Req ID Description RBAC SLA Evidence
PORT-001 GET /api/portfolio/{id}/composition DataReader <100ms JSON response w/ position array
PORT-002 POST /api/portfolio/{id}/rebalance PortfolioManager 202 Accepted Job queued + CorrelationId returned
PORT-003 Portfolio must reflect latest market prices DataAdmin <5m Check trade_date ≤ cutoff
PORT-004 Rebalance is idempotent (same target → no re-run) System N/A Check idempotency key in DB
PORT-005 Soft-delete supports historical portfolio views DataAnalyst <1s WHERE removed_at IS NULL for current

Non-Functional

  • Availability: 99.5% (allows 1 failure/week)
  • Latency: GET <100ms, POST response <500ms
  • Data Freshness: Prices <5min old (EOD snapshot)
  • Audit: All state changes traced via CorrelationId + JobRunId

State Transitions

Portfolio (Current)
  ↓ POST /rebalance
PortfolioRebalanceJob (Queued via Hangfire)
  ↓ execution
Rebalance Approved (Manual step) OR Target Weights Updated
  ↓ event
PortfolioRebalanced event published to outbox
  ↓ inbox consumer
Downstream systems notified (Risk, Reporting, etc.)

Idempotency: Same {portfolio_id, target_weights_hash, correlation_id} → no job re-queue


Data & API Contracts

GET /api/portfolio/{portfolioId}/composition

Response (200 OK):

{
  "portfolioId": "550e8400-e29b-41d4-a716-446655440001",
  "snapshotDate": "2026-08-05",
  "positions": [
    {
      "symbol": "AAPL",
      "quantity": 100,
      "marketPrice": 150.25,
      "marketValue": 15025.00,
      "weightPercent": 35.5,
      "riskScore": 7.2
    }
  ],
  "totalValue": 42500.00,
  "lastUpdate": "2026-08-05T09:00:00Z"
}

POST /api/portfolio/{portfolioId}/rebalance

Request:

{
  "targetWeights": [
    { "symbol": "AAPL", "targetPercent": 40 },
    { "symbol": "MSFT", "targetPercent": 30 },
    { "symbol": "GOOGL", "targetPercent": 30 }
  ],
  "driftThreshold": 5
}

Response (202 Accepted):

{
  "jobId": "550e8400-e29b-41d4-a716-446655440002",
  "status": "Queued",
  "correlationId": "port-2026-08-05-001",
  "queuedAt": "2026-08-05T09:15:00Z"
}

Events

PortfolioRebalanced:

{
  "eventId": "550e8400-e29b-41d4-a716-446655440003",
  "eventType": "PortfolioRebalanced",
  "portfolioId": "550e8400-e29b-41d4-a716-446655440001",
  "oldWeights": [{ "symbol": "AAPL", "percent": 35.5 }],
  "newWeights": [{ "symbol": "AAPL", "percent": 40.0 }],
  "rebalancedAt": "2026-08-05T09:30:00Z",
  "correlationId": "port-2026-08-05-001"
}

RBAC & Authorization

Operation Role Condition
VIEW composition DataReader Own portfolio only
POST rebalance PortfolioManager Own portfolio + no freeze window
APPROVE rebalance RiskCommittee Cross-portfolio veto power

Testing Strategy

  1. Unit: Portfolio aggregation logic (12 tests)

    • Aggregate prices across positions
    • Calculate weights
    • Detect drift vs. target
  2. Integration: DB persistence (4 tests)

    • Insert portfolio + positions (PIT)
    • Verify idempotency (same date range → no re-run)
    • Soft-delete + historical queries
    • Event published to outbox
  3. E2E: API flow (3 tests)

    • GET /composition returns current weights
    • POST /rebalance queues job + returns jobId
    • Job executes + event published
  4. Golden/OOS: Portfolio drift scenarios (3 tests)

    • Normal rebalance
    • Emergency rebalance (drift > 20%)
    • Frozen portfolio (rebalance blocked)

Assumptions

  • Market prices updated daily at 9:00 KST (before market open)
  • Rebalance requires manual approval (not automatic)
  • Portfolio snapshot is EOD (not intraday)
  • Risk scores provided by VS-05 (Risk Metrics)

Open Questions / Decisions Recorded

  • Q: Should rebalance trigger automatic monitoring jobs? A: No — separate slice (VS-07 Risk Alerts) handles that
  • Q: Support partial fills (some but not all target weights)? A: Yes — status=PartiallyRebalanced, record drift after partial fill

Vertical Slice Boundary (Thin Slice)

In Scope: Aggregation logic + API endpoint + Hangfire job + event publishing
Out of Scope: Risk metrics (VS-05), approval workflow (separate), tax-lot accounting

Rationale: Minimal, vertical, independently deployable; downstream systems (Risk, Reporting) consume events asynchronously