SpotFutureArbDelta · 原始碼 Code Review / 教學
對照 MMK C++ 策略 + mm-hub 後端/前端 · 供 cross-check 邏輯與 UI 映射
1. 核心檔案地圖
| 檔案 | 職責 |
|---|---|
| strategy/spot_future_arb_delta_strategy.h | Class 定義:ArbState、Direction、OrderRole、PairContext、所有 private action 方法。 |
| strategy/spot_future_arb_delta_strategy.cc | 666 行完整狀態機:Start → quote thread → ProcessPair → OnOrder fill 處理 → PrintMetricIfDue。 |
| strategy/parameter.h | 新增 Entry_Threshold_Bps、Emergency_Unwind_Bps、Chase_*、Order_Qty(Mongo 字串 parse)。 |
| trading/mmk_manager.cc | ParseStategyParameter() 讀上述欄位;RecoveryPosition() 在 Start() 前注入倉位。 |
| server/main.py | /api/arb-delta/state parse ARBVIEW;log/history/clear-halt/unwind 等 LIVE 控制 API。 |
| static/arb-delta.js | 輪詢 state、渲染 Live pairs、Exchange History、Start/Stop/Unwind、Clear halt。 |
2. 執行架構(event-driven)
OnQuote(symbol) ──► quote_map_ + quote_queue_
│
TradingPairsWork() ◄─────┘ (dedicated thread, TryConsume)
│
└── state_mutex_ ──► ProcessPair(trading_pair_id)
│
┌───────────────┼───────────────┐
Flat/Entry/Race/Carry/Halted │
PlaceEntry / Chase / MarketUnwind │
│
OnOrder(fill/reject) ──────────────┘ (same state_mutex_)
策略繼承 StrategyBase:落單走 SendOrder / SendAmend / CancelOrder;
行情走 SubscribeQuote;成交回調 OnOrder。
唔 poll REST、唔寫 JSON snapshot——倉位靠 RecoveryPosition + internal model(spot_pos / future_pos)。
quote_map_mutex_ 保護最新 quote;state_mutex_ 保護 pair_ctx_ 同 order 路由。
ProcessPair 同 OnOrder 共用同一把鎖,避免 fill 同 chase 競態。
3. Start() — 恢復邏輯(L52–105)
對每個 strategy_parameter_map_ entry 建立 PairContext,並從 GetPosition() seed internal pos:
flat_spot ∧ flat_fut → state = Flat, last_action = "restored_flat"
spot × future < 0 且 |net| ≤ 5%×max leg → CarryHedged, direction 由 spot_pos 正負推導
其他(單腳/不平衡) → Halted, last_error = "restart_unbalanced_position_review"
RaceExit phase 或舊 open order(RecoveryOrder 未啟用)。
Operator 應在重啟前確保 exchange 大致 flat,否則可能 halted 或 ghost carry(mm-hub Start 時若 exchange flat 會清 LMDB)。
4. SignalFromBasis() + Basis 計算(L157–181)
spot_mid = (spot_bid + spot_ask) / 2
fut_mid = (future_bid + future_ask) / 2
basis_bps = (fut_mid / spot_mid - 1) × 10⁴
if basis_bps > Entry_Threshold_Bps → LongSpotShortFuture (買 spot + 賣 future)
if basis_bps < -Entry_Threshold_Bps → ShortSpotLongFuture (賣 spot + 買 future)
else → None
UI 欄位 signal = 當前 basis 推導方向;direction = 已 commit 嘅持倉方向(flat 時 none)。
Log 另印 fut_minus_spot_bps = (fut_mid - spot_mid)/spot_mid × 10⁴,與 basis_bps 數值相同但公式寫法不同,方便交叉核對。
5. 狀態機 — ProcessPair() switch(L188–205)
| state | 觸發 action | 下一狀態 |
|---|---|---|
flat | signal != None → PlaceEntry | entry_working |
entry_working | ChaseEntry(amend 或 cancel+replace) | 維持,直至 entry fill |
race_exit | ChaseRaceOrders | 兩腿 exit fill → flat 或 carry |
carry_hedged | MaybeEmergencyUnwind 或 signal flip → MarketUnwind | flat 或 halted |
halted | 只 PrintMetricIfDue,唔落新單 | 需 binary restart 或 UI clear-halt |
每 tick 先跑 MaybeEmergencyUnwind(只對 carry_hedged),再跑 state switch。
6. Entry 流程 — spot-only first leg
PlaceEntry(L318–337):
- 若 internal pos 非 flat →
Halt("entry_blocked_internal_position_not_flat") LongSpotShortFuture:spot Limit BUY @spot.bid[0]ShortSpotLongFuture:spot Limit SELL @spot.ask[0]- Qty =
Order_Qty(base coin);唔會同時落 future leg
ChaseEntry(L339–357):
- 間隔
Chase_Interval_Ms;target = 方向對應 best bid/ask - 若
|target/entry_price - 1| × 10⁴ > Chase_Max_Slip_Bps→CancelRef+ 新 limit(唔 amend) - 否則
AmendRefchase best - recreate 失敗 →
Halt("entry_recreate_failed");REJECT →Halt("entry_spot_rejected")
entry_spot_rejected 通常係 chase cancel+replace 時 Bybit 170131 Insufficient balance——spot sell 需要 base 庫存或 margin 不足。
7. Race Exit — entry fill 後 hedge
OnEntryFilled(L463–476)更新 spot_pos,再 PlaceRaceOrders:
| direction | Future leg | Spot exit leg |
|---|---|---|
long_spot_short_future |
Limit SELL future @ swap.ask[0] |
Limit SELL spot @ max(spot.ask, entry_price) |
short_spot_long_future |
Limit BUY future @ swap.bid[0] |
Limit BUY spot @ min(spot.bid, entry_price) |
兩張 limit 互斥 race:邊張先 fill 決定 carry 或 flat。
ChaseRaceOrders 無 Chase_Max_Slip 限制,只按 interval amend。
Submit 失敗 → cancel 兩腿 + MarketUnwind("race_submit_failed")。
OnOrder race fill 分支(L618–649):
- Future fill 先:cancel spot exit →
carry_hedged(last_action=carry_open) - Spot exit fill 先:cancel future exit →
ResetToFlat("spot_exit_flat")(完整平倉) - 兩腿都 fill:
HandleOverHedge→ResetToFlat("both_exit_filled")
8. Carry / Emergency / Market Unwind
MaybeEmergencyUnwind(L403–418,只限 carry_hedged):
Emergency_Unwind_Bps > 0且 spread 反向超過門檻 → market unwindEmergency_Unwind_Bps ≤ 0(你而家設定)→ 只靠 signal flip- Signal 方向與
ctx.direction相反 →MarketUnwind("signal_flip")
SubmitSpotMarketBuyQuote(L297–316)— 關鍵 workaround:
quote_qty = base_qty × ref_price × (1 + 10bps)(kSpotBuyCrossBps=10),確保買足 base。
Spot Market SELL 同所有 perp Market 仍用 base qty。
Market 單送出成功後 樂觀歸零 internal pos(不等 fill ack)。極端 REJECT 可能誤判已平——v1 已知取捨。
9. OnOrder() — REJECT / CANCEL(L652–662)
FILLED+ 對應 role → 見上節 race / entry 分支REJECTEDon EntrySpot / ExitSpot / ExitFuture →Halt("{role}_rejected")CANCELLED→ 清ref.active,唔一定 halt(chase 正常 cancel)
HandleOverHedge:若 future leg residual → market close future(over_hedge_future_close)。
10. Halt 條件一覽
| last_error 前綴 | 原因 | 恢復 |
|---|---|---|
entry_spot_rejected | Entry limit REJECT(常見 balance) | 補 balance / 清 halt + restart |
entry_recreate_failed | Chase slip 後 recreate 失敗 | 同上 |
restart_unbalanced_position_review | 重啟時倉位不平衡 | 手動平倉後 restart |
market_unwind_failed_* | Emergency market 平倉失敗 | 檢查 API / 流動性 |
race_quotes_missing | Entry fill 後無 quote | restart |
Halt 係 in-memory(ctx.state = Halted),唔寫 LMDB。
mm-hub POST /api/arb-delta/clear-halt 要求 exchange + ARBVIEW 已 flat,再 systemctl restart。
11. PrintMetricIfDue() — ARBVIEW 遙測(L535–576)
每秒最多一行(kHeartbeatMs=1000),格式:
ARBVIEW pair=BTC strategy=SpotFutureArbDelta spot=... future=...
state=... direction=... signal=... basis_bps=... fut_minus_spot_bps=...
spot_bid=... spot_ask=... future_bid=... future_ask=...
spot_pos=... future_pos=... entry_price=... open_orders=...
signals=... orders=... fills=... last_action=... last_fill=... last_error=...
mm-hub _arb_delta_state_payload() tail log、grep ARBVIEW 、按 pair 取最新行 parse 成 JSON。
Live pairs table 每 1.5s 輪詢 /api/arb-delta/state。
12. mm-hub 對照(非 MMK,但影響操作)
| API / UI | 對應策略行為 |
|---|---|
GET /api/arb-delta/state | Parse ARBVIEW → Live pairs |
GET /api/arb-delta/log?filter=key | ORDER / ARBVIEW / WARN 重點行 |
GET /api/arb-delta/history | Bybit REST open/order/trade(spot+linear),key 來自 mmk_config.xml |
| Start / Stop | systemctl spot-future-arb-delta.service;Stop = MMK cancel all,唔平倉 |
| Unwind & Stop | Stop 後 Python limit chase 平倉(唔用策略 MarketUnwind) |
| Clear halt | 驗證 flat → restart binary → restored_flat |
| Footer 帳戶 | bybit-account.json,可能同策略 key 不同 |
13. Cross-check 清單(逐項核對)
Entry_Threshold_Bps=5時,|basis|≥5 應觸發 entry;UIsignal同 log 一致- Entry 只落 spot limit;future 單只喺 entry fill 後 race 出現(Exchange History 時間序)
carry_hedged時spot_pos × future_pos < 0且 |spot_pos|≈|future_pos|≈Order_Qtyopen_orders等於 ARBVIEW 數字;同 Exchange History open orders 對得上- Chase entry 超 slip → log 有
cancel reason=entry_slip_too_far再 submit - Signal flip 時 log 有
market_unwind_failed_signal_flip或成功ResetToFlat - Spot market buy unwind 用 quote qty(log
quote_qty=),唔係 base qty - 重啟後 state:exchange flat →
restored_flat;對沖 carry →restored_carry - Halt 後 UI
last_error同 logERR strategy_haltedreason 一致 - 改 Mongo 參數後必須「儲存並重啟 binary」,否則 ARBVIEW 仍顯示舊 threshold