Skip to content

FeeManager

Handles fee tier lookup and 14-day trailing volume tracking for trade fee calculations.

Purpose

FeeManager determines the fee rate for each trade based on a 7-tier volume-based fee structure. It tracks daily trading volume in a daily_volume SQLite table and uses the trailing 14-day volume (excluding the current day) to determine the user's fee tier.

The manager supports both taker fees (market orders, close orders) and maker fees (limit orders). All monetary calculations use BigNumber for exact decimal precision, since SQLite's REAL type lacks the precision needed for crypto trading.

High-Level Design

7-Tier Fee Structure

Tiers are determined by the 14-day trailing volume (excluding the current day):

TierVolume ThresholdTaker FeeMaker Fee
0$00.045%0.015%
1$5M0.040%0.012%
2$25M0.035%0.008%
3$100M0.030%0.004%
4$500M0.028%0.000%
5$2B0.026%0.000%
6$7B0.024%0.000%

Tiers are sorted descending by threshold; the first tier where volume >= threshold wins.

Fee Calculation Flow

Trade submitted (market order / limit order execution / close)

getCurrentFeeTier()
  → get14DayVolume() queries daily_volume for [today-14d, today)
  → Match against FEE_TIERS_BY_VOLUME_DESCENDING

calculateFee(sizeUsd, feeType)
  → fee = sizeUsd × feeRate

Fee adjustment (in OrderExecutionManager)
  → actualFee = min(calculatedFee, max(balance, 0))
  → Orders always execute; fee reduced if balance insufficient

Phase 2 commit (atomic transaction)
  → balanceManager.applyPnL(realizedPnL, actualFee)
  → feeManager.updateVolume(sizeUsd, actualFee, feeType)

Taker vs Maker Classification

  • Taker: Market orders and close orders — immediate execution, takes liquidity
  • Maker: Limit orders — passive, waits for price to cross threshold before executing

The fee type is determined by the caller (OrderExecutionManager passes FeeType.TAKER for market/close, PriceAlertManager passes FeeType.MAKER for limit order execution).

14-Day Rolling Volume Window

Today: 2025-02-01
Window: [2025-01-18 00:00 UTC, 2025-02-01 00:00 UTC)

2025-01-17: Excluded (outside window)
2025-01-18: Included (start of window)
2025-01-31: Included (last day in window)
2025-02-01: Excluded (current day)

Current day exclusion: The current day's trades are excluded from the volume calculation. This prevents the fee tier from changing mid-day as the user trades. The tier is effectively locked at UTC midnight.

No tier caching: The tier is recalculated fresh on every order by querying the database. This guarantees correctness without stale cache concerns.

Daily Volume Tracking

updateVolume() maintains a per-day, per-account record with taker/maker breakdown:

  • If record exists for today: Reads current values, adds new volume/fee via BigNumber arithmetic, updates in place
  • If no record: Inserts a new row with the trade's volume and fee

Each record tracks volume_usd (total), taker_volume_usd, maker_volume_usd, and total_fee_usd — providing a per-day breakdown for analytics and audit.

The volume_usd column is TEXT (not REAL) because SQLite REAL has ~15 significant digits — insufficient for high-volume accounts. All arithmetic is done in JavaScript with BigNumber, then serialized back to TEXT.

Account Scoping

All volume queries are scoped to account_id, not user_id. Each challenge attempt or account reset creates a new account_id, so:

  • Volume doesn't carry across resets
  • Each new attempt starts at Tier 0
  • Historical volume data is retained (immutable) but isolated by account

Fee Affordability Adjustment

Orders always execute regardless of balance. If the user's balance can't cover the full fee, the fee is reduced to whatever the balance can afford (floored at zero). Only the actualFee (the capped amount) is recorded in the trades table. This "eventually correct" approach keeps trading functional while MLL breach detection handles account failure separately.

Volume Accounting Rules

  • Only executed trades update volume (pending orders and cancellations do not)
  • Volume updates happen inside the Phase 2 atomic transaction alongside balance and position changes
  • Idempotency keys at the RPC layer prevent duplicate volume entries from retried requests

Edge Cases & Error Handling

  • cleanupOldVolume() is a no-op: historical volume data is kept for audit trail since queries filter by account_id
  • Fee tiers are recalculated per-order (no caching) to avoid stale tier after UTC midnight rollover

See Also