Skip to content

Order Execution Lifecycle

Orders enter the system through three paths: TradingView webhooks (automated alerts from Pine Script strategies), dashboard UI (manual orders placed by users), and background fill verification (Lambda-driven status polling). All three paths converge at the per-user trading worker — a dedicated ECS task that maintains broker connections and executes orders. The API layer never touches broker SDKs directly; every order flows through Redis queues to the user's isolated worker.


Three Execution Paths

flowchart LR
    TV["TradingView"] -->|"4-layer validation"| API
    UI["Dashboard"] -->|"JWT + CSRF"| API
    API["FastAPI"] -->|"ensure worker"| Queue["Redis Queue"]
    Queue --> Worker["Per-User Worker"]
    Worker --> Broker["Broker API"]
    Worker -->|"fill check"| SQS["SQS FIFO"]
    SQS --> Lambda["Lambda"] --> DB["PostgreSQL"]

Webhook Order Flow

The webhook path handles automated orders from TradingView alerts. This is the primary execution path for production trading.

sequenceDiagram
    participant TV as TradingView
    participant WAF as AWS WAF
    participant ALB as ALB
    participant API as FastAPI
    participant Redis as Valkey (Redis)
    participant SQS as SQS FIFO
    participant Lambda as Lambda
    participant Pool as Pool Worker
    participant Worker as User Worker
    participant Broker as Broker API

    TV->>WAF: 1. POST /webhook/tradingview<br/>{action, symbol, qty, secret}
    WAF->>ALB: 2. WAF rules pass (TV IPs exempted)
    ALB->>API: 3. TLS terminated, forwarded

    rect rgb(30, 58, 95)
        Note over API: 4-Layer Webhook Validation
        API->>API: Layer 1: Validate webhook token → resolve user
        API->>API: Layer 2: Validate timestamp (reject stale)
        API->>API: Layer 3: Validate secret (HMAC match)
        API->>API: Layer 4: Replay hash (reject duplicates)
    end

    API->>API: 5. Check subscription plan + daily order limit
    API->>API: 6. Verify credentials + ToS acceptance

    API->>Redis: 7. Check worker:active:{user_id}
    alt No active worker
        API->>SQS: 8a. Send to worker-control.fifo
        SQS->>Lambda: 8b. Lambda triggered
        Lambda->>Pool: 8c. Pool claim via SQS
        Pool->>Redis: 8d. Set worker mark (897ms median)
    end

    API->>Redis: 9. RPUSH to trading:user:{id}:requests
    Worker->>Redis: 10. BLPOP picks up order
    Worker->>Broker: 11. Execute via broker API
    Broker-->>Worker: 12. Order confirmation
    Worker->>Redis: 13. RPUSH to trading:response:{req_id}
    API->>API: 14. BLPOP response (30s timeout)
    API-->>TV: 15. HTTP 200 with OrderResult

    Note over API,SQS: Background: dispatch fill verification
    API->>SQS: 16. Queue to order-tasks.fifo
    SQS->>Lambda: 17. Lambda polls broker for fill
    Lambda->>Redis: 18. Update order state in DB

Step-by-Step Breakdown

Step Component Action Failure Mode
1 TradingView Fires alert with action, symbol, quantity, timestamp, secret Alert misconfigured → no request sent
2 WAF Rate limit, IP filter, managed rules (TradingView IPs exempted) Blocked → 403
3 ALB TLS termination, health check routing Unhealthy target → 502
4 FastAPI 4-layer validation: token → timestamp → secret → replay Any failure → 401/403
5 FastAPI Subscription check, daily order limit Over limit → 429
6 FastAPI Credential verification, ToS acceptance Not verified → 403
7 Redis Check worker:active:{user_id} Redis down → 503
8 SQS → Lambda Start worker via pool claim (897ms) or RunTask fallback (3.6s) All fail → 503 (worker unavailable)
9 Redis Push order to per-user queue Redis down → 503
10 Worker Pick up order via BLPOP Worker crashed → request times out
11 Broker Execute order on broker API Broker rejects → error in OrderResult
12–13 Worker → Redis Write response Response lost → API times out → 202
14 FastAPI BLPOP on response key (30s timeout) Timeout → 202 Accepted (order may still fill)
15 FastAPI → TV Return result TradingView has 10s limit
16–18 SQS → Lambda Background fill verification Retry via SQS visibility timeout

