Raxx · internal docs

internal · gated ↑ index

Reasonator Sequence Diagrams

Date: 2026-05-09 UTC Refs: docs/architecture/reasonator/design.md, docs/architecture/reasonator/api-contract.md, #1385

All times UTC.


Diagram 1: Pro Tier — Background Sentiment for User's Positions

A Pro user views their AAPL position history. Raptor's background scheduler fills in the sentiment window for that position asynchronously.

sequenceDiagram
    autonumber
    participant User as User (Pro)
    participant Antlers as Antlers (frontend)
    participant Raptor as Raptor (backend)
    participant Sched as Raptor Scheduler
    participant Reasonator as Reasonator service
    participant DB as sentiment_events (Raptor DB)

    User->>Antlers: Open positions page
    Antlers->>Raptor: GET /api/positions
    Raptor-->>Antlers: positions[] (with sentiment_status: "pending" or "available")
    Antlers-->>User: Show positions; sentiment badge shows "Loading..."

    Note over Sched: Scheduler runs every N minutes for unscored events
    Sched->>DB: SELECT id, headline FROM sentiment_events<br/>WHERE scorer_model_version IS NULL AND symbol IN (user_symbols)
    DB-->>Sched: [unscored rows]

    Sched->>Reasonator: POST /v1/score/batch<br/>X-Raxx-Tier: pro<br/>{ job_id, model_sha, headlines: [...] }
    Reasonator-->>Sched: 202 Accepted { job_id, status: "queued" }

    loop Poll every 30s
        Sched->>Reasonator: GET /v1/score/batch/{job_id}
        Reasonator-->>Sched: { status: "processing", progress: 0.42 }
    end

    Reasonator-->>Sched: { status: "complete", results: [...] }

    Sched->>DB: UPDATE sentiment_events SET<br/>sentiment_label, sentiment_score,<br/>sentiment_confidence, scorer_model_version<br/>WHERE id IN (scored_ids)
    Sched->>DB: INSERT INTO sentiment_score_audit<br/>(event_id, scored_at, model_sha, label, score, source: "rack_batch")

    Note over User,Antlers: User refreshes or next poll cycle
    Antlers->>Raptor: GET /api/sentiment/AAPL?start=2026-04-14T10:00:00Z&end=2026-04-18T16:00:00Z
    Raptor->>DB: Aggregate scored events in window
    DB-->>Raptor: { score: 0.08, label: "neutral", count: 12, events: [...] }
    Raptor-->>Antlers: Sentiment summary + top headlines
    Antlers-->>User: Show sentiment badge "Neutral (+0.08)<br/>12 articles during your hold"

Key invariants preserved: - I-1: Score describes the window — not a forecast. - I-2: Reasonator is read-only. No order state touched. - I-3: If Reasonator is down, Sched retries later; positions page still loads (no sentiment badge). - I-6: All timestamps UTC.


Diagram 2: Pro+ Tier — Real-Time Reasonator Call on Trade Ticket Open

A Pro+ user opens a trade ticket for AAPL. Raptor synchronously fetches the current sentiment snapshot from Reasonator, which the user can review before deciding.

sequenceDiagram
    autonumber
    participant User as User (Pro+)
    participant Antlers as Antlers (frontend)
    participant Raptor as Raptor (backend)
    participant Reasonator as Reasonator service
    participant DB as sentiment_events (Raptor DB)
    participant CB as Circuit Breaker (Raptor-side)

    User->>Antlers: Open trade ticket for AAPL
    Antlers->>Raptor: GET /api/trade-context/AAPL

    Note over Raptor: Check circuit breaker state
    Raptor->>CB: Is Reasonator circuit open?
    CB-->>Raptor: closed (Reasonator is healthy)

    Raptor->>DB: SELECT headlines from sentiment_events<br/>WHERE symbol='AAPL' AND published_at > NOW()-4h<br/>AND scorer_model_version IS NOT NULL

    alt Scored events exist in DB (cache hit)
        DB-->>Raptor: [pre-scored rows]
        Note over Raptor: Skip Reasonator call — use cached scores
        Raptor->>Raptor: Aggregate cached scores
    else Unscored events or cache miss
        DB-->>Raptor: [unscored or stale rows]
        Raptor->>Reasonator: POST /v1/score<br/>X-Raxx-Tier: pro_plus<br/>{ model_sha, headlines: [last N hours] }

        alt Reasonator responds within 2s (p99 target)
            Reasonator-->>Raptor: 200 { results: [...] }
            Raptor->>DB: Write scores to sentiment_events + audit log
        else Reasonator timeout or 5xx
            Reasonator-->>Raptor: timeout / 503
            Raptor->>CB: Record failure
            CB-->>Raptor: Circuit still closed (< 5 failures)
            Raptor->>Raptor: Serve stale cache or null sentiment
        end
    end

    Raptor-->>Antlers: { trade_context: { symbol, price, ... },<br/>sentiment: { score: 0.42, label: "positive",<br/>count: 8, events: [...], stale: false } }
    Antlers-->>User: Trade ticket with sentiment panel:<br/>"AAPL sentiment: Positive (+0.42) — 8 articles in last 4h<br/>Here are the headlines [...]"

    Note over User: User reviews facts. User decides.<br/>Raxx never proposes the trade.

    opt User configures Pro+ alert rule
        User->>Antlers: "Alert me when AAPL sentiment flips negative<br/>on positions > $10K"
        Antlers->>Raptor: POST /api/alert-rules<br/>{ symbol: "AAPL", condition: "sentiment_label=negative",<br/>position_threshold: 10000 }
        Raptor-->>Antlers: Rule saved

        Note over Raptor: Next time Reasonator returns negative for AAPL...
        Raptor->>Raptor: Evaluate rule against new score
        Raptor->>User: Email: "AAPL sentiment is currently negative.<br/>You have $X in open AAPL positions.<br/>[No recommendation. No trade proposal.]"
    end

