跳轉到

訂單執行生命週期

訂單透過三個路徑進入系統:TradingView webhook(來自 Pine Script 策略的自動化警報)、儀表板 UI(使用者手動下單),以及背景成交驗證(Lambda 驅動的狀態輪詢)。三條路徑匯聚於每位使用者的交易 Worker——一個維護券商連線並執行訂單的專用 ECS 任務。API 層從不直接接觸券商 SDK;每筆訂單都透過 Redis 佇列流向使用者的隔離 Worker。

端到端時序

Webhook 到券商:Worker Pool 預熱時通常 < 2 秒(897ms Pool Claim + 券商 API 呼叫)。成交確認:訂單下達後 30-60 秒非同步完成,透過 SQS FIFO 和 Lambda。自動反轉(平倉反向部位後開新倉):平倉腿成交確認增加 1-5 秒,5 秒中止逾時防止雙重曝險。


三條執行路徑

flowchart LR TV["TradingView"] -->|"4 層驗證"| API UI["儀表板"] -->|"JWT + CSRF"| API API["FastAPI"] -->|"確保 Worker 運行"| Queue["Redis 佇列"] Queue --> Worker["每用戶 Worker"] Worker --> Broker["券商 API"] Worker -->|"成交檢查"| SQS["SQS FIFO"] SQS --> Lambda["Lambda"] --> DB["PostgreSQL"]

Webhook 訂單流程

Webhook 路徑處理來自 TradingView 警報的自動化訂單。這是正式交易的主要執行路徑。

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 池 Worker participant Worker as 使用者 Worker participant Broker as 券商 API TV->>WAF: 1. POST /webhook/tradingview<br/>{action, symbol, qty, secret} WAF->>ALB: 2. WAF 規則通過(TradingView IP 已豁免) ALB->>API: 3. TLS 終止,轉發 rect rgb(30, 58, 95) Note over API: 4 層 Webhook 驗證 API->>API: 第 1 層:驗證 webhook 權杖 → 解析使用者 API->>API: 第 2 層:驗證時間戳(拒絕過期) API->>API: 第 3 層:驗證密鑰(HMAC 比對) API->>API: 第 4 層:重放雜湊(拒絕重複) end API->>API: 5. 檢查訂閱方案 + 每日下單限制 API->>API: 6. 驗證憑證 + 服務條款接受 API->>Redis: 7. 檢查 worker:active:{user_id} alt 無活躍 Worker API->>SQS: 8a. 發送至 worker-control.fifo SQS->>Lambda: 8b. Lambda 觸發 Lambda->>Pool: 8c. 池分配(透過 SQS) Pool->>Redis: 8d. 設定 Worker 標記(中位數 897ms) end API->>Redis: 9. RPUSH 至 trading:user:{id}:requests Worker->>Redis: 10. BLPOP 提取訂單 Worker->>Broker: 11. 透過券商 API 執行 Broker-->>Worker: 12. 訂單確認 Worker->>Redis: 13. RPUSH 至 trading:response:{req_id} API->>API: 14. BLPOP 回應(30 秒逾時) API-->>TV: 15. HTTP 200 附帶 OrderResult Note over API,SQS: 背景:派發成交驗證 API->>SQS: 16. 排入 order-tasks.fifo SQS->>Lambda: 17. Lambda 輪詢券商成交狀態 Lambda->>Redis: 18. 更新資料庫中的訂單狀態

逐步分解

步驟 元件 動作 失敗模式
1 TradingView 發送包含 action、symbol、quantity、timestamp、secret 的警報 警報設定錯誤 → 未發送請求
2 WAF 速率限制、IP 過濾、受管規則(TradingView IP 已豁免) 被阻擋 → 403
3 ALB TLS 終止、健康檢查路由 不健康的目標 → 502
4 FastAPI 四層驗證:權杖 → 時間戳 → 密鑰 → 重放 任一失敗 → 401/403
5 FastAPI 訂閱方案檢查、每日下單限制 超過限制 → 429
6 FastAPI 憑證驗證、服務條款接受 未驗證 → 403
7 Redis 檢查 worker:active:{user_id} Redis 不可用 → 503
8 SQS → Lambda 透過池分配啟動 Worker(897ms)或 RunTask 退回(3.6s) 全部失敗 → 503(Worker 不可用)
9 Redis 將訂單推入每位使用者的佇列 Redis 不可用 → 503
10 Worker 透過 BLPOP 提取訂單 Worker 崩潰 → 請求逾時
11 券商 在券商 API 上執行訂單 券商拒絕 → OrderResult 中的錯誤
12-13 Worker → Redis 寫入回應 回應遺失 → API 逾時 → 202
14 FastAPI 對回應鍵執行 BLPOP(30 秒逾時) 逾時 → 202 Accepted(訂單可能仍會成交)
15 FastAPI → TV 回傳結果 TradingView 有 10 秒限制
16-18 SQS → Lambda 背景成交驗證 透過 SQS 可見性逾時重試

