Renumbers the four 2026-08-07 slices (ApprovalWorkflow, AuditTrail, TradeExecution, PortfolioReconciliation) to previously-unused VS-26..29, leaving WBS_MASTER.csv's original VS-03/04/12/14 definitions (IngestMarketDataPIT, ApplyCorporateActions, RankBuyCandidates, GenerateDailyRecommendations) untouched, per docs/DECISIONS/ADR-WBS-001-slice-renumbering.md. While investigating, found two things not yet resolved by this commit: - DEBT-017 (duplicate ApprovalWorkflow implementation): the tested backend (ApprovalWorkflow/) is [DontRegister]'d dead code; the live one (Features/ApprovalWorkflow/, wired in Program.cs) has no dedicated tests. AEG-VS-26-01 downgraded from COMPLETED to BLOCKED in the tracker pending an architect decision on which implementation is canonical. - Features/MarketData and Features/Portfolio (VS-03/04/05/08 Market Data Ingestion Dashboard, Portfolio Rebalance, Risk Metrics, Dashboard) are a third, already-implemented-and-tested body of work entirely absent from WBS_PROGRESS_TRACKER.csv. Flagged in CURRENT_ROADMAP.md as a follow-up. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
7.2 KiB
VS-29: Portfolio Reconciliation (Sell Decision → Trade → Holdings)
Vertical Slice: VS-29 (Portfolio Reconciliation)
Version: 1.0 COMPLETE
Date: 2026-08-07
Owner: Data Architecture + Finance
Status: ✅ READY FOR IMPLEMENTATION
Depends On: K (trade execution), uses VS-26 (approval) + VS-27 (audit)
📋 User Story
As a portfolio manager / compliance officer
I want to reconcile portfolio holdings after trade execution
So that we can verify execution accuracy, track cost basis, and detect discrepancies
Acceptance Criteria:
- ✅ Holdings updated after trade execution (quantity, cost basis)
- ✅ Cost basis tracked: weighted average, FIFO/LIFO support
- ✅ Gain/loss calculated (unrealized, realized on sale)
- ✅ Mismatches detected: quantity, price, timing, settlement variance
- ✅ Audit trail immutable (reconciliation_logs INSERT-only)
- ✅ API endpoints: GET holdings state, GET mismatch discrepancies
- ✅ Daily/weekly reconciliation reporting
🎯 Non-Goals
- ❌ Tax lot assignment strategies (use FIFO by default)
- ❌ Real-time market valuation (use T+1 settlement assumption)
- ❌ Corporate actions (splits, dividends) handling (deferred)
- ❌ Multi-account consolidation (single account only for v1.0)
📊 Reconciliation Flow
Trade Executed (from VS-28)
↓
Extract trade details: quantity, price, settlement date
↓
Validate against approval (from VS-26)
↓
Update holdings: quantity ± executed
↓
Calculate cost basis: weighted average
↓
Calculate gain/loss: (market_value - cost_basis)
↓
Detect mismatches: quantity, price, timing, settlement
↓
Log reconciliation event (immutable, INSERT-only)
↓
Generate reconciliation report (daily/weekly)
↓
Alert on discrepancies (for manual review)
📊 Data Schema
holdings (Current Portfolio State)
CREATE TABLE holdings (
id UUID PRIMARY KEY,
security_id UUID NOT NULL REFERENCES financial_security_master.securities(id),
quantity INT NOT NULL DEFAULT 0,
weighted_avg_cost DECIMAL(15,2) NOT NULL DEFAULT 0,
total_cost_basis DECIMAL(18,2) NOT NULL DEFAULT 0,
market_value DECIMAL(18,2), -- T+1 settlement basis
unrealized_gain_loss DECIMAL(18,2), -- (market_value - cost_basis)
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
correlation_id UUID NOT NULL,
revision INT NOT NULL DEFAULT 1
);
CREATE INDEX idx_holdings_security_id ON holdings(security_id);
CREATE INDEX idx_holdings_correlation_id ON holdings(correlation_id);
reconciliation_logs (Immutable Audit Trail)
CREATE TABLE reconciliation_logs (
id UUID PRIMARY KEY,
trade_id UUID NOT NULL REFERENCES trades(id),
holding_id UUID NOT NULL REFERENCES holdings(id),
quantity_before INT,
quantity_after INT,
cost_basis_delta DECIMAL(18,2),
unrealized_gain_loss_delta DECIMAL(18,2),
mismatch_detected BOOLEAN DEFAULT FALSE,
mismatch_reason TEXT, -- e.g., "quantity_variance", "price_variance", "settlement_delay"
reconciled_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
published_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
correlation_id UUID NOT NULL
);
CREATE INDEX idx_reconciliation_logs_trade_id ON reconciliation_logs(trade_id);
CREATE INDEX idx_reconciliation_logs_holding_id ON reconciliation_logs(holding_id);
CREATE INDEX idx_reconciliation_logs_mismatch ON reconciliation_logs(mismatch_detected);
🔄 Cost Basis Calculation
Weighted Average Method
New Weighted Avg Cost =
(Previous Cost Basis + New Purchase Cost) / Total Quantity
Gain/Loss = Market Value - Total Cost Basis
Unrealized = Market Value - Cost Basis (for open positions)
Realized = (Execution Price - Avg Cost) × Quantity Sold
FIFO/LIFO Tracking (Lot Level)
CREATE TABLE lots (
id UUID PRIMARY KEY,
holding_id UUID REFERENCES holdings(id),
purchase_date DATE,
quantity INT,
unit_cost DECIMAL(15,2),
total_cost DECIMAL(18,2),
status VARCHAR(50), -- OPEN, PARTIAL_SOLD, CLOSED
fifo_order INT, -- For FIFO sequencing
published_at TIMESTAMPTZ,
correlation_id UUID
);
🔐 Mismatch Detection Rules
| Type | Condition | Alert Level |
|---|---|---|
| Quantity | Executed ≠ Approved (>0.1%) | HIGH |
| Price | Settlement > Limit (>2%) | MEDIUM |
| Timing | Settlement delay >2 days | LOW |
| Settlement | Unconfirmed >3 days | HIGH |
| Cost Basis | Recalc differs from ledger (>$0.01) | MEDIUM |
📋 API Contract
GET /reconciliation/holdings (Current Portfolio)
Query Params: security_id=uuid, include_mismatch=bool
Response (200):
{
"items": [
{
"id": "holding-uuid",
"securityId": "security-uuid",
"quantity": 100,
"weightedAvgCost": 150.50,
"totalCostBasis": 15050.00,
"marketValue": 18750.00,
"unrealizedGainLoss": 3700.00,
"updatedAt": "2026-09-10T14:30:00Z",
"correlationId": "correlation-uuid"
}
],
"total": 1,
"pages": 1
}
GET /reconciliation/mismatches (Flagged Discrepancies)
Query Params: severity=HIGH|MEDIUM|LOW, dateFrom=2026-09-01, dateTo=2026-09-30
Response (200):
{
"items": [
{
"id": "log-uuid",
"tradeId": "trade-uuid",
"mismatchReason": "quantity_variance",
"quantity": {"before": 100, "after": 99},
"costBasisDelta": -150.50,
"detectedAt": "2026-09-10T14:30:00Z"
}
],
"total": 2,
"pages": 1
}
✅ Governance Gates
Pre-Merge Gates
- Schema: 3NF normalized, PIT tracked (published_at + correlation_id)
- Calculation: Weighted avg cost, FIFO/LIFO lot tracking tested
- Mismatch: Detection rules defined + prioritized
- Immutability: reconciliation_logs INSERT-only, no UPDATE/DELETE
- Audit: All state changes logged with CorrelationId
Post-Merge Validation (Deferred)
- Integration tests (E2E trade → holdings update)
- Cost basis calculation verified vs. accounting standards
- Mismatch alert accuracy (low false-positive rate)
- Performance: reconciliation completes <5 seconds
🛡️ Security & Compliance
Immutability Guarantees:
- INSERT-only reconciliation_logs (no UPDATE, no DELETE)
- Timestamp immutable after insertion
- Correlation_id immutable (traceability)
Regulatory Requirements:
- Cost basis accuracy (audited annually)
- Lot tracking (tax reporting compliance)
- Mismatch documentation (compliance review)
Access Control:
- Portfolio Manager: Read/reconcile holdings
- Finance: Read cost basis + gain/loss
- Compliance: Read mismatch alerts + audit trail
- System: Automatic reconciliation (no manual entry)
📋 Related Specifications
- VS-26: Approval workflow (approval_proposals, evidence linkage)
- VS-27: Audit trail (reconciliation events logged)
- K (VS-28): Trade execution (provides trade_id, quantity, price)
- VS-00: PIT envelope (published_at, correlation_id, revision)
Co-Authored-By: Claude Haiku 4.5 noreply@anthropic.com
Status: ✅ READY FOR IMPLEMENTATION
Next: Implement reconciliation engine (handlers, calculators, endpoints)