Skip to content

UserPaperTradePortfolio

Per-user Durable Object managing the full paper trading lifecycle: position management, order execution, challenge tracking, subscriptions, and account health. Each user gets one instance, persisting all state in SQLite with BigNumber precision for monetary values.

No alarm loop — all execution is event-driven via callbacks from PriceAlert (onAlert, onPriceArrival) and AccountValueAggregationTracker (onHourlyExtremes, onDailyExtremesFinalized).

Architecture

31 managers organized by domain, all inheriting from BaseDurableManager<UserPaperTradePortfolio>. The DO itself is a thin shell — business logic lives entirely in managers that communicate through the parent DO instance.

Price & Order Execution

         AlertRegistrationManager
                   │ registers/deregisters alerts

              PriceAlert ────────────┐
              (read replica)         │
                   ▲                 │ onAlert
      getPriceData │                 │ onPriceArrival
                   │                 ▼
          PriceFetchManager   PriceAlertManager
                   │                 │
                   │                 │
         OrderExecutionManager ◀─────┘
          │    │     │      │
          ▼    ▼     ▼      ▼              ┌─────────────┐
   Position Balance Fee     WebSocket ───▶ │ Frontend UI │
   Manager  Manager Manager Manager        └─────────────┘

Challenge, Account Value & MLL

  ChallengeManager

    ├─ startChallenge()
    │   ├─ setBalanceForChallenge(capital, minBalance)  ← enables MLL detection
    │   └─ AccountValueTrackerManager.subscribe()
    │                 │
    │                 ▼
    │      AccountValueAggregationTracker (external DO)
    │                 ▲               │
    │  updateCoefficients()           │ callbacks at time boundaries
    │  (on each position change)      │
    │                                 ├─ onHourlyExtremes(peak, bottom)
    │                                 └─ onDailyExtremesFinalized()
    │                                          │
    │                                          ▼
    │                                    MLL breach check
    │                                    bottom ≤ minBalance?
    │                                          │ yes
    │                                          ▼
    ├─ failChallenge() ◀───────── MllRiskManager ───▶ MllBreachRegistry (external DO)

    └─ AccountHealthManager (periodic safety net)

Subscription Lifecycle

  SubscriptionManager
    │  create / cancel / resume / renew

    ├─ PlanChangeManager
    │     upgrade (mid-cycle proration) / scheduled downgrade

    ├─ PaymentHistoryManager
    │     records all payment events

    ├─ ResetCreditsManager
    │     credits for challenge resets (active + pending)

    └─ SubscriptionReminderManager
          schedules expiry reminders (7d, 3d, 1d, expired, grace)
          via TaskScheduler → EmailNotificationManager

Manager Domains

Trading Core

ManagerResponsibility
OrderExecutionManagerCentral orchestrator for all trade execution — validates, executes position updates, applies PnL, registers alerts, broadcasts events
PositionManagerPosition netting (add/partial close/reversal), liquidation price calculation, entry price averaging
PendingOrderManagerPending market order state machine (CREATED → PENDING → EXECUTED/FAILED), executes on price arrival callback
LimitOrderManagerLimit order lifecycle with margin reservation at creation, PriceAlert registration, execute/cancel
BalanceManagerAccount balance, multi-account lifecycle, account failure detection (balance ≤ minBalance)
FeeManager7-tier volume-based fee calculation with 14-day rolling window, daily volume tracking
LeverageSettingsManagerPer-symbol leverage persistence, strictly-increasing enforcement, auto-adjustment on position open

Price & Alert Integration

ManagerResponsibility
PriceFetchManagerFetches prices from PriceAlert read replica with in-memory caching
AlertRegistrationManagerRegisters/deregisters alerts with PriceAlert DOs (0-9 instances, random load balancing) for all 4 alert types
PriceAlertManagerTranslates external onAlert()/onPriceArrival() callbacks into internal trade execution

Challenge & Risk