儀表板訂單流程

儀表板路徑處理透過交易 UI 手動下的訂單。驗證使用 JWT(HttpOnly cookies)+ CSRF 權杖,而非 webhook 驗證。

flowchart TB A["儀表板 UI<br/>POST /trading/order"] --> B["FastAPI"] B --> C{"JWT + CSRF<br/>有效?"} C -->|"否"| D["401 / 403"] C -->|"是"| E["解析使用者 + 帳戶"] E --> F["確保 Worker 運行"] F --> G["Redis 佇列"] G --> H["Worker BLPOP"] H --> I["券商 API"] I --> J["透過 Redis 回應"] J --> K["HTTP 200<br/>OrderResult"]
與 Webhook 的差異 儀表板 Webhook
驗證方式 JWT cookie + CSRF 權杖 Webhook 權杖 + 密鑰
驗證層數 2(JWT + CSRF) 4(權杖 + 時間戳 + 密鑰 + 重放)
訂單來源 source="dashboard" source="webhook"
標的驗證 使用者從已載入標的中選擇 必須在允許清單中
速率限制 標準 API 速率限制 每個 webhook 的每日下單限制

成交驗證

每次下單後,背景 Lambda 會透過輪詢券商 API 來驗證成交狀態。這處理了訂單提交與成交確認之間的間隔——對於非同步回報成交的券商尤其重要。

flowchart TB A["Worker 已下單"] --> B["API 派發至<br/>SQS FIFO: order-tasks"] B --> C["Lambda: order_tasks<br/>(由 SQS 觸發)"] C --> D["從資料庫載入訂單"] D --> E["路由至使用者的 Worker<br/>(透過 Redis 佇列)"] E --> F["Worker 呼叫<br/>broker.check_order_status()"] F --> G{"成交狀態?"} G -->|"已成交"| H["更新 order_history<br/>fill_price, fill_qty, status='filled'"] G -->|"部分成交"| I["更新部分成交<br/>以可見性逾時重新入列"] G -->|"已取消 / 失敗"| J["更新狀態<br/>記錄原因"] G -->|"待處理"| K["重新入列<br/>可見性逾時 = 30s"] I --> C K --> C

驗證細節

