群益API 程式架構:一支程式同時跑行情、下單、回報

群益API Python 程式架構:一支程式同時跑行情、下單、回報

前面每一篇的範例各自跑得動,合在一支程式就亂了:不知道先後順序,也不知道哪裡要等。行情來了就想下單,回報卻還沒就緒。斷線重連、訂閱、處理資料的程式碼散在各個事件裡,越寫越難追。測試時,也擔心不小心送出真單。

群益API 程式架構的重點只有一個:事件只記狀態、只收集資料;抽訊息、重連、訂閱、處理資料、下單,全部回到主迴圈做。下單一律經過模擬開關,預設模擬。

本文是〈群益API Python 入門〉的延伸,只負責把前面各篇接起來;每一步的細節,都連回原本說明它的那一篇。

群益API 程式架構示意圖:登入前一段,登入後三條線,最後由主迴圈統一處理

群益API 程式從啟動到可以下單,要經過哪些步驟?

【環境】      環境檢查通過
【登入前】    GetModule → CreateObject(四個物件)→ GetEvents(每個物件一個事件類別)
              (要切換環境時:SetAuthority)
              Login
【登入後,三條線】
  下單線:    SKOrderLib_Initialize → ReadCertByID → GetUserAccount(帳號從 OnAccount 回來)
  回報線:    SKReplyLib_ConnectByID → 等 OnComplete(回補完成)
  行情線:    EnterMonitorLONG → 等 OnConnection 收到 3003 → 訂閱
【主迴圈】    抽送訊息 → 重連 → 訂閱 → 處理收集到的資料 → 需要時下單(經過模擬開關)

每一步的細節:

步驟細節見
環境檢查〈群益API Python 環境檢查〉
物件、事件、登入前後的順序〈群益API 的六個 COM 物件各管什麼〉
切換環境〈群益API 測試環境是什麼〉
登入錯誤碼〈群益API 登入錯誤怎麼判讀〉
下單線、回報線〈群益API 下單初始化有哪 7 步〉
行情線、3003〈群益API Python 入門〉、〈群益API 連線狀態碼怎麼解讀〉
商品代碼〈群益API 商品代碼怎麼填〉

三條線之間,官方沒有規定先後;本文照下單、回報、行情的順序寫。

什麼時候可以開始下單?建議等回報線收到 OnComplete(回補完成)再送單,原因見〈群益API 下單初始化有哪 7 步〉。你的策略如果需要報價,再加上行情就緒這個條件;行情就緒是策略需要報價時的條件,不是下單函式的前提。

主迴圈怎麼分工?事件記狀態,主迴圈做事

事件裡只做兩種事:改狀態旗標、把資料放進收集區。這是〈群益API 事件回呼跑在哪個執行緒〉回呼紀律的應用。

主迴圈每一圈做這些事:

  1. 抽送訊息:呼叫 pythoncom.PumpWaitingMessages(),讓事件進來,見〈Python 接群益API 收不到事件〉。
  2. 行情斷線就重連:收到 3002 的旗標後,在事件之外重新連線,見〈群益API 斷線怎麼重連〉。
  3. 行情就緒就訂閱一次:不論成敗都只送一次,避免每一圈重試;斷線時把「已訂閱」旗標清掉,重連就緒後再送一次。
  4. 回報斷線就等幾秒再重連:同樣見〈群益API 斷線怎麼重連〉。
  5. 處理收集到的資料:報價用 GetStockByIndexLONG 取快照;回報字串以 980 開頭時原樣記錄,見〈群益API 委託回報怎麼解析〉;帳號資料收下來留給下單用。
  6. 需要下單時,呼叫經過模擬開關的 send_order()。

跟入門篇的寫法有一點不同:入門篇在 OnNotifyQuoteLONG 事件裡直接取快照,這裡改成事件只收集索引、主迴圈再取。兩種寫法都可以;這裡這樣寫,是為了讓所有事件都只做最輕量的事。主迴圈取到的,是取的當下的快照。

下單一律走模擬開關

骨架用的是〈群益API 測試環境是什麼〉模擬開關四點裡的前兩點:

  1. 全域的開關,預設是模擬。
  2. 模擬時,在呼叫下單函式之前攔截,不送出委託。

刪改單在模擬時的處理、切換到真單的明確動作,骨架沒有寫,做法見同一篇。

另外,真單的路徑多一道檢查:回報還沒就緒,就不送單。骨架的主流程也沒有呼叫 send_order();什麼時候下單,由你自己的策略決定。

為什麼用單執行緒?

在這個單執行緒的骨架下,事件和主迴圈都在同一條執行緒上,所有 COM 呼叫也都在這條執行緒,不需要另外加鎖。

在 STA 下,事件會在你抽送訊息的那條執行緒上進來,見〈Python 接群益API 收不到事件〉。之後如果想把運算交給其他執行緒,COM 呼叫要留在建立物件的那條執行緒上,見〈群益API 事件回呼跑在哪個執行緒〉。