Key invariants preserved: - I-1: Score is current historical state of sentiment — not a prediction. - I-2: Raptor evaluates alert rules; no order fires. User decides. - I-3: Circuit breaker shields trade ticket from Reasonator failures. Ticket still opens. - I-7: No broker names in the sentiment panel.


Diagram 3: Re-Scoring Sweep — Model SHA Bump

The operator updates the pinned FinBERT model SHA. A sweep job re-scores all historical sentiment_events rows and writes the updated scores alongside the old ones for provenance.

sequenceDiagram
    autonumber
    participant Operator as Operator
    participant Vault as Infisical Vault
    participant Reasonator as Reasonator service
    participant Sched as Raptor Re-score Sweep Job
    participant DB as Raptor DB<br/>(sentiment_events + audit)

    Operator->>Vault: Update FINBERT_MODEL_SHA=def789abc012<br/>at /reasonator/prod/FINBERT_MODEL_SHA
    Operator->>Reasonator: SIGHUP (or redeploy)<br/>→ Reasonator reloads model with new SHA

    Reasonator->>Reasonator: Download + load ProsusAI/finbert@def789abc012<br/>(~30–120s model load time)
    Reasonator-->>Operator: GET /v1/health → { model_sha: "def789abc012", model_loaded: true }

    Operator->>Sched: Trigger rescore sweep<br/>(manual or scheduled job)

    loop Pages of 500 rows
        Sched->>DB: SELECT id, headline, symbol, published_at,<br/>sentiment_score AS prev_score, scorer_model_version AS prev_sha<br/>FROM sentiment_events<br/>WHERE scorer_model_version != 'def789abc012'<br/>ORDER BY published_at DESC<br/>LIMIT 500 OFFSET :page
        DB-->>Sched: [500 rows with prev scores]

        Sched->>Reasonator: POST /v1/score/rescore<br/>X-Raxx-Tier: pro<br/>{ new_model_sha: "def789abc012",<br/>  headlines: [{ id, text, symbol, published_at,<br/>               previous_score, previous_model_sha }] }
        Reasonator-->>Sched: 200 { results: [{ id, label, score, confidence,<br/>                             previous_score, score_delta }] }

        Sched->>DB: UPDATE sentiment_events<br/>SET sentiment_label=new, sentiment_score=new,<br/>sentiment_confidence=new,<br/>scorer_model_version='def789abc012'<br/>WHERE id IN (batch_ids)

        Sched->>DB: INSERT INTO sentiment_score_audit<br/>({ event_id, scored_at, scorer_model_version: "def789abc012",<br/>   label, score, confidence, score_source: "rack_rescore",<br/>   previous_score, previous_model_sha: "abc123def456" })

        Note over Sched: Wait 100ms between pages<br/>to avoid starving Pro+ sync requests
    end

    Sched-->>Operator: Sweep complete.<br/>N rows rescored. M rows unchanged.<br/>Score drift summary: mean |delta| = 0.03

Key invariants preserved: - I-5: Every score write (including re-scores) is audited in sentiment_score_audit with old and new SHA. - I-4: Model SHA is env-driven (Vault); no SHA hardcoded in code. - I-3: Re-score sweep uses X-Raxx-Tier: pro so it does not starve Pro+ sync requests.


Diagram 4: Reasonator Down — Circuit Breaker Path

Reasonator becomes unavailable (dyno restart, deploy, crash). Raptor's circuit breaker opens and all in-flight requests return gracefully without blocking users.

sequenceDiagram
    autonumber
    participant User as User (Pro+)
    participant Antlers as Antlers
    participant Raptor as Raptor
    participant Reasonator as Reasonator service (DOWN)
    participant CB as Circuit Breaker
    participant Cache as Response Cache (Raptor)

    User->>Antlers: Open trade ticket for AAPL
    Antlers->>Raptor: GET /api/trade-context/AAPL

    Raptor->>CB: Is Reasonator circuit open?
    CB-->>Raptor: closed

    Raptor->>Cache: Any cached sentiment for AAPL in last 15m?
    Cache-->>Raptor: cache miss

    Raptor->>Reasonator: POST /v1/score (timeout: 3s)
    Reasonator-->>Raptor: [no response — dyno down]
    Raptor->>CB: Record failure #1

    Note over CB: Threshold: 5 failures in 60s

    Note over Raptor: Retries 1-4 (other requests in flight)<br/>Each timeout → CB records failure

    Raptor->>CB: Record failure #5
    CB->>CB: Circuit opens. State: OPEN.<br/>Reset timer: 120s

    Note over CB,Raptor: All subsequent Reasonator calls<br/>short-circuit immediately

    Raptor-->>Antlers: { trade_context: { ... },<br/>sentiment: null,<br/>sentiment_reason: "rack_unavailable" }
    Antlers-->>User: Trade ticket loads normally.<br/>Sentiment panel: "Sentiment data temporarily unavailable"

    Note over User: Trade ticket still fully functional.<br/>User can trade normally.<br/>Reasonator being down NEVER blocks execution.

    Note over CB: After 120s, circuit enters HALF-OPEN

    CB->>Reasonator: Probe: GET /v1/health
    Reasonator-->>CB: 200 { status: "ok", model_loaded: true }
    CB->>CB: Circuit closes. State: CLOSED.
    Note over Raptor: Normal Reasonator calls resume

Key invariants preserved: - I-3: Trade ticket opens. Order entry is available. Reasonator is always optional. - I-2: The execution path is completely independent of sentiment availability.