參數
佇列 SQS FIFO(order-tasks.fifo
觸發器 Lambda(order_tasks
初始延遲 下單後 2-3 秒
重試間隔 30 秒可見性逾時
最大重試次數 3(之後移至 DLQ)
部分成交處理 以更新的已成交數量重新入列

為什麼使用 SQS FIFO?

FIFO 排序確保同一使用者的成交檢查按順序執行(MessageGroupId = user_id)。這防止兩個並行驗證寫入衝突的成交資料的競態條件。180 秒的可見性逾時給予 Lambda 足夠的時間來輪詢券商並更新資料庫。


自動反轉

long_entry 訊號到達但使用者持有空倉時,系統會自動在開多倉之前平掉空倉。這模仿了 TradingView 的 strategy.entry() 行為——進入反向倉位時自動反轉現有倉位。

flowchart TB A["訊號:long_entry<br/>使用者持有空倉"] --> B{"持有反向<br/>倉位?"} B -->|"否"| F["place_entry_order"] B -->|"是"| C["close_position<br/>買入回補"] C --> D["輪詢成交狀態"] D --> E{"已成交?"} E -->|"是"| F E -->|"逾時"| G["中止<br/>需人工介入"] F --> H["OrderResult<br/>auto_exit=true"]

反轉矩陣

目前倉位 訊號 步驟 1 步驟 2 最終倉位
long_entry(買 1) 買 1 +1(多倉)
+1(多倉) long_entry(買 1) 買 1 +2(多倉)
-2(空倉) long_entry(買 1) 平空倉(買 2) 買 1 +1(多倉)
short_entry(賣 1) 賣 1 -1(空倉)
+3(多倉) short_entry(賣 1) 平多倉(賣 3) 賣 1 -1(空倉)
-1(空倉) short_entry(賣 1) 賣 1 -2(空倉)

支援的動作

動作 描述 佇列操作 券商方法
long_entry 開立或加碼多倉 PLACE_ENTRY_ORDER(action=buy) place_entry_order_with_reversal()
long_exit 平掉現有多倉 PLACE_EXIT_ORDER(direction=long) place_exit_order()
short_entry 開立或加碼空倉 PLACE_ENTRY_ORDER(action=sell) place_entry_order_with_reversal()
short_exit 平掉現有空倉 PLACE_EXIT_ORDER(direction=short) place_exit_order()

Webhook 動作對應

TradingView 警報使用簡化的動作格式,對應至內部操作:

警報 action order_type 內部操作
buy entry(預設) PLACE_ENTRY_ORDER(action=buy)
sell entry(預設) 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

錯誤處理

券商拒絕

拒絕原因 系統回應
保證金不足 回傳錯誤給呼叫者,記錄至 order_history
市場關閉 回傳錯誤並附帶市場交易時間資訊
無效標的 回傳錯誤,在券商合約中找不到該標的
被券商限速 在券商指定的延遲後重試
持倉模式不符(加密) 回傳錯誤:「請切換至單向模式」

Worker 不可用

flowchart TB A["訂單請求到達"] --> B{"Worker 運行中?<br/>檢查 Redis 標記"} B -->|"是"| C["路由至 Worker 佇列"] B -->|"否"| D["啟動 Worker<br/>(SQS → Lambda → 池)"] D --> E{"60 秒內啟動?"} E -->|"是"| C E -->|"否"| F["503:Worker 不可用<br/>冷啟動後重試"]
失敗場景 行為
Worker 未執行 透過池分配自動啟動(897ms)或 RunTask(3.6s)
Worker 啟動逾時(60s) 回傳 503,使用者重試
Worker 在請求中途崩潰 請求逾時(30s),回傳 202
Worker 與券商斷線 下次請求時自動重新連線(最多 3 次重試)
Redis 不可用 回傳 503,所有下單路徑阻斷

逾時行為

逾時 時長 結果
BLPOP 回應逾時 30 秒 API 回傳 202 Accepted(訂單可能仍會成交)
Worker 閒置逾時 12 小時(可配置) Worker 關閉,下次請求啟動新的
SQS 可見性逾時(order-tasks) 180 秒 Lambda 有 3 分鐘驗證成交
反轉成交輪詢逾時 5 秒(10 x 0.5s) 中止反轉,回傳錯誤

202 Accepted 的情況

當 API 等待 Worker 回應逾時時,回傳 202 Accepted——表示訂單已提交但結果未知。背景成交驗證 Lambda 最終會確定結果並更新 order_history 記錄。前端應輪詢 /trading/orders/{id} 以取得最終狀態。


訂單歷史

每筆訂單都記錄在 order_history 表中:

欄位群組 欄位
請求 symbol、action、quantity、order_type、source(webhook/dashboard)
結果 order_id、broker_refs、status、submitted_at
成交 fill_price、fill_quantity、filled_at(由驗證更新)
反轉 auto_exit、exit_quantity、position_before、exit_order_result
中繼資料 user_id、trading_account_id、broker_name、simulation

檔案對應表

概念 檔案
Webhook 端點 + 驗證 app/routers/webhook.py
儀表板交易端點 app/routers/trading.py
Webhook 請求綱要 app/schemas/webhook.py
佇列客戶端(API → Worker) app/services/trading_queue.py
交易 Worker(券商執行) app/services/trading_worker.py
訂單驗證服務 app/services/order_verification.py
Lambda:order_tasks(成交驗證) lambda/order_tasks.py
Lambda:worker_control(生命週期) lambda/worker_control.py
訂單歷史模型 app/models/models.pyOrderHistory
交易操作列舉 app/enums.pyTradingOperation
SQS 佇列定義 terraform/sqs.tf