Dashboard Order Flow

The dashboard path handles manual orders placed through the trading UI. Authentication uses JWT (HttpOnly cookies) + CSRF tokens instead of webhook validation.

flowchart TB
    A["Dashboard UI<br/>POST /trading/order"] --> B["FastAPI"]
    B --> C{"JWT + CSRF<br/>Valid?"}
    C -->|No| D["401 / 403"]
    C -->|Yes| E["Resolve user + account"]
    E --> F["Ensure worker running"]
    F --> G["Redis queue"]
    G --> H["Worker BLPOP"]
    H --> I["Broker API"]
    I --> J["Response via Redis"]
    J --> K["HTTP 200<br/>OrderResult"]
Difference from Webhook Dashboard Webhook
Authentication JWT cookie + CSRF token Webhook token + secret
Validation layers 2 (JWT + CSRF) 4 (token + timestamp + secret + replay)
Order source source="dashboard" source="webhook"
Symbol validation User selects from loaded symbols Must be in allowed list
Rate limiting Standard API rate limit Per-webhook daily order limit

Fill Verification

After every order placement, a background Lambda verifies the fill status by polling the broker API. This handles the gap between order submission and fill confirmation — especially important for brokers with asynchronous fill reporting.

flowchart TB
    A["Order placed by Worker"] --> B["API dispatches to<br/>SQS FIFO: order-tasks"]
    B --> C["Lambda: order_tasks<br/>(triggered by SQS)"]
    C --> D["Load order from DB"]
    D --> E["Route to user's worker<br/>via Redis queue"]
    E --> F["Worker calls<br/>broker.check_order_status()"]
    F --> G{Fill Status?}

    G -->|"Filled"| H["Update order_history<br/>fill_price, fill_qty, status='filled'"]
    G -->|"Partially filled"| I["Update partial fill<br/>Re-queue with visibility timeout"]
    G -->|"Cancelled / Failed"| J["Update status<br/>Log reason"]
    G -->|"Pending"| K["Re-queue<br/>visibility timeout = 30s"]

    I --> C
    K --> C

Verification Details

Parameter Value
Queue SQS FIFO (order-tasks.fifo)
Trigger Lambda (order_tasks)
Initial delay 2–3 seconds after order placement
Retry interval 30s visibility timeout
Max retries 3 (then moves to DLQ)
Partial fill handling Re-queues with updated filled quantity

Why SQS FIFO?

FIFO ordering ensures fill checks for the same user happen sequentially (MessageGroupId = user_id). This prevents race conditions where two concurrent verifications could write conflicting fill data. The 180-second visibility timeout gives the Lambda enough time to poll the broker and update the database.


Auto-Reversal

When a long_entry signal arrives but the user holds a short position, the system automatically closes the short before opening the long. This mirrors TradingView's strategy.entry() behavior where entering a position in the opposite direction reverses the existing one.

flowchart TB
    A["Signal: long_entry<br/>User has short position"] --> B{"Has opposite<br/>position?"}
    B -->|No| F["place_entry_order"]
    B -->|Yes| C["close_position<br/>buy to cover"]
    C --> D["Poll fill status"]
    D --> E{"Filled?"}
    E -->|Yes| F
    E -->|Timeout| G["Abort<br/>Manual intervention"]
    F --> H["OrderResult<br/>auto_exit=true"]

Reversal Matrix

Current Position Signal Step 1 Step 2 Final Position
None long_entry (buy 1) Buy 1 +1 (long)
+1 (long) long_entry (buy 1) Buy 1 +2 (long)
-2 (short) long_entry (buy 1) Close short (buy 2) Buy 1 +1 (long)
None short_entry (sell 1) Sell 1 -1 (short)
+3 (long) short_entry (sell 1) Close long (sell 3) Sell 1 -1 (short)
-1 (short) short_entry (sell 1) Sell 1 -2 (short)

Supported Actions

Action Description Queue Operation Broker Method
long_entry Open or add to a long position PLACE_ENTRY_ORDER (action=buy) place_entry_order_with_reversal()
long_exit Close an existing long position PLACE_EXIT_ORDER (direction=long) place_exit_order()
short_entry Open or add to a short position PLACE_ENTRY_ORDER (action=sell) place_entry_order_with_reversal()
short_exit Close an existing short position PLACE_EXIT_ORDER (direction=short) place_exit_order()

Webhook Action Mapping

TradingView alerts use a simplified action format that maps to internal operations:

Alert action order_type Internal Operation
buy entry (default) PLACE_ENTRY_ORDER (action=buy)
sell entry (default) PLACE_ENTRY_ORDER (action=sell)
buy exit PLACE_EXIT_ORDER (direction=short)
sell exit PLACE_EXIT_ORDER (direction=long)
close_long CLOSE_POSITION
close_short CLOSE_POSITION

Error Handling

Broker Rejection

Rejection Reason System Response
Insufficient margin Return error to caller, log to order_history
Market closed Return error with market hours information
Invalid symbol Return error, symbol not found in broker contracts
Rate limited by broker Retry after broker-specified delay
Position mode mismatch (crypto) Return error: "Switch to one-way mode"

Worker Unavailable

flowchart TB
    A["Order request arrives"] --> B{"Worker running?<br/>Check Redis mark"}
    B -->|Yes| C["Route to worker queue"]
    B -->|No| D["Start worker<br/>(SQS → Lambda → Pool)"]
    D --> E{Started within 60s?}
    E -->|Yes| C
    E -->|No| F["503: Worker unavailable<br/>Retry after cold start"]
Failure Scenario Behavior
Worker not running Automatic start via pool claim (897ms) or RunTask (3.6s)
Worker start timeout (60s) 503 returned, user retries
Worker crashed mid-request Request times out (30s), 202 returned
Worker disconnected from broker Auto-reconnect on next request (up to 3 retries)
Redis unavailable 503 returned, all order paths blocked

Timeout Behavior

Timeout Duration Outcome
BLPOP response timeout 30s API returns 202 Accepted (order may still fill)
Worker idle timeout 12h (configurable) Worker shuts down, next request starts a new one
SQS visibility timeout (order-tasks) 180s Lambda has 3 minutes to verify fill
Reversal fill poll timeout 5s (10 × 0.5s) Abort reversal, return error

The 202 Accepted case

When the API times out waiting for the worker response, it returns 202 Accepted — meaning the order was submitted but the result is unknown. The background fill verification Lambda will eventually determine the outcome and update the order_history record. The frontend should poll /trading/orders/{id} to get the final status.


Order History

Every order is recorded in the order_history table:

Field Group Fields
Request symbol, action, quantity, order_type, source (webhook/dashboard)
Result order_id, broker_refs, status, submitted_at
Fill fill_price, fill_quantity, filled_at (updated by verification)
Reversal auto_exit, exit_quantity, position_before, exit_order_result
Metadata user_id, trading_account_id, broker_name, simulation

File Map

Concept File
Webhook endpoint + validation app/routers/webhook.py
Dashboard trading endpoints app/routers/trading.py
Webhook request schema app/schemas/webhook.py
Queue client (API → Worker) app/services/trading_queue.py
Trading worker (broker execution) app/services/trading_worker.py
Order verification service app/services/order_verification.py
Lambda: order_tasks (fill verification) lambda/order_tasks.py
Lambda: worker_control (lifecycle) lambda/worker_control.py
Order history model app/models/models.py (OrderHistory)
Trading operations enum app/enums.py (TradingOperation)
SQS queue definitions terraform/sqs.tf