群益API Python 整合骨架

# 群益API Python 整合骨架:行情、下單、回報同一支程式(下單預設模擬)
# 環境:Windows、Python 64 位元、已註冊 64 位元元件、pip install comtypes pywin32
import os
import time
import pythoncom
import comtypes.client

DLL_PATH = r"C:\SKCOM\x64\SKCOM.dll"          # 改成你註冊的元件路徑
USER_ID = os.environ.get("CAPITAL_ID", "")     # 帳密從環境變數讀,不寫死
PASSWORD = os.environ.get("CAPITAL_PWD", "")
SIMULATION = True                              # 模擬開關:預設模擬,不呼叫下單函式

comtypes.client.GetModule(DLL_PATH)
import comtypes.gen.SKCOMLib as sk

center = comtypes.client.CreateObject(sk.SKCenterLib, interface=sk.ISKCenterLib)
reply = comtypes.client.CreateObject(sk.SKReplyLib, interface=sk.ISKReplyLib)
quote = comtypes.client.CreateObject(sk.SKQuoteLib, interface=sk.ISKQuoteLib)
order = comtypes.client.CreateObject(sk.SKOrderLib, interface=sk.ISKOrderLib)

# 事件裡只記狀態、只收集資料;真正的處理在主迴圈
state = {"quote_ready": False, "quote_down": False, "subscribed": False,
         "reply_ready": False, "reply_down_at": None}
inbox = []                                     # 事件收集到的資料,主迴圈再處理
accounts = []                                  # 帳號資料留給下單使用,不要印到主控台或 log


class ReplyEvents:
    def OnReplyMessage(self, bstrUserID, bstrMessage):
        return -1                              # 登入前必須掛上,回 -1

    def OnComplete(self, bstrUserID):
        state["reply_ready"] = True            # 回補完成,之後才開放送單

    def OnNewData(self, bstrUserID, bstrData):
        inbox.append(("reply", bstrData))      # 回報字串交給主迴圈解析

    def OnSolaceReplyDisconnect(self, bstrUserID, nErrorCode):
        state["reply_ready"] = False
        state["reply_down_at"] = time.time()   # 不在事件裡重連


class QuoteEvents:
    def OnConnection(self, nKind, nCode):
        if nKind == 3003:
            state["quote_ready"] = True
        elif nKind == 3002:
            state["quote_ready"] = False
            state["subscribed"] = False
            state["quote_down"] = True         # 不在事件裡重連

    def OnNotifyQuoteLONG(self, sMarketNo, nIndex):
        inbox.append(("quote", (sMarketNo, nIndex)))


class OrderEvents:
    def OnAccount(self, bstrLogInID, bstrAccountData):
        inbox.append(("account", bstrAccountData))


# 每個物件一個事件類別;回傳值要存起來
reply_conn = comtypes.client.GetEvents(reply, ReplyEvents())
quote_conn = comtypes.client.GetEvents(quote, QuoteEvents())
order_conn = comtypes.client.GetEvents(order, OrderEvents())


def check(name, code):
    print(name, code, center.SKCenterLib_GetReturnCodeMessage(code))
    return code == 0


def send_order(order_obj):
    """策略要下單時呼叫這裡;模擬時根本不呼叫下單函式"""
    if SIMULATION:
        print("模擬單,未送出")
        return 0, "SIM"
    if not state["reply_ready"]:
        return -1, "回報尚未就緒,不送單"
    message, code = order.SendFutureOrderCLR(USER_ID, False, order_obj)
    return int(code), message


# 1. 登入
if not check("Login", center.SKCenterLib_Login(USER_ID, PASSWORD)):
    raise SystemExit("登入失敗,判讀見登入錯誤碼那篇")

# 2. 下單線
check("SKOrderLib_Initialize", order.SKOrderLib_Initialize())
check("ReadCertByID", order.ReadCertByID(USER_ID))
check("GetUserAccount", order.GetUserAccount())   # 帳號從 OnAccount 回來

# 3. 回報線
check("SKReplyLib_ConnectByID", reply.SKReplyLib_ConnectByID(USER_ID))

# 4. 行情線
check("EnterMonitorLONG", quote.SKQuoteLib_EnterMonitorLONG())

