訂單執行生命週期¶
訂單透過三個路徑進入系統: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 秒中止逾時防止雙重曝險。
三條執行路徑¶
Webhook 訂單流程¶
Webhook 路徑處理來自 TradingView 警報的自動化訂單。這是正式交易的主要執行路徑。
逐步分解¶
| 步驟 | 元件 | 動作 | 失敗模式 |
|---|---|---|---|
| 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 驗證。
| 與 Webhook 的差異 | 儀表板 | Webhook |
|---|---|---|
| 驗證方式 | JWT cookie + CSRF 權杖 | Webhook 權杖 + 密鑰 |
| 驗證層數 | 2(JWT + CSRF) | 4(權杖 + 時間戳 + 密鑰 + 重放) |
| 訂單來源 | source="dashboard" |
source="webhook" |
| 標的驗證 | 使用者從已載入標的中選擇 | 必須在允許清單中 |
| 速率限制 | 標準 API 速率限制 | 每個 webhook 的每日下單限制 |
成交驗證¶
每次下單後,背景 Lambda 會透過輪詢券商 API 來驗證成交狀態。這處理了訂單提交與成交確認之間的間隔——對於非同步回報成交的券商尤其重要。
驗證細節¶
| 參數 | 值 |
|---|---|
| 佇列 | 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() 行為——進入反向倉位時自動反轉現有倉位。
反轉矩陣¶
| 目前倉位 | 訊號 | 步驟 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 不可用¶
| 失敗場景 | 行為 |
|---|---|
| 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.py(OrderHistory) |
| 交易操作列舉 | app/enums.py(TradingOperation) |
| SQS 佇列定義 | terraform/sqs.tf |