feat: Phase 3 VS-08 Risk Dashboard — GOV+DATA+DOMAIN+BE+FE (5/7)
- VS-08_DASHBOARD_SLICE_SPEC.md: Comprehensive dashboard specification - VS-08_DATA_CONTRACT.md: PIT aggregation schema + caching strategy - VS08_DashboardPolicy.cs: Aggregation logic (health score, insights, validation) - VS08_DashboardEndpoint.cs: GET /api/dashboard/risk + cache layer - RiskDashboard.vue: Unified portfolio view with real-time metrics - VS08_DashboardIntegrationTests.cs: 5 core policy tests Status: GOV+DATA+DOMAIN+BE+ASYNC+FE complete (5/7 vertical slices) TESTOPS: In progress (test suite has minor compatibility issues with VS-04/07) Cumulative: Phase 2 Batch 3 + Phase 3 = 27/36 components (75% COMPLETE) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
# Phase 2 Batch 3-4: Risk & Portfolio Domain (VS-04~08)
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
**Domain:** Portfolio composition, risk metrics, stress testing, alerts, dashboard
|
||||
**Pattern:** Vertical Slice (GOV → DATA → DOMAIN → BE → ASYNC → FE → TESTOPS)
|
||||
**Strategy:** AGENTS.md v16.0 WBS Optimization — execute all non-blocking tasks immediately
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ Component Structure
|
||||
|
||||
| VS | Name | Purpose | Data Model | Endpoint | Event |
|
||||
|----|------|---------|------------|----------|-------|
|
||||
| **VS-04** | Portfolio Composition | Aggregate positions & risk weights | `portfolios.*` (PIT) | POST /api/portfolio/rebalance | PortfolioRebalanced |
|
||||
| **VS-05** | Risk Metrics | VAR, Sharpe, Sortino calculations | `risk_metrics.*` (PIT) | GET /api/portfolio/{id}/risk | RiskMetricsCalculated |
|
||||
| **VS-06** | Stress Testing | Scenario analysis (bull/bear/rate-shock) | `stress_tests.*` (append-only) | POST /api/portfolio/{id}/stress | StressTestCompleted |
|
||||
| **VS-07** | Risk Alerts | Threshold breach + escalation | `risk_alerts.*` (soft-delete) | GET /api/portfolio/{id}/alerts | RiskAlertTriggered |
|
||||
| **VS-08** | Risk Dashboard | Real-time risk aggregation + UI | `risk_dashboard_agg` (denorm) | GET /api/dashboard/risk | (read-only) |
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Dependencies & Parallelization
|
||||
|
||||
```
|
||||
VS-04 (Portfolio Composition)
|
||||
↓
|
||||
VS-05 (Risk Metrics) ← requires portfolio data
|
||||
↓
|
||||
VS-06 (Stress Testing) ← requires risk metrics
|
||||
↓
|
||||
VS-07 (Risk Alerts) ← requires stress results
|
||||
↓
|
||||
VS-08 (Risk Dashboard) ← aggregates all above
|
||||
```
|
||||
|
||||
**Parallelizable:**
|
||||
- Each VS can be GOV+DATA defined in parallel (9 docs in parallel)
|
||||
- DOMAIN logic for VS-04 & VS-05 in parallel (once specs done)
|
||||
- BE endpoints for all VS in parallel (once DOMAIN ready)
|
||||
|
||||
**Critical Path:**
|
||||
- VS-04 DATA must complete before VS-05 DOMAIN
|
||||
- VS-05 DOMAIN must complete before VS-06 BE
|
||||
- Total: Sequential on hot path, but 40% parallelization possible
|
||||
|
||||
---
|
||||
|
||||
## 📅 WBS Schedule (Optimized)
|
||||
|
||||
**Day 1 (Today): GOV + DATA (All 5 VS)**
|
||||
- VS-04: `VS04_PORTFOLIO_SLICE_SPEC.md` + `VS04_DATA_CONTRACT.md`
|
||||
- VS-05: `VS05_RISK_METRICS_SLICE_SPEC.md` + `VS05_DATA_CONTRACT.md`
|
||||
- VS-06: `VS06_STRESS_TESTING_SLICE_SPEC.md` + `VS06_DATA_CONTRACT.md`
|
||||
- VS-07: `VS07_RISK_ALERTS_SLICE_SPEC.md` + `VS07_DATA_CONTRACT.md`
|
||||
- VS-08: `VS08_RISK_DASHBOARD_SLICE_SPEC.md` + (no separate data schema)
|
||||
- **Deliverable:** 9 spec documents, schema validation complete
|
||||
|
||||
**Day 2: DOMAIN (VS-04, 05, 06, 07)**
|
||||
- VS-04: Portfolio aggregation logic (12 tests)
|
||||
- VS-05: Risk calculation logic (15 tests)
|
||||
- VS-06: Scenario application logic (10 tests)
|
||||
- VS-07: Alert threshold evaluation (8 tests)
|
||||
- **Parallel:** All 4 can run in parallel after specs
|
||||
- **Deliverable:** 45 unit tests, 4/4 domains PASS
|
||||
|
||||
**Day 3: BE + ASYNC (All 5 VS)**
|
||||
- VS-04: Rebalance endpoint + Hangfire job
|
||||
- VS-05: Risk metrics fetch endpoint + background calculator
|
||||
- VS-06: Stress test trigger + async batch processing
|
||||
- VS-07: Alert query endpoint + event publisher
|
||||
- VS-08: Aggregation endpoint (read-only)
|
||||
- **Deliverable:** 5 endpoints, 5 async jobs, 20 tests
|
||||
|
||||
**Day 4: FE + TESTOPS (Batch 3)**
|
||||
- VS-04: Rebalance form + confirmation dialog
|
||||
- VS-05: Risk metrics display + trend charts
|
||||
- VS-06: Scenario builder UI + results visualization
|
||||
- VS-07: Alert list + drill-down view
|
||||
- VS-08: Risk dashboard (aggregate KPIs + real-time updates)
|
||||
- **Deliverable:** 5 FE components, 12+ E2E tests
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Acceptance Criteria (AGENTS.md v16.0)
|
||||
|
||||
**Per VS:**
|
||||
- ✅ Contract-first: Specs + schema before code
|
||||
- ✅ SOLID: No cross-cutting concerns, single responsibility
|
||||
- ✅ Complexity: Cyclomatic complexity ≤ 10 (Policy exceptions)
|
||||
- ✅ Idempotency: All jobs + scenarios replay-safe
|
||||
- ✅ Audit: Correlation IDs, event published, PIT versioned
|
||||
- ✅ Safety: Transaction boundaries, soft-deletes, no partial success
|
||||
- ✅ Testing: Unit → Integration → Data → E2E coverage
|
||||
- ✅ Traceability: ADR links, evidence preserved
|
||||
|
||||
**Cross-VS:**
|
||||
- ✅ No SELECT * or direct module-to-module queries
|
||||
- ✅ Async coupling via Outbox/Inbox (no direct function calls)
|
||||
- ✅ Tech debt registered (if any deferral)
|
||||
- ✅ Architecture tests pass
|
||||
- ✅ All prior tests still pass (no regressions)
|
||||
|
||||
---
|
||||
|
||||
## 📊 Success Metrics
|
||||
|
||||
| Metric | Target | Checkpoint |
|
||||
|--------|--------|------------|
|
||||
| Test Pass Rate | 100% | End of each day |
|
||||
| Architecture Violations | 0 | Before commit |
|
||||
| Tech Debt Registered | 100% | In PR description |
|
||||
| Code Review Comments | <5 | Per PR |
|
||||
| Build Time | <5s | Continuous |
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Execution Plan (This Session)
|
||||
|
||||
**Phase 2 Batch 3 Start (VS-04~07):**
|
||||
|
||||
1. ✅ Confirm domain scope (Risk & Portfolio) — **DONE**
|
||||
2. ⏳ GOV + DATA (9 docs, parallel) — **START NOW**
|
||||
3. ⏳ DOMAIN (4 VS, parallel) — **Follow after specs**
|
||||
4. ⏳ BE + ASYNC (5 endpoints, parallel) — **Follow after domain**
|
||||
5. ⏳ FE + TESTOPS (5 components, Batch 3) — **Follow after BE**
|
||||
|
||||
**Phase 2 Batch 4 (VS-08):**
|
||||
6. ⏳ Risk Dashboard (depends on all others)
|
||||
7. ⏳ Final integration testing
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
- **SSH Tunnel:** Required for any DB-backed integration tests. Keep open during dev.
|
||||
- **Parallel Execution:** GOV+DATA can be written concurrently; post in 5 separate docs
|
||||
- **Debt Threshold:** Keep new debt <20 impact points per batch (manage quarterly paydown)
|
||||
- **Git Strategy:** One commit per component (GOV+DATA) or (DOMAIN) or (BE+ASYNC), then squash if needed
|
||||
|
||||
---
|
||||
|
||||
**Status:** READY TO START
|
||||
**Next Command:** Begin VS-04 GOV specification
|
||||
@@ -0,0 +1,152 @@
|
||||
# VS-08: Risk Dashboard — Vertical Slice Specification
|
||||
|
||||
**Domain:** Comprehensive Risk Monitoring
|
||||
**Capability:** Real-time aggregation of portfolio, risk metrics, stress scenarios, and alerts
|
||||
**User Goal:** "I need a unified view of my entire portfolio risk profile in one dashboard"
|
||||
|
||||
---
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- Custom dashboard builder (fixed layout)
|
||||
- Real-time market tick updates (EOD refresh acceptable)
|
||||
- Mobile-optimized view (desktop focus)
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Functional
|
||||
|
||||
| Req ID | Description | RBAC | SLA | Evidence |
|
||||
|--------|-------------|------|-----|----------|
|
||||
| **DASH-001** | GET /api/dashboard/risk | DataReader | <500ms | Aggregated JSON |
|
||||
| **DASH-002** | Render portfolio composition (VS-04) | System | <100ms FE | Visual table |
|
||||
| **DASH-003** | Display risk metrics (VS-05) | System | <100ms FE | Metric cards |
|
||||
| **DASH-004** | Show stress scenarios (VS-06) | System | <100ms FE | Scenario grid |
|
||||
| **DASH-005** | List active alerts (VS-07) | System | <100ms FE | Alert badges |
|
||||
| **DASH-006** | Real-time updates via SignalR | System | <5s latency | WebSocket push |
|
||||
|
||||
### Non-Functional
|
||||
|
||||
- **Availability:** 99.5%
|
||||
- **Latency:** <500ms aggregation, <100ms FE render
|
||||
- **Caching:** Cache dashboard for <1hr (refresh on alert escalation)
|
||||
- **Audit:** All data sourced from authoritative VS-04~07 tables
|
||||
|
||||
---
|
||||
|
||||
## State Transitions
|
||||
|
||||
```
|
||||
Portfolio Snapshot (VS-04)
|
||||
Risk Metrics (VS-05)
|
||||
Stress Results (VS-06)
|
||||
Risk Alerts (VS-07)
|
||||
↓ (All aggregated)
|
||||
Dashboard Data (VS-08)
|
||||
↓ (Publish event)
|
||||
DashboardUpdated event → SignalR push
|
||||
```
|
||||
|
||||
**Frequency:** On-demand + event-driven updates
|
||||
**Real-time:** SignalR WebSocket (no polling)
|
||||
|
||||
---
|
||||
|
||||
## Data & API Contracts
|
||||
|
||||
### GET /api/dashboard/risk
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"portfolioId": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"snapshotDate": "2026-08-05",
|
||||
"portfolio": {
|
||||
"totalValue": 42700.00,
|
||||
"positions": [
|
||||
{
|
||||
"symbol": "AAPL",
|
||||
"quantity": 100,
|
||||
"marketValue": 15025,
|
||||
"weightPercent": 35.3
|
||||
}
|
||||
]
|
||||
},
|
||||
"riskMetrics": {
|
||||
"var95": 15250,
|
||||
"sharpe": 1.85,
|
||||
"sortino": 2.45,
|
||||
"volatility": 0.185,
|
||||
"concentration": {
|
||||
"topFivePercent": 52.3,
|
||||
"maxPosition": 40.0
|
||||
}
|
||||
},
|
||||
"stressResults": [
|
||||
{
|
||||
"scenario": "bull",
|
||||
"portfolioLoss": 12500,
|
||||
"lossPercent": 4.2,
|
||||
"stressedVar": 13750
|
||||
}
|
||||
],
|
||||
"activeAlerts": [
|
||||
{
|
||||
"alertId": "550e8400-e29b-41d4-a716-446655440008",
|
||||
"threshold": "Concentration",
|
||||
"severity": "Warning",
|
||||
"message": "Top 5 holdings at 52.3%"
|
||||
}
|
||||
],
|
||||
"lastUpdate": "2026-08-05T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### SignalR Message
|
||||
|
||||
**DashboardUpdated:**
|
||||
```json
|
||||
{
|
||||
"eventType": "DashboardUpdated",
|
||||
"portfolioId": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"changedComponents": ["riskMetrics", "activeAlerts"],
|
||||
"updatedAt": "2026-08-05T10:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## RBAC & Authorization
|
||||
|
||||
| Operation | Role | Condition |
|
||||
|-----------|------|-----------|
|
||||
| VIEW dashboard | DataReader | Own portfolio only |
|
||||
| TRIGGER refresh | DataAnalyst | Manual override |
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Unit:** Data aggregation logic (5 tests)
|
||||
2. **Integration:** DB → aggregation → API (4 tests)
|
||||
3. **E2E:** Full dashboard load + SignalR push (2 tests)
|
||||
4. **Golden:** Known portfolio → expected snapshot
|
||||
|
||||
---
|
||||
|
||||
## Assumptions
|
||||
|
||||
- All VS-04~07 data is fresh (<1hr old)
|
||||
- SignalR hub is available (separate deployment)
|
||||
- Portfolio ID is authenticated via RBAC
|
||||
|
||||
---
|
||||
|
||||
## Vertical Slice Boundary
|
||||
|
||||
✅ **In Scope:** Aggregation logic + API endpoint + real-time updates
|
||||
❌ **Out of Scope:** Custom drill-down reports, export functionality
|
||||
|
||||
**Rationale:** Minimal, read-only aggregation; all mutations in VS-04~07
|
||||
@@ -0,0 +1,265 @@
|
||||
# VS-08: Risk Dashboard — Data Contract
|
||||
|
||||
**Domain:** Comprehensive Risk Monitoring
|
||||
**Pattern:** Point-in-Time (PIT) Read Model + Event Stream
|
||||
|
||||
---
|
||||
|
||||
## Schema Overview
|
||||
|
||||
| Table | Purpose | Ownership | TTL |
|
||||
|-------|---------|-----------|-----|
|
||||
| `risk_management.dashboard_snapshots` | Cached aggregations (portfolio + risk + stress + alerts) | VS-08 | <1hr |
|
||||
| `risk_management.vw_dashboard_data` | JOIN view (portfolio_positions + risk_metrics + stress + alerts) | Read-only | — |
|
||||
|
||||
### dashboard_snapshots (PIT Write Model)
|
||||
|
||||
Cached snapshot of portfolio risk profile, refreshed on-demand or event-triggered.
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS risk_management.dashboard_snapshots (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
portfolio_id UUID NOT NULL,
|
||||
snapshot_date DATE NOT NULL,
|
||||
|
||||
-- Portfolio aggregates
|
||||
total_portfolio_value DECIMAL(18, 2) NOT NULL,
|
||||
position_count INT NOT NULL,
|
||||
|
||||
-- Risk metrics (VS-05)
|
||||
var95 DECIMAL(18, 2),
|
||||
sharpe_ratio NUMERIC(5, 2),
|
||||
sortino_ratio NUMERIC(5, 2),
|
||||
volatility_percent NUMERIC(5, 2),
|
||||
concentration_top_five_percent NUMERIC(5, 2),
|
||||
max_position_percent NUMERIC(5, 2),
|
||||
|
||||
-- Stress scenario flags (VS-06)
|
||||
bull_scenario_loss_percent NUMERIC(6, 2),
|
||||
bear_scenario_loss_percent NUMERIC(6, 2),
|
||||
rate_shock_loss_percent NUMERIC(6, 2),
|
||||
vol_spike_loss_percent NUMERIC(6, 2),
|
||||
|
||||
-- Alert count (VS-07)
|
||||
alert_initial_count INT DEFAULT 0,
|
||||
alert_warning_count INT DEFAULT 0,
|
||||
alert_critical_count INT DEFAULT 0,
|
||||
|
||||
-- Audit
|
||||
published_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
revision INT DEFAULT 1,
|
||||
source_component VARCHAR(50) NOT NULL, -- 'api' or 'event'
|
||||
|
||||
CONSTRAINT fk_portfolio FOREIGN KEY (portfolio_id)
|
||||
REFERENCES risk_management.portfolios(id),
|
||||
CONSTRAINT unique_snapshot_per_portfolio_per_date
|
||||
UNIQUE(portfolio_id, snapshot_date, published_at DESC)
|
||||
);
|
||||
|
||||
CREATE INDEX idx_dashboard_portfolio_date
|
||||
ON risk_management.dashboard_snapshots(portfolio_id, snapshot_date DESC);
|
||||
```
|
||||
|
||||
### vw_dashboard_data (Read-Only JOIN View)
|
||||
|
||||
Real-time aggregation view joining VS-04~07 source tables. Used by API endpoint for <500ms latency.
|
||||
|
||||
```sql
|
||||
CREATE OR REPLACE VIEW risk_management.vw_dashboard_data AS
|
||||
SELECT
|
||||
p.portfolio_id,
|
||||
p.snapshot_date,
|
||||
|
||||
-- Portfolio (VS-04)
|
||||
COUNT(DISTINCT pp.symbol) as position_count,
|
||||
SUM(pp.market_value) as total_portfolio_value,
|
||||
|
||||
-- Risk Metrics (VS-05)
|
||||
(SELECT var95 FROM risk_management.risk_metrics
|
||||
WHERE portfolio_id = p.portfolio_id
|
||||
AND published_at <= CURRENT_TIMESTAMP
|
||||
AND removed_at IS NULL
|
||||
ORDER BY published_at DESC LIMIT 1) as var95,
|
||||
|
||||
(SELECT sharpe_ratio FROM risk_management.risk_metrics
|
||||
WHERE portfolio_id = p.portfolio_id
|
||||
AND published_at <= CURRENT_TIMESTAMP
|
||||
AND removed_at IS NULL
|
||||
ORDER BY published_at DESC LIMIT 1) as sharpe_ratio,
|
||||
|
||||
-- Stress (VS-06)
|
||||
(SELECT portfolio_loss_percent FROM risk_management.stress_test_results
|
||||
WHERE portfolio_id = p.portfolio_id
|
||||
AND scenario_name = 'bear'
|
||||
AND published_at <= CURRENT_TIMESTAMP
|
||||
ORDER BY published_at DESC LIMIT 1) as bear_loss_percent,
|
||||
|
||||
-- Alerts (VS-07)
|
||||
COUNT(CASE WHEN ra.severity = 'Warning' THEN 1 END) as warning_alert_count
|
||||
|
||||
FROM risk_management.portfolios p
|
||||
LEFT JOIN risk_management.portfolio_positions pp
|
||||
ON p.id = pp.portfolio_id
|
||||
AND pp.published_at <= CURRENT_TIMESTAMP
|
||||
AND pp.removed_at IS NULL
|
||||
LEFT JOIN risk_management.risk_alerts ra
|
||||
ON p.id = ra.portfolio_id
|
||||
AND ra.published_at <= CURRENT_TIMESTAMP
|
||||
AND ra.removed_at IS NULL
|
||||
AND ra.resolved_at IS NULL
|
||||
WHERE p.published_at <= CURRENT_TIMESTAMP
|
||||
AND p.removed_at IS NULL
|
||||
GROUP BY p.id, p.snapshot_date;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Query Patterns
|
||||
|
||||
### 1. Fetch Dashboard Snapshot (GET /api/dashboard/risk)
|
||||
|
||||
**Source:** `dashboard_snapshots` cache OR `vw_dashboard_data` (fallback)
|
||||
|
||||
```sql
|
||||
-- Try cache first (< 1 hour)
|
||||
SELECT * FROM risk_management.dashboard_snapshots
|
||||
WHERE portfolio_id = $1
|
||||
AND snapshot_date >= CURRENT_DATE - INTERVAL '1 hour'
|
||||
AND published_at <= $2
|
||||
ORDER BY published_at DESC
|
||||
LIMIT 1;
|
||||
|
||||
-- Fallback: read-only view (real-time)
|
||||
SELECT * FROM risk_management.vw_dashboard_data
|
||||
WHERE portfolio_id = $1
|
||||
AND snapshot_date = CURRENT_DATE;
|
||||
```
|
||||
|
||||
### 2. Refresh Dashboard on Event
|
||||
|
||||
**Trigger:** PortfolioRebalanced, PortfolioMetricsCalculated, StressTestCompleted, AlertEscalated
|
||||
|
||||
```sql
|
||||
INSERT INTO risk_management.dashboard_snapshots (
|
||||
portfolio_id, snapshot_date, total_portfolio_value, position_count,
|
||||
var95, sharpe_ratio, alert_warning_count, source_component, published_at
|
||||
)
|
||||
SELECT
|
||||
portfolio_id, CURRENT_DATE,
|
||||
COALESCE(total_portfolio_value, 0),
|
||||
COALESCE(position_count, 0),
|
||||
var95, sharpe_ratio, warning_alert_count,
|
||||
'event', CURRENT_TIMESTAMP
|
||||
FROM risk_management.vw_dashboard_data
|
||||
WHERE portfolio_id = $1
|
||||
ON CONFLICT (portfolio_id, snapshot_date, published_at DESC)
|
||||
DO UPDATE SET
|
||||
total_portfolio_value = EXCLUDED.total_portfolio_value,
|
||||
revision = revision + 1,
|
||||
published_at = CURRENT_TIMESTAMP;
|
||||
```
|
||||
|
||||
### 3. List All Positions (for dashboard visualization)
|
||||
|
||||
```sql
|
||||
SELECT symbol, quantity, market_price, market_value, weight_percent
|
||||
FROM risk_management.portfolio_positions
|
||||
WHERE portfolio_id = $1
|
||||
AND published_at <= $2
|
||||
AND removed_at IS NULL
|
||||
ORDER BY weight_percent DESC;
|
||||
```
|
||||
|
||||
### 4. List Active Alerts
|
||||
|
||||
```sql
|
||||
SELECT alert_id, threshold_type, current_value, severity, message
|
||||
FROM risk_management.risk_alerts
|
||||
WHERE portfolio_id = $1
|
||||
AND published_at <= $2
|
||||
AND removed_at IS NULL
|
||||
AND resolved_at IS NULL
|
||||
ORDER BY severity DESC, triggered_at DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Idempotency & Concurrency
|
||||
|
||||
**Idempotency Key:** `(portfolio_id, snapshot_date, source_component)`
|
||||
|
||||
- Cache refresh from event is idempotent (no duplicates via UPSERT)
|
||||
- Multiple concurrent API calls return same cached result
|
||||
- View queries are always consistent (no transaction isolation needed)
|
||||
|
||||
---
|
||||
|
||||
## Performance SLA
|
||||
|
||||
| Query | Source | Latency | Cache |
|
||||
|-------|--------|---------|-------|
|
||||
| Dashboard snapshot | `dashboard_snapshots` | <100ms | 1 hour |
|
||||
| Fallback (real-time) | `vw_dashboard_data` | <500ms | — |
|
||||
| Active alerts | Direct table | <50ms | — |
|
||||
| Positions table | Direct table | <100ms | — |
|
||||
|
||||
**Indexes:**
|
||||
```sql
|
||||
CREATE INDEX idx_dashboard_portfolio_date
|
||||
ON risk_management.dashboard_snapshots(portfolio_id, snapshot_date DESC);
|
||||
|
||||
CREATE INDEX idx_portfolio_positions_portfolio_date
|
||||
ON risk_management.portfolio_positions(portfolio_id, trading_date DESC);
|
||||
|
||||
CREATE INDEX idx_risk_alerts_portfolio_resolved
|
||||
ON risk_management.risk_alerts(portfolio_id, resolved_at, published_at DESC);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Event Publishing (Outbox Integration)
|
||||
|
||||
When dashboard is refreshed, emit event for SignalR push:
|
||||
|
||||
**Event: DashboardUpdated**
|
||||
```json
|
||||
{
|
||||
"eventType": "DashboardUpdated",
|
||||
"portfolioId": "550e8400-e29b-41d4-a716-446655440001",
|
||||
"changedComponents": ["riskMetrics", "activeAlerts"],
|
||||
"snapshotId": "550e8400-e29b-41d4-a716-446655440002",
|
||||
"updatedAt": "2026-08-05T10:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
Published via: `shared.outbox` → Hangfire → SignalR Hub → `DashboardHub.UpdateDashboard(portfolioId)`
|
||||
|
||||
---
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
1. **Unit:** Aggregation SQL queries (with mock data)
|
||||
2. **Integration:** Dashboard endpoint → cache hit/miss → DB fallback
|
||||
3. **E2E:** Event trigger → dashboard update → SignalR push
|
||||
4. **Golden:** Known portfolio snapshot → expected aggregates (variance <0.01%)
|
||||
|
||||
---
|
||||
|
||||
## Assumptions
|
||||
|
||||
- All source tables (VS-04~07) maintain PIT audit trail
|
||||
- `published_at <= cutoff` enforced on all source reads
|
||||
- Cache TTL managed by application (not DB expiry)
|
||||
- SignalR hub configured separately; dashboard job just publishes event
|
||||
|
||||
---
|
||||
|
||||
## Migration
|
||||
|
||||
**DbUp Script:** `0034_VS08_DashboardSchema.sql`
|
||||
|
||||
```sql
|
||||
-- Create tables, views, indexes
|
||||
-- Seed initial cache from existing data if present
|
||||
-- Grant SELECT on views to DataReader role
|
||||
```
|
||||
Reference in New Issue
Block a user