Appearance
ExtremesTrackingManager
Detects peak and bottom account values by replaying price history against coefficient snapshots.
Purpose
ExtremesTrackingManager is the core computation engine for account value extremes detection. It processes batches of price data, calculates account values at each timestamp using the coefficient formula, and tracks the highest (peak) and lowest (bottom) values encountered.
High-Level Design
Two Modes of Operation
The manager serves two distinct processing paths within the alarm cycle:
1. Incremental Daily Extremes (processTrackingBatch)
Called during the alarm's batch processing phase. Picks up from the tracking's lastProcessedTimestamp + 1, carries forward existing daily peak/bottom, and extends them with new price data. This runs every alarm cycle (5 min) as new prices arrive.
alarm → batchProcessPrices → processPriceBatchForTrackings
→ ExtremesTrackingManager.processTrackingBatch(tracking, prices)
→ AccountValueTrackingManager.updateTrackingExtremes(result)2. On-Demand Hourly Replay (calculateHourlyExtremes)
Called at hour boundaries. Replays an entire hour's price data from scratch — no accumulated state, no dependency on incremental tracking. Produces a self-contained hourly snapshot.
alarm → handleHourBoundary → emitHourlyExtremes(previousHourStart)
→ PriceFetchManager.fetchPrices(hourStart, hourStart + 3599)
→ ExtremesTrackingManager.calculateHourlyExtremes(tracking, prices, hourStart, hourEnd)
→ ExtremesTrackingManager.storeHourlySnapshot(result)
→ CallbackManager.emitHourlyCallbacksBatch(...)This "replay from scratch" approach means ~12× fewer DB writes compared to incrementally persisting hourly state every 5-minute alarm cycle (12 writes/hour → 1), and eliminates race conditions from partial state.
Shared Calculation Core
Both modes delegate to the same private calculateExtremesFromPrices() method:
- Sort and filter timestamps by range and
accountCreatedAt - For each timestamp in ascending order:
- Look up applicable coefficient snapshot via CoefficientManager
- Compute
accountValue = constantTerm + Σ(coefficient[token] × price[token]) - Compare against current peak/bottom, update if new extreme found
- Track closing value (always the latest valid computation)
- Return peak, bottom, closing, last processed timestamp, and stop info
The only difference: processTrackingBatch seeds initial peak/bottom from existing tracking state, while calculateHourlyExtremes starts fresh.
Stop-on-Missing-Data Guarantee
A critical invariant: processing stops immediately when price data is incomplete. This applies when:
- A timestamp's
PriceDatais null or empty - A token with a non-zero coefficient has no price entry (
MissingPriceDataError)
This prevents advancing past timestamps where a true peak or bottom might exist but can't be computed. The caller retries later when data becomes available. The stoppedDueToMissingPrices and stoppedAtTimestamp fields in the result communicate this to the parent DO, which decides whether to wait or advance.
Contrast with null from calculateValueAtTimestamp (no applicable coefficient snapshot yet) — this is a normal skip, not a stop. It means the user had no positions at that timestamp, so no extremes are possible.
Day Boundary Impact
At UTC midnight, after daily callbacks are emitted, AccountValueTrackingManager resets each tracking's peak/bottom to the recalculated midnight value. ExtremesTrackingManager participates via initializeExtremes() (providing the seed values) and calculateInitialValue() (computing account value at midnight price). See parent DO docs for the full day boundary flow.
Hourly Snapshot Storage
Hourly results are persisted to hourly_snapshots using INSERT OR REPLACE on (user_id, account_version, hour_timestamp). This makes storage idempotent — safe to re-emit on retry or DO restart. Snapshots older than 30 days are cleaned up periodically.
Edge Cases & Error Handling
- Processing stops immediately on
MissingPriceDataErrorto avoid skipping potential extremes - Empty price records (
size === 0) also trigger a stop - Non-
MissingPriceDataErrorexceptions (e.g., database errors) are re-thrown - Timestamps before
accountCreatedAtare skipped - If
calculateValueAtTimestampreturnsnull(no applicable snapshot), the timestamp is skipped without stopping calculateHourlyExtremesreturnsnullif account was created afterhourEnd
See Also
- CoefficientManager -- provides the calculation engine
- AccountValueTrackingManager -- manages tracking state and calls
initializeExtremes/calculateInitialValue - CallbackManager -- delivers the computed extremes
- PriceFetchManager -- supplies the price data
- AccountValueAggregationTracker -- parent DO