Appearance
AnalyticsManager
Data layer for portfolio performance analytics — stores time-series snapshots, maintains trade statistics, and serves retrieval queries for charts and summaries.
High-Level Design
AnalyticsManager is a passive storage manager. It never initiates work or schedules alarms. Instead, it receives data from three sources and persists it to SQLite:
┌──────────────────────────────────┐
│ AccountValueTrackerManager │ Hourly/daily callbacks from
│ storeHourlySnapshot() │ AccountValueAggregationTracker DO
│ storeDailySnapshot() │
├──────────────────────────────────┤
│ OrderExecutionManager │ After each position close
│ updateTradeStatistics(pnl) │
├──────────────────────────────────┤
│ ChallengeManager / │ On challenge start/reset
│ ChallengeAdminManager │ (including admin resets)
│ resetTradeStatistics() │
│ purgeOldSnapshots() │
└──────────────┬───────────────────┘
▼
┌──────────────────┐
│ SQLite Tables │
│ daily_snapshots │
│ hourly_snapshots │
│ account (cols) │
│ daily_volume │
└─────────┬────────┘
▼
RPC read methods
(dashboard, charts, summary)Data Domains
1. Time-Series Snapshots
Hourly snapshots arrive from AccountValueTrackerManager.handleHourlyExtremes() every hour. Each records closing account value plus peak/bottom extremes for that hour.
Daily snapshots arrive from handleDailyExtremesFinalized() at UTC midnight. These are richer — they include closing balance, MLL balance, realized and unrealized PnL, and peak/bottom extremes.
Both use UPSERT (INSERT ... ON CONFLICT UPDATE) keyed on (account_id, date|hour_timestamp). This makes storage idempotent: the AccountValueAggregationTracker retries failed callbacks with exponential backoff, and duplicate deliveries simply overwrite with identical data.
2. Trade Statistics (Running Totals)
Rather than aggregating from the trades table on every read, win/loss counts and amounts are stored as running totals directly on the account row. OrderExecutionManager calls updateTradeStatistics(realizedPnL) after each position close:
- PnL > 0 → increment
win_count, add tototal_win_amount - PnL < 0 → increment
loss_count, add tototal_loss_amount - PnL = 0 → skip (breakeven trades excluded)
All arithmetic uses BigNumber (18-decimal precision) in JavaScript, not SQL, to avoid SQLite's 15-digit float limitation.
3. Volume History
Reads from the daily_volume table (written by FeeManager during trade execution). Provides taker/maker volume breakdown and total fees per day. The current (incomplete) day is always excluded.
Challenge Scoping
All retrieval methods filter by the current challenge's challengeStartAt timestamp. This ensures charts and statistics only reflect the active challenge — previous challenge data is invisible without deletion.
On startChallenge(), ChallengeManager calls (and ChallengeAdminManager.resetChallenge() mirrors):
resetTradeStatistics()— zeroes win/loss counters on the account row for the new challengepurgeOldSnapshots()— retains all historical snapshots for audit purposes; only prunes when row counts exceed safety thresholds (300K daily / 7.2M hourly rows, ~10,000 challenges worth)
Bundled Dashboard Query
The parent DO exposes a getAnalyticsDashboard() RPC that calls five AnalyticsManager methods in a single round-trip: daily snapshots, hourly snapshots, volume history, trade statistics, and analytics summary. This avoids 5 separate DO invocations from the client.
Key Design Decisions
- O(1) trade stats: Running totals on the account row avoid scanning the trades table.
- UPSERT idempotency: Safe against duplicate callback delivery from the tracker's retry mechanism.
- JS-side BigNumber: Monetary precision preserved at 18 decimals; SQLite only stores stringified values.
- Stateless: No in-memory state. All data lives in SQLite and survives DO eviction.
- Audit-friendly retention: Historical snapshots are kept across challenges. Pruning only kicks in at extreme row counts (~10,000 challenges). Read queries filter by
challengeStartAtso old data is invisible without deletion.