# 5. 主迴圈:抽送訊息、處理收集到的資料、在事件之外重連與訂閱
REPLY_WAIT_SECONDS = 5                         # 示意值,依你的程式調整;官方只說「等待數秒」
while True:
    pythoncom.PumpWaitingMessages()

    if state["quote_down"]:
        state["quote_down"] = False
        quote.SKQuoteLib_EnterMonitorLONG()
    if state["quote_ready"] and not state["subscribed"]:
        page_no, code = quote.SKQuoteLib_RequestStocks(1, "TX00")
        check("RequestStocks", code)
        state["subscribed"] = True             # 不論成敗只送一次;失敗看訊息,重連就緒後才會再送

    down_at = state["reply_down_at"]
    if down_at and time.time() - down_at >= REPLY_WAIT_SECONDS:
        state["reply_down_at"] = None
        if reply.SKReplyLib_IsConnectedByID(USER_ID) == 0:
            reply.SKReplyLib_ConnectByID(USER_ID)

    while inbox:
        kind, data = inbox.pop(0)
        if kind == "quote":
            stock, code = quote.SKQuoteLib_GetStockByIndexLONG(data[0], data[1], sk.SKSTOCKLONG())
            if code == 0:
                print(stock.bstrStockNo, stock.nClose / (10 ** stock.sDecimal))
        elif kind == "reply":
            fields = data.split(",")
            if fields[0] == "980":
                print("回報原樣記錄:", data)  # 980 開頭不套欄位表
            # 其他欄位的解析見委託回報那篇
        elif kind == "account":
            accounts.append(data)              # 帳號資料留給下單使用,不要印到主控台或 log

    time.sleep(0.05)

幾點說明:

  • 這段骨架的每個呼叫,都在前面各篇說明過;整合起來,需要你用自己的帳號實際跑一次。
  • SIMULATION = True 時,策略呼叫 send_order() 不會送出任何委託。
  • 帳號密碼從環境變數讀,不寫死在程式裡;帳號資料只收下來留給下單用,不印到主控台或 log。
  • 各步驟失敗時要不要中止,依你的程式設計;骨架只印出結果。
  • REPLY_WAIT_SECONDS = 5 是示意值:官方只說「等待數秒」,請依你的程式調整。
  • inbox 只是示意:收集區的資料結構,依你的程式設計。
  • 回傳值一律直接拆包,原因見〈群益API Python 回傳值為什麼是 list〉;下單失敗的判讀,見〈群益API 下單回傳 980 怎麼讀〉。

群益API 程式架構的 4 個誤解

  1. 誤解: 行情就緒就可以下單。其實: 建議等回報線收到 OnComplete(回補完成)再送單;行情就緒是策略需要報價時才加上的條件。
  2. 誤解: 事件來了,就在事件裡把事情做完。其實: 事件裡只記狀態、收集資料,重連、訂閱、下單都回到主迴圈做。
  3. 誤解: 測試時用小口數下單就好。其實: 在程式裡做模擬開關、預設模擬,才能確保不會送出真單。
  4. 誤解: 重連之後,訂閱會自己恢復。其實: 斷線時清掉「已訂閱」的旗標,重連就緒後重新送出訂閱。

常見問題

群益API 的行情、下單、回報要照什麼順序初始化?

官方沒有規定三條線的先後;本文照下單、回報、行情的順序寫。各條線自己的順序與就緒訊號,見〈群益API 的六個 COM 物件各管什麼〉。

什麼時候才能開始送單?

建議等回報線收到 OnComplete(回補完成)再送單。你的策略如果需要報價,再加上行情收到 3003 這個條件。

怎麼確保測試時不會送出真單?

在程式裡做模擬開關,預設模擬:模擬時在呼叫下單函式之前攔截,不送出委託。切換到真單要有明確的動作。

程式要結束時要做什麼?

先停止送單、停止新的訂閱。另外,官方文件沒有登出函式。見〈群益API 斷線怎麼重連〉的常見問題。

風險揭露

期貨與選擇權屬高槓桿商品,價格波動可能造成超過原始保證金的損失,交易人須自負交易責任。本文為程式開發技術教學,說明群益API Python 程式的整體架構,不構成投資建議,也不保證任何交易結果。程式化交易並不降低市場風險,程式錯誤可能造成非預期的委託行為;本文的骨架預設模擬、不含任何策略,正式使用前請自行撰寫並充分測試。

投資人教育資源

機構資源
臺灣期貨交易所(TAIFEX)期貨及選擇權數位學習網
證券暨期貨市場發展基金會證券期貨市場教育推廣
金融智慧網金管會金融知識平台
中華民國期貨業商業同業公會期貨業法規與宣導
證券投資人及期貨交易人保護中心投資人保護與申訴

延伸閱讀

參考資料

  • 各函式與事件的宣告,依群益官方元件說明文件;逐項核對見本系列各篇。
  • 三條線的撰寫順序、等回報就緒再送單、事件只收集的分工:本系列各篇的做法與通用工程做法,非官方規定。
  • 整合骨架:已做語法與流程的檢查,登入之後的整段沒有整合實跑紀錄。

免責聲明

本文章僅作為群益API實作經驗分享,不構成投資建議,且策略及程式皆應自行撰寫。期貨及衍生性金融商品交易屬高風險投資,請謹慎評估自身風險承擔能力。

延伸閱讀|相關文章

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *