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 |