像股票這樣的金融商品百百種,加上市場的變化性與多樣的市場規則,使得這類的分析總是相當困難。但相關數據取得與蒐集的管道也相當稀少,甚至難以整合與上手。國內幾家券商提供之API服務便顯得相當地重要。
前言
上一篇群益API行情串接(一)介紹了群益金鼎證券提供之API服務,包含先備知識,並且完成元件註冊與帳號連線。本篇將會繼續往下介紹如何實現相關報價功能。
因此,本篇將會真正進行報價功能的實作與使用,報價功能主要是一個事件,事件的相關方法我們可以透過Class去定義與實作出來,並且透過comtypes套件進行事件的註冊。
在開始實作之前,要提醒一下,相關的設定或變數、函式宣告與定義等部份,在這邊並不會重新提及,這是由於篇幅考量,所以這個系列基本上就是垂直往下連貫著進行。如果有在這個系列中發現有些變數或函式,甚至是名詞等等未定義,可以在先前的篇章中找到。
程式環境
程式語言: Python 3.6.8
作業系統: Windows 10 64位元
API版本: 2.13.16 x86版本
撰寫自己的報價功能
為了將事件的實際功能實作出來,我們首先可以定義一個Class:
class SKQuoteLibEvents:
def __init__(self):
self.__is_connect = False
self.__is_ready = False
self.__is_data_get = False
self.__Stock_list_dict = {key: [] for key in market_number_dict.keys()}
self.__Stock_dict = {}
self.__Stock_id = ''
在這個Class裡面,我們先使用__init__(self)初始化需要的變數,這邊的變數我們都使用private的形式,所以變數名稱開頭會是以兩個底線(__)作為命名。其中,定義了三個flag,以布林值為型態,分別為<self.__is_connect>代表是否已連線、<self.__is_ready>代表報價相關功能是否已準備好、<self.__is_data_get>代表資料是否已正常取得(正常取得也包含無資料回傳的訊息,且資料可以是連線的狀態所回傳的訊息)。
另外,還有三個變數,代表與股票等商品相關之資料儲存,<self.__Stock_id>用來代表每一次請求的商品代號、<self.__Stock_list_dict>用來儲存不同類型市場下的所有能取得之商品代號名稱、<self.__Stock_dict>用來儲存不同商品代號各自的所有能取得之交易訊息。
這邊的變數名稱是寫<…Stock…>,但其實也可以是涵蓋其他商品,各位可以依照自己的需求進行修改。反正,變數名稱能夠一定程度地被人辨識出其意義就行了。(如果使用<…Goods…>等等變數名稱,或許還可能與其他定義混淆,所以這邊選擇<...Stock...>)
如果需要存取以上這些private變數的話,雖然在python還是可以直接進行呼叫與修改,但為了保持習慣,還是寫幾個set()與get()函式:
class SKQuoteLibEvents:
...
"""
set、get fuction.
"""
def set_is_ready(self, status: bool):
self.__is_ready = status
def set_is_data_get(self, status: bool):
self.__is_data_get = status
def set_Stock_id(self, id: str):
self.__Stock_id = id
def get_is_connect(self):
return self.__is_connect
def get_is_ready(self):
return self.__is_ready
def get_is_data_get(self):
return self.__is_data_get
def get_Stock_list_dict(self, key: int):
if key not in self.__Stock_list_dict:
return []
return self.__Stock_list_dict[key]
def get_Stock_dict(self, key: str):
if key not in self.__Stock_dict:
return []
return self.__Stock_dict[key]
我們並不需要針對每個變數都設置一個set()與get()函式,只需要考慮有需要在在物件外部存取的。像是<self.__is_connect>只需要設置get()函式,因為是否連線的狀態,應該是這個物件內部自己能夠管理與設置就行。
這裡我們設定了三個set()函式,以及五個get()函式。
就像是剛才所說的,<self.__Stock_id>用來代表每一次請求的商品代號,這邊的做法是會隨時用來將它來使物件改變狀態,代表對不同的商品進行請求,因此,設置一個set_Stock_id()函式。set_is_ready()以及set_is_data_get()也是同樣道理。
get()函式方面,除了<self.__is_connect>變數之外,皆各自設置一個,方便進行確認。
其中,get_Stock_list_dict()與get_Stock_dict()函式都有一個<key>傳入參數,代表這個查詢的動作,在前者是針對市場類別進行回傳,而後者是針對一個特定的商品代號進行查詢。
這邊為了顯示統一,所以另外寫一個物件的member function:
class SKQuoteLibEvents:
...
def print(self, message: str):
print('>\t' + message)
接著,我們終於要來進入主題,要來決定要定義與使用的事件,詳細的功能與有哪些事件能夠去調用,可以參考群益API附帶的官方文件。
我們要使用到的ATL物件是SKQuoteLib,所以可以查看其擁有的相關可用函式。而首先當然是要進行連線,我們會需要去定義自己的OnConnection()函式。從這邊開始,要注意的是,自己定義的相關函式,其傳入參數必需按照群益API官方文件所提供的架構去寫,如此才能正確對應。
class SKQuoteLibEvents:
...
"""
define events.
"""
def OnConnection(self, nKind, nCode):
self.__is_data_get = True
self.__is_connect = False
str_msg = "Connect Error!"
if nCode == 0:
if nKind == 3001:
str_msg = "Connected!"
self.__is_connect = True
elif nKind == 3002:
str_msg = "DisConnected!"
elif nKind == 3003:
str_msg = "Stocks ready!"
self.__is_connect = True
self.__is_ready = True
elif nKind == 3021:
str_msg = "Connect Error!"
self.print(str_msg)
OnConnection()函式有兩個傳入參數,分別是<nKind>以及<nCode>,兩者都是一組代碼,前者用來代表連線狀態,後者用來代表連線的執行是否成功,成功的話會回傳0,否則代表有例外產生。
到時候這個連線事件若有觸發,會針對不同的狀態(由<nKind>以及<nCode>兩個傳入參數組合而成)進行調整。
這邊要特別注意的是,先前提到,<self.__is_data_get>代表資料是否已正常取得(正常取得也包含無資料回傳的訊息,且資料可以是連線的狀態所回傳的訊息),所以由於事件觸發了,代表有能取得正常資料的能力,我們將其設定為True。
再來,我們會需要取得目前不同市場類別底下各自的商品有哪些,會需要用到OnNotifyStockList事件:
class SKQuoteLibEvents:
...
def OnNotifyStockList(self, sMarketNo: int, bstrStockData: str):
"""
市場編號分別為:
上市 0、上櫃 1、期貨 2、選擇權 3、興櫃 4
各類股(水泥、食品、塑膠等)以「換行」做分隔。
"""
self.__is_data_get = True
# [商品代碼],[商名名稱],[最後交易日];
if not bstrStockData.startswith('##'):
row_list = ['[OnNotifyStockList]']
row_list.extend(bstrStockData.split(';')[:-1])
self.__Stock_list_dict[sMarketNo].append(row_list)
else:
self.print("StockList Response end.")
self.__is_ready = True
OnNotifyStockList()函式需要包含兩個傳入參數,分別為<sMarketNo>以及<bstrStockData>,其實名稱可以自己命名,但要記得其原本意義為何。
<sMarketNo>代表市場類別的代號,這也對應到前一篇設置的<market_number_dict>變數:
market_number_dict = {
0: '上市',
1: '上櫃',
2: '期貨',
3: '選擇權',
4: '興櫃'
}
而<bstrStockData>會回傳所請求的市場類別下,所有商品代號,不同類股會分不同次回傳,每一個商品又各自以一組逗號(,)分隔之資料表示,並以分號(;)結尾([商品代碼],[商名名稱],[最後交易日];)。
這邊使用到了bstrStockData.split(';')進行分割,回傳是一個List型態的物件。這行程式處理完後,會如下圖這樣(我們等等會進行測試,這邊只是先進行輸出,先看看格式):
而由於每組商品字串,都會以分號(;)結尾,所以最後會多分割出一個空字串,我們使用Python Slice用法,bstrStockData.split(';')[:-1]忽略這個空字串就行。
而當所有資料結束回傳時,會有一筆以"##"開頭的字串作為結尾。當我們讀到這筆資料,代表這個步驟的資料取得正常結束,並準備完成,將<self.__is_ready>設定為True作為標記。
雖然還有一些功能尚未設定,但我們已經可以準備先跑跑看事件能不能正常運作了。
所以我們先在SKQuoteLibEvents物件告一段落,其他功能等測試完再加。
這邊我們要撰寫連接與離線報價伺服器的程式,為了方便管理,就以自定義函式呈現:
def EnterMonitor():
m_nCode = skQ.SKQuoteLib_EnterMonitor()
print("Quote enter monitor [{}]".format(skC.SKCenterLib_GetReturnCodeMessage(m_nCode)))
def LeaveMonitor():
m_nCode = skQ.SKQuoteLib_LeaveMonitor()
print("Quote leave monitor [{}]".format(skC.SKCenterLib_GetReturnCodeMessage(m_nCode)))
skQ是上一篇我們在進行環境初始化時,設定的一個報價物件,這邊是直接使用全域變數,就不另行以傳入參數的形式了。這個報價物件裡面有SKQuoteLib_EnterMonitor()以及SKQuoteLib_LeaveMonitor()函式,分別代表連線報價伺服器與斷線報價伺服器的功能,它們會各自回傳一個代碼,這個代碼可以透過skC物件(登入與環境管理用)中的SKCenterLib_GetReturnCodeMessage()函式,將這個代碼轉為文字訊息,這樣可以更直觀地在螢幕上做輸出。
然後,這邊有一個很重要的東西要去實作,前一篇我們有提到Event Loop以及Message Queue,然後我們有說會使用pywin32套件裡pythoncom模組包含的PumpWaitingMessages()將獲取到的訊息,從Message Queue裡面取出並利用。
所以現在就要將其實現,我們可以設想一下,假設這個報價功能正常執行後,每一次收到訊息都是好幾串,隨著時間一筆一筆回傳回來,這意味著,我們得不停地去從Message Queue中去接收訊息,直到回傳結束。
這個功能我們把它包裝為一個函式,以方便使用與呼叫:
def run_callback_sync(QuoteEvent: SKQuoteLibEvents):
start_time = time.time()
while not QuoteEvent.get_is_ready():
pythoncom.PumpWaitingMessages()
while not QuoteEvent.get_is_connect():
print('未成功連線...')
capital_Login()
EnterMonitor()
pythoncom.PumpWaitingMessages()
time.sleep(5)
if not QuoteEvent.get_is_data_get() and time.time() - start_time > 1.0:
break
QuoteEvent.set_is_ready(False)
QuoteEvent.set_is_data_get(False)
這個函式取名為run_callback_sync,會傳入一個實例化後SKQuoteLibEvents的物件,而由於在Python中將一個實例化的物件當作一個函式傳入參數,實際上是做別名(Aliasing)的動作,所以若在這個函式中對這個傳入參數物件<QuoteEvent>進行修改,對整個程式而言,被當作傳入參數的這個物件也會跟著改變狀態。(它們各自存取的記憶體位置相同)
這邊的流程大概是,如果資料尚未準備好(資料尚未全部取得完畢),就會一直執行pythoncom.PumpWaitingMessages()函式。
只是過程中我們會另外檢查是否有連線,若無連線會反覆重試,(不過目前來說,只有在一開始像是未連接網路導致的"未連線成功"有用而已)因為SKQuoteLibEvents物件中的<self.__is_connect>並不會馬上在自己在斷線時改變狀態,當斷線一定時間,OnConnection事件也會又觸發一次,回傳連線失敗的訊息。此外,就算是重新連線,也要經過商品下載與重新註冊商品等等動作,否則也是無法繼續往下做。無論如何,這邊就簡單判斷就好,反正遇到問題,或是先確保網路沒問題,再去執行一次就行了。(如果真的要自動化這個部分,其實也能透過相關的檢查連線狀態函式去確認,並且記錄已抓取的資料有哪些,這樣就只剩去抓取尚未成功抓取完成的資料就好)
這邊需要特別注意,事件每次回傳訊息都會存在Message Queue中,所以即便登入或連線,都要呼叫pythoncom.PumpWaitingMessages()函式,才能取得正常的數據。
另外,如果任何訊息都無法獲取,且超過一秒鐘,我們就會將迴圈停止,就當作沒有回傳訊息了。
這邊我們可以先來看看實際執行結果(等等會講到如何呼叫與使用已經實作好的功能),如下圖所示:
我們可以看到,如果一開始網路斷線導致登入未成功,報價伺服器也未成功連線,等到網路恢復,就能繼續往下正常執行。
我們最後來測試一下,到目前為止,所實現的功能運作起來會是甚麼樣子,首先會需要進行事件的註冊,我們可以透過comtypes.client.GetEvents()函式去進行綁定:
if __name__ == '__main__':
skC, skQ = capital_api_init(api_path)
capital_Login(account, password)
# event registration.
SKQuoteEvent = SKQuoteLibEvents()
SKQuoteLibEventHandler = comtypes.client.GetEvents(skQ, SKQuoteEvent)
接著,是進行報價伺服器的連線,以及取得商品列表:
if __name__ == '__main__':
skC, skQ = capital_api_init(api_path)
capital_Login(account, password)
# event registration.
SKQuoteEvent = SKQuoteLibEvents()
SKQuoteLibEventHandler = comtypes.client.GetEvents(skQ, SKQuoteEvent)
EnterMonitor()
print('start...')
run_callback_sync(SKQuoteEvent)
# get Goods List.
for index in market_number_dict.keys():
print("RequestStockList: {}".format(index))
skQ.SKQuoteLib_RequestStockList(index)
run_callback_sync(SKQuoteEvent)
取得商品列表之後,我們可以透過skQ.SKQuoteLib_RequestTicks()函式去請求商品資訊,其會需要兩個傳入參數,第一個參數代表Page Number,基礎會有1~49 Page可以使用,不同的函式所訂立一個Page各自的索取量不同,如在skQ.SKQuoteLib_RequestStocks()中,一個Page可以有100檔的索取量;這邊則是一個Page只接受一檔。而若重複使用相同Page,內容會被新的索取內容所取代。第二個參數代表商品代號。
這邊要注意的是,目前我們尚未去設定從相關事件(像是歷史Tick回補等等)的接收函式,所以使用skQ.SKQuoteLib_RequestTicks()函式是沒有特別的功用,所以我們先註解掉:
if __name__ == '__main__':
...
page_index = 1
page_size_index = 0
for index in market_number_dict.keys():
for row in SKQuoteEvent.get_Stock_list_dict(index):
print(row)
for item in row[1:]:
stock_info = item.split(',')
# 國內股價指數類商品:臺股期貨(TX)、小型臺指期貨(MTX)、電子期貨(TE)及臺指選擇權(TXO)
if index == 2:
if not (stock_info[0].startswith('TX') or stock_info[0].startswith('MTX') or
stock_info[0].startswith('TE')):
continue
if index == 3 and not stock_info[0].startswith('TXO'):
continue
if (index < 2 or index > 3) and len(stock_info[0]) > 5:
continue
SKQuoteEvent.set_Stock_id(stock_info[0])
# skQ.SKQuoteLib_RequestTicks(page_index, stock_info[0])
#
# run_callback_sync(SKQuoteEvent)
print("RequestTicks: {}\t{}\t{}".format(stock_info[0], market_number_dict[index], page_index))
if page_size_index >= 80:
page_size_index = 0
page_index += 1
else:
page_size_index += 1
由於不確定,其說明文件所表示的一個Page最大100檔,會不會與重複使用同一Page相衝突,比方說同一Page的內容覆蓋超過100次會發生什麼事等等。這邊的作法是,每抓取81檔就換新的一頁,因為有Page編號上限問題,除非欲抓取的資料超過81 * 49 = 3969檔,否則可以先這樣處理。(這邊有另外進行篩選資料,只是簡單做個處理,各位可以依照自己的需求進行篩選,或者調整篩選方法)
最後,進行離線的動作:
if __name__ == '__main__':
...
LeaveMonitor()
SKQuoteLibEventHandler.disconnect()
結論
雖然還有幾個相關事件尚未進行處理,但本篇基本上已經把大部分會需要知道的概念跟相關實作提及了,所以先進行一個總結。
本篇主要介紹到了如何註冊事件、接收事件的訊息、報價物件的部分函式功能以及報價伺服器的連線與離線等等。最後,我們也成功取得了事件回傳的訊息。因此,歷史Tick回補等等功能,將會更容易去實作,基本上只差要去理解那複雜的群益API的官方使用說明。
群益api官方使用說明真的超複雜,謝謝大大,有第三篇嗎
回覆刪除感謝您的回覆,由於目前比較忙,所以很多想法未能分享出來,如果回響更大,或是有機會會再更新。
刪除真的很感謝您,交會了我很多!!!
回覆刪除小弟想問個問題就是上面有提到"skQ是上一篇我們在進行環境初始化時,設定的一個報價物件,這邊是直接使用全域變數,就不另行以傳入參數的形式了"
我想要建立兩個報價物件該怎麼做呢?我試了很多方法都是不出來,還想請大大開示!! 謝謝
理論是我已經登入第二條報價行情線了
刪除但是我的skQ.SKQuoteLib_RequestStock()就只會傳入第一條行情線
想跟J大討教一下
依照目前版本可以理解為一個人最多有兩條連線,而這個人可以開啟國內與海外期貨帳戶。
刪除在這樣的條件之下,可以有幾種搭配方式,就分兩大類吧。
1. 兩條連線都是國內,或者都是海外
2. 兩條連線分別是國內以及海外
最簡單且直觀的方式,就是直接分開執行,執行的程序不同,連線出去,不會互相衝突,因此可以達成兩個執行的程式各自占用一條國內連線(也就是第一種分類,第一種分類可以通,第二種分類也能通)。
若是你要單跑一支程式就能連兩條行情線,就得要建立另一個skQ2(舉例),同樣要有事件的註冊,監控連線等等,用來控制第二條聯線,並且應該是只能適用第二種分類,不然會互相衝突,認證不會過。
感謝J大回復,小弟在努力試試看!!
刪除抱歉,手機怪怪的回覆到了3次⋯⋯
回覆刪除想請問J大 為何在取得商品列表後還要再執行 run_callback_sync 不是一直連線中嗎
回覆刪除Dear 石方,
刪除在定義run_callback_sync函式時,有提到我們需要自己去從系統中的Message Queue中去接收訊息,不僅僅是為了連線。