ManagerResponsibility
ChallengeManagerChallenge lifecycle (start/pass/fail), criteria evaluation (profit, consistency, trading days), enables failure detection via setBalanceForChallenge()
ChallengeAdminManagerAdmin operations: force pass, reset, fail with reason — all logged to audit trail
MllRiskManagerMLL breach event tracking, risk level calculation (safe/warning/critical)
AccountValueTrackerManagerSubscribes to AccountValueAggregationTracker for peak/bottom tracking, sends coefficient snapshots on position changes
AccountHealthManagerPeriodic health checks for MLL breach detection

Subscription & Billing

ManagerResponsibility
SubscriptionManagerSubscription lifecycle (create/cancel/resume/renew), status calculation at read-time
PlanChangeManagerMid-cycle upgrades with proration, scheduled downgrades
PaymentHistoryManagerPayment record storage and paginated retrieval
ResetCreditsManagerReset credit management (active + pending), challenge reset with credit deduction
SubscriptionReminderManagerSchedules expiry reminders (7d, 3d, 1d, expired, grace) via TaskScheduler with email notifications

User & Notifications

ManagerResponsibility
NotificationManagerIn-app notification CRUD with filtering and pagination
EmailNotificationManagerEmail address management, verification, notification queuing
WebSocketManagerReal-time event broadcasting via Hibernation API
SocialAccountManagerOAuth 2.0 social account linking (Twitter/X) with AES-GCM token encryption
ReferralManagerReferral code generation (ACE-{FIRST5}-{LAST3}), status tracking, rebate calculation
PromoCodeRedemptionManagerPromo/referral code validation via PromoCodeRegistry DO
PreferencesManagerUser display and trading preferences
AnalyticsManagerAnalytics queries: daily/hourly snapshots, trade statistics, volume history

Helpers

ManagerResponsibility
IdempotencyManager24-hour TTL deduplication keys with PROCESSING status for race condition prevention
InputValidationParameter validation for all trading operations (leverage, margin, size constraints)
StateSeedingManagerTest scenario seeding (dev/test only)

Key Data Flows

Market Order Execution

User: openPosition(symbol, direction, sizeUsd, leverage)

  ├─ IdempotencyManager.withIdempotency()
  ├─ InputValidation.validateOpenPositionParams()
  ├─ PriceFetchManager.fetchPriceForOrder(timestamp)

  ├─ IF price unavailable:
  │   ├─ PendingOrderManager.createPendingOrder()
  │   ├─ AlertRegistrationManager → PriceAlert.registerPriceArrivalAlert()
  │   └─ Return { status: "PENDING" }

  └─ IF price available:
      └─ OrderExecutionManager.executeOrder()
          ├─ PositionManager.executePositionUpdate()  → netting logic
          ├─ FeeManager.calculateFee()
          ├─ BalanceManager.applyPnL()
          │   └─ IF balance ≤ minBalance → ChallengeManager.failChallenge()
          ├─ AlertRegistrationManager.registerLiquidationWatch()
          ├─ AccountValueTrackerManager.updateCoefficients()
          ├─ WebSocketManager.broadcast(POSITION_OPENED)
          └─ Return { status: "EXECUTED", trade, position }

Limit Order Flow

User: openLimitOrder(symbol, direction, sizeUsd, leverage, targetPrice)

  ├─ Validate params, check available margin
  ├─ Reserve margin (sizeUsd / leverage)
  ├─ PriceAlert.registerLimitOrderWithPriceCheck()
  │   └─ Atomic: check current price + register condition alert

  ├─ IF price already crosses target → execute immediately
  ├─ IF price data not yet available → create PENDING_PRICE order, wait for arrival
  └─ IF price exists but target not met → create ACTIVE order with condition alert

      ... later, when price crosses target ...

      PriceAlert.onAlert() → PriceAlertManager
        ├─ LimitOrderManager.getLimitOrderByAlertId()
        ├─ OrderExecutionManager.executeOrder()
        ├─ Release reserved margin
        └─ WebSocketManager.broadcast(LIMIT_ORDER_TRIGGERED)

Challenge Lifecycle

