Skip to content

AlertEvaluationManager

Condition-checking engine for PRICE_CONDITION alerts. Evaluates registered alerts against prices each tick, triggers callbacks to UserPaperTradePortfolio, and self-heals zombie alerts.

High-Level Design

AlertEvaluationManager is the decision engine that determines which alerts have triggered. It is stateless — it holds no in-memory state and delegates all storage to AlertStorageManager. It only handles PRICE_CONDITION alerts (LIMIT_ORDER, STOP_LOSS, TAKE_PROFIT, LIQUIDATION_WATCH); PRICE_ARRIVAL alerts follow a completely separate path in the PriceAlert alarm loop.

Data Flow

     PriceAlert alarm loop (1s ticks)

 for each timestamp in cursor range:

 ┌───────────────┴─────────────────┐
 │  if hasConditionAlerts:         │
 │                                 │
 │  1. evaluateAlertsSync(prices)  │
 │     ├─ storageManager           │
 │     │  .getAlertsBySymbol()     │
 │     ├─ match price per symbol   │
 │     └─ return triggered[]       │
 │                                 │
 │  2. processTriggeredAlerts()    │
 │     (sequential, one-by-one)    │
 │     └─ triggerAlert() per alert │
 └───────────────┬─────────────────┘

          triggerAlert()


   UserPaperTradePortfolio
     .onAlert(alert, ts, prices)

       ┌─────────┼───────────┐
       ▼         ▼           ▼
    Success   POSITION_   Other
    + oneTime NOT_FOUND   error
       │      STALE_ALERT    │
       ▼         │        keep alert
   deregister    ▼        (retry next tick)
    alert     deregister
              (self-heal)

Note: The alarm loop calls evaluateAlertsSync() and processTriggeredAlerts() as two separate steps — the manager does not orchestrate both internally. The loop also gates evaluation behind hasConditionAlerts to skip work when no condition alerts are registered.

Direction-Aware Trigger Logic

Each alert type uses direction (LONG/SHORT) to determine when to fire:

Alert TypeLONG triggers whenSHORT triggers when
LIMIT_ORDERprice <= targetprice >= target
STOP_LOSSprice <= targetprice >= target
LIQUIDATION_WATCHprice <= targetprice >= target
TAKE_PROFITprice >= targetprice <= target

TAKE_PROFIT has inverted logic — a LONG take-profit fires when price rises above target, while the other three fire when price falls below. Unknown alert types log a warning and return false.

Sequential Alert Processing

Triggered alerts are processed one-by-one, not in parallel. This prevents overwhelming downstream UserPaperTradePortfolio DOs when multiple alerts fire on the same tick (e.g., a sharp price move triggering several stop-losses simultaneously).

Self-Healing Zombie Alerts

When a user closes a position, the associated alerts may still be registered. Rather than requiring upstream cleanup, this manager detects orphaned alerts reactively:

  • POSITION_NOT_FOUND — position was closed, alert is stale
  • STALE_ALERT — alert references outdated state

Both trigger permanent deregistration via storageManager.deregisterAlert(), preventing zombie alerts from accumulating and wasting evaluation cycles.

One-Time vs Persistent Alerts

Alerts with oneTime === true (e.g., limit orders) are deregistered after a successful trigger. Non-one-time alerts (e.g., liquidation watches) persist and continue evaluating on subsequent ticks until explicitly deregistered.

Transient Failure Resilience

Non-specific RPC errors (network issues, DO eviction) do not deregister the alert. It stays registered and will be re-evaluated on the next 1-second tick. RPC exceptions are caught and logged but never crash the alarm loop.

State & Storage

In-Memory State

None. This manager is stateless.

SQLite Tables

None directly. Delegates to AlertStorageManager for alert CRUD.

Interactions

Depends On

  • AlertStorageManager (durable.storageManager): reads getAlertsBySymbol() every tick; calls deregisterAlert() for triggered/zombie alerts.
  • UserPaperTradePortfolio DO (via getUserPaperTradePortfolioDO()): calls onAlert() RPC.

Depended By

  • PriceAlert alarm loop: calls evaluateAlertsSync() and processTriggeredAlerts() each tick when condition alerts exist.

Edge Cases

  • targetPrice is stored as a string for precision; parsed to float at evaluation time via parseFloat().
  • Symbols with no price data in the current tick are silently skipped (no alert can trigger without a price).
  • Self-healing prevents zombie alerts from accumulating after positions are closed or become stale.
  • RPC failures are caught and logged but do not crash the alarm loop; the alert remains registered for retry on the next tick.

See Also