Appearance
AccountHealthManager
Retrospective account value validation. Detects if account value ever dipped below the MLL threshold between trade events, when unrealized PnL fluctuations could have caused a breach that direct balance checks would miss.
Why Retrospective?
Account failure has two detection paths:
- Direct balance drain — caught immediately by
BalanceManager.applyPnL()when a trade closes andbalance <= minBalance. No positions involved, no snapshot needed. - Unrealized PnL breach — between trades, open positions fluctuate with price. The account value (
balance + unrealizedPnL) may have dipped below threshold at some point without any trade event to trigger detection. This is what AccountHealthManager catches.
The challenge: checking every second of price data is prohibitively expensive. A 24-hour window has 86,400 seconds × N positions × price lookups. The hierarchical search solves this.
Hierarchical Search Algorithm
Three-level funnel that progressively narrows the search space, achieving ~99.89% data reduction:
Level 1: Hourly Candles (coarse)
├── For each hour, calculate worst-case account value
│ LONG positions → use candle.low (maximum loss scenario)
│ SHORT positions → use candle.high (maximum loss scenario)
├── worst-case > threshold → SAFE, skip entirely
└── worst-case ≤ threshold → SUSPICIOUS, drill into Level 2
Level 2: Minute Candles (fine)
├── For each suspicious hour, examine 60 minutes
├── Same worst-case logic with 1m candles
└── Suspicious minutes pass to Level 3
Level 3: 10-Second Prices (exact)
├── For each suspicious minute, check 6 buckets (10s intervals)
├── Calculate EXACT account value using real prices
└── value ≤ threshold → BREACH DETECTEDKey insight: Levels 1-2 are conservative filters. They can produce false positives (marking safe periods as suspicious) but never false negatives. Only Level 3 produces the definitive breach determination.
Position State Reconstruction
The search needs to know what positions existed at any given timestamp. This is rebuilt by replaying the trades table chronologically.
Incremental hourly building (Level 1): Instead of replaying all trades for each hour boundary (O(trades × hours)), a single chronological pass snapshots state at each hour boundary — O(trades + hours).
Per-timestamp reconstruction (Levels 2-3): Replays all trades up to the target timestamp. More expensive but only runs for the small number of suspicious minutes/buckets that survived earlier filters.
Trade actions handled: OPEN (new or add-to-position with weighted average entry), CLOSE (remove position), reversal X_TO_Y (replace with new position in opposite direction).
Account Value Formula
Account Value = balance + totalUnrealizedPnLbalancealready incorporates all realized PnL and fees from past trades- Margin locked in positions and reserved for limit orders is still equity — not subtracted
- This is gross equity, not available margin
Trigger Points
| Trigger | Frequency | Source |
|---|---|---|
| Rolling alarm | Every 24 hours | HealthCheckHandler in TaskScheduler |
| On-demand | On portfolio fetch, 1-min cooldown | BalanceManager.handleGetPortfolioRPC() |
Both call runHealthCheck(lastCheckTimestamp, now) to cover the gap since the last check.
Fast paths (skip hierarchical search):
- Account already failed → no-op
- No positions existed during the entire period → balance-only check
Breach Recording
When a breach is detected:
- Writes snapshot to
account_breach_snapshots(positions, balance, unrealized PnL, margin state at breach time) - Sets
is_failed = 1andaccount_failed_aton the account atomically - Records MLL breach via
MllRiskManager(uses the historically-accurate breach timestamp, not current time) - Calls
ChallengeManager.failChallenge()if challenge is active
Edge Cases
- Missing candle data: Period marked as suspicious (conservative — false positive, never false negative)
- Missing exact price at Level 3: Timestamp skipped (may miss breach — acceptable tradeoff vs blocking on missing data)
- No candle for a symbol: Assumes zero unrealized PnL for that position (neutral, not worst-case)
See Also
- ChallengeManager — receives breach notifications, handles challenge failure
- MllRiskManager — MLL threshold tracking, breach history