User: startChallenge(plan)

  ├─ Close all positions, cancel all limit orders
  ├─ Deregister all alerts
  ├─ Reset balance to plan capital
  ├─ BalanceManager.setBalanceForChallenge(capital, minBalance)
  │   └─ Enables MLL failure detection
  ├─ AccountValueTrackerManager.subscribe()
  │   └─ AccountValueAggregationTracker starts peak/bottom tracking
  └─ Challenge status: ACTIVE

      ... during trading ...

      ├─ Each trade → BalanceManager.applyPnL() checks MLL
      ├─ Each position change → updateCoefficients() to tracker
      ├─ Hourly → onHourlyExtremes(peak, bottom)
      └─ Daily at UTC midnight → onDailyExtremesFinalized()
          └─ IF bottom ≤ minBalance → failChallenge()

      ... user requests pass ...

      passChallenge()
        ├─ Check: profit ≥ target, consistency ≤ 0.5, days ≥ target
        ├─ Register PriceArrivalAlert for latest prices
        └─ onPassChallengeValidation() → verify MLL with real prices
            └─ IF all criteria met → PASSED

Key Concepts

Deterministic Pricing

All orders use Math.floor(Date.now() / 1000) as their target timestamp. There is no "latest available price" — every order targets a specific second. This makes retries idempotent: the same timestamp always resolves to the same price.

Pending Order State Machine

When a price is unavailable at order time (common for recent timestamps still being collected), the order enters a state machine that survives DO eviction:

CREATED → PENDING → EXECUTED | FAILED

Margin is reserved at CREATED. PriceAlert fires onPriceArrival() when the timestamp's data becomes available, which triggers execution. This makes the system "eventually correct" — like a limit order that fills when data arrives.

Position Netting Algorithm

Three cases when a new trade interacts with an existing position:

ScenarioBehavior
Same directionWeighted average entry price, combined size
Opposite, partial closeRealize PnL on closed portion, keep original entry, reduce size
Opposite, full reversalRealize PnL on full old position, open new position at current price

Stale Alert Rejection

Alerts carry an accountId from creation time. When an alert fires after account reset, the current accountId won't match — the alert is rejected as stale. This prevents old alerts from a previous challenge attempt executing on a new account.

Margin Reservation

Limit orders reserve margin at creation (sizeUsd / leverage), blocking it from being used by other trades. This ensures the order can execute when its condition triggers, even if the user has opened other positions since. Reserved margin is released on execution or cancellation.

Margin Modes

Two margin modes: CROSS (default) and ISOLATED. Cannot be changed with open positions or limit orders.

  • CROSS: availableMargin = balance + unrealizedPnL - marginUsed - reservedMargin
  • ISOLATED: availableMargin = balance - marginUsed - reservedMargin (ignores unrealized PnL)

Leverage: Strictly Increasing

Integer-only, 1 to token-dependent max (see src/constants/symbols/leverage.ts). Leverage can only increase for an existing position (1x → 20x allowed, 20x → 10x rejected). When opening a new position or limit order at higher leverage, the system auto-adjusts the existing position's leverage first. Higher leverage releases margin and updates the liquidation price. Adding to a position must not cause immediate liquidation.

Challenge Pass Validation

Passing isn't instant — it triggers a price arrival alert to get the latest market prices, then validates all criteria with real account values. This prevents passing when entry-price-based calculations show a pass but real market prices would breach MLL.

Key Constants

ConstantValue
Starting balance$10,000
Minimum order size$10 USD (configurable)
Idempotency TTL24 hours
Fee tiers7 (volume-based)
Volume window14-day rolling
Challenge plansFREE, STARTER, STANDARD, PRO
WebSocket eventsPOSITION_OPENED/CLOSED, LIMIT_ORDER_EXECUTED/CANCELLED, CHALLENGE_PASSED/FAILED

Interactions

DODirectionPurpose
PriceAlertPrice queries (read replica), alert registration/deregistration, onAlert()/onPriceArrival() callbacks
AccountValueAggregationTracker→ ←subscribe(), updateCoefficients(), receives onHourlyExtremes()/onDailyExtremesFinalized()
PromoCodeRegistryPromo/referral code validation
UserRegistryUser registration by wallet/email
PublicTradesFeedPublishes closed trades
UserPaperTradeLeaderboardRanking updates on challenge events
MllBreachRegistryMLL breach event recording
TradeFundAccountRegistryFunded account applications