iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 15
3
Software Development

Python 程式交易 30 天新手入門系列 第 15

Day-15 券商串接:串接元大期貨行情 API(二)

安裝所需套件

使用 pip 安裝

# ASCII Chart
pip install asciichartpy

終端機輸出控制

實現終端機下的簡易版看盤軟體。

import json
import os
import re
import sys

# 停用 loguru 的 stdout 輸出
os.environ['LOGURU_AUTOINIT'] = 'False'

import ctypes
import comtypes
import comtypes.client
import datetime
import dateutil.relativedelta
import loguru
import wcwidth
import wx

class YuantaQuoteAXCtrl:
    def __init__(self, parent):
        self.parent = parent

        container = ctypes.POINTER(comtypes.IUnknown)()
        control = ctypes.POINTER(comtypes.IUnknown)()
        guid = comtypes.GUID()
        sink = ctypes.POINTER(comtypes.IUnknown)()

        ctypes.windll.atl.AtlAxCreateControlEx(
            'YUANTAQUOTE.YuantaQuoteCtrl.1',
            self.parent.Handle,
            None,
            ctypes.byref(container),
            ctypes.byref(control),
            ctypes.byref(guid),
            sink
        )
        self.ctrl = comtypes.client.GetBestInterface(control)
        self.sink = comtypes.client.GetEvents(self.ctrl, self)

        self.Host = None
        self.Port = None
        self.Username = None
        self.Password = None

        # 儲存繪圖點位
        self.Series = []

    def Config(self, host, port, username, password):
        self.Host = host
        self.Port = port
        self.Username = username
        self.Password = password

    def XXF(self):
        today = datetime.date.today()
        day = datetime.date.today().replace(day=1)
        while day.weekday() != 2:
            day = day + datetime.timedelta(days=1)
        day = day + dateutil.relativedelta.relativedelta(days=14)
        if day < today:
            day = day + dateutil.relativedelta.relativedelta(months=1)
        codes = [
            'A',
            'B',
            'C',
            'D',
            'E',
            'F',
            'G',
            'H',
            'I',
            'J',
            'K',
            'L'
        ]
        y = day.year % 10
        m = codes[day.month - 1]
        return f'{m}{y}'

    def TXF(self):
        return f'TXF{self.XXF()}'

    def Logon(self):
        self.ctrl.SetMktLogon(
            self.Username,
            self.Password,
            self.Host,
            self.Port,
            1,
            0
        )

    # 省略未使用事件函數

    def OnGetMktAll(self,
        symbol,
        refPri,
        openPri,
        highPri,
        lowPri,
        upPri,
        dnPri,
        matchTime,
        matchPri,
        matchQty,
        tolMatchQty,
        bestBuyQty,
        bestBuyPri,
        bestSellQty,
        bestSellPri,
        fdbPri,
        fdbQty,
        fdsPri,
        fdsQty,
        reqType):
        # 因畫面寬度關係,僅保存最後 165 個點位
        self.Series.append(float(matchPri))
        if len(self.Series) > 165:
            self.Series = self.Series[1:]

        now = datetime.datetime.now()
        clX = f' {now:%H:%M:%S.%f} '
        cl01 = f' {symbol: >6} '
        cl02 = f' {reqType: >4} '
        cl03 = f' {refPri: >10} '
        cl04 = f' {openPri: >10} '
        cl05 = f' {highPri: >10} '
        cl06 = f' {lowPri: >10} '
        cl07 = f' {upPri: >10} '
        cl08 = f' {dnPri: >10} '
        cl09 = f' {matchTime: >15} '
        if matchTime:
            cl09 = f' {matchTime[:2]}:{matchTime[2:4]}:{matchTime[4:6]}.{matchTime[6:]} '
        cl10 = f' {matchPri: >10} '
        cl11 = f' {matchQty: >8} '
        cl12 = f' {tolMatchQty: >8} '

        # 產生標題行
        date = f'{datetime.date.today():%Y-%m-%d}'
        title = '                                                                                        MatchPrice                                                                              '
        # 如果有二個點以上,則產生折線圖
        # 原因是 asciichartpy 套件的 Y 軸需要二個點相減來產生,不然會發生例外
        chart = ''
        if len(set(self.Series)) > 1:
            chart = asciichartpy.plot(self.Series, {
                'width': 116,
                'height': 30
            }) 
        sys.stdout.write(
            '\033[2J' +
            f'          元大期貨                                                                                                                                                    {  date  }\n' +
            f'          ----------------------------------------------------------------------------------------------------------------------------------------------------------------------\n' +
            f'          |        接收時間 |   代碼 | 盤別 |     參考價 |     開盤價 |     最高價 |     最低價 |     漲停價 |     跌停價 |        成交時間 |   成交價位 | 成交數量 | 總成交量 |\n' +
            f'          ----------------------------------------------------------------------------------------------------------------------------------------------------------------------\n' +
            f'          |{           clX}|{  cl01}|{cl02}|{      cl03}|{      cl04}|{      cl05}|{      cl06}|{      cl07}|{      cl08}|{           cl09}|{      cl10}|{    cl11}|{    cl12}|\n' +
            f'          ----------------------------------------------------------------------------------------------------------------------------------------------------------------------\n' +
            f'\n' +
            f'{title}\n' +
            f'\n' +
            f'{chart}\n'
        )

    def OnMktStatusChange(self,
        status,
        msg,
        reqType):
        code = ''
        find = re.match(r'^(?P<code>\d).+$', msg)
        if find:
            code = msg[0]
            msg = msg[1:]
        if msg.endswith('!'):
            msg = msg[:-1]
        msg = msg + ' ' * (50 - wcwidth.wcswidth(msg))
        clX = f' {datetime.datetime.now():%H:%M:%S.%f} '
        cl01 = f' {reqType: >7} '
        cl02 = f' {status: >6} '
        cl03 = f' {code: >4} '
        cl04 = f' {msg} '

        loguru.logger.info(
            f'OnMktStatusChange\n' +
            f'--------------------------------------------------------------------------------------------------\n' +
            f'|                 | reqType | status | code | msg                                                |\n' +
            f'--------------------------------------------------------------------------------------------------\n' +
            f'|{            clX}|{   cl01}|{  cl02}|{cl03}|{cl04                                              }|\n' +
            f'--------------------------------------------------------------------------------------------------\n'
        )

        if status != 2:
            return

        code = self.TXF()
        loguru.logger.info(f'OnMktStatusChange: AddMktReg(code={code}, 4, reqType={reqType}, 0)\n')
        result = self.ctrl.AddMktReg(code, 4, reqType, 0)
        loguru.logger.info(f'OnMktStatusChange: AddMktReg(code={code}, 4, reqType={reqType}, 0) => {result}\n')

    def OnGetTimePack(self,
        strTradeType,
        strTime,
        reqType):
        clX = f' {datetime.datetime.now():%H:%M:%S.%f} '
        cl01 = f' {reqType: >7} '
        cl02 = f' {strTime[:2]}:{strTime[2:4]}:{strTime[4:6]}.{strTime[6:]} '
        cl03 = f' {strTradeType: >12} '
        loguru.logger.info(
            f'OnGetTimePack\n' +
            f'--------------------------------------------------------------\n' +
            f'|                 | reqType |         strTime | strTradeType |\n' +
            f'--------------------------------------------------------------\n' +
            f'|{            clX}|{   cl01}|{           cl02}|{        cl03}|\n' +
            f'--------------------------------------------------------------\n'
        )

    # 省略未使用事件函數

def main():
    app = wx.App()

    frame = wx.Frame(
        parent=None,
        id=wx.ID_ANY,
        title='Yuanta.Quote'
    )
    frame.Hide()

    quote = YuantaQuoteAXCtrl(frame)
    quote.Config(
        host='<Host>',
        port='<Port>',
        username='<Username>',
        password='<Password>'
    )
    quote.Logon()

    app.MainLoop()

if __name__ == '__main__':
    loguru.logger.add(
        f'{datetime.date.today():%Y%m%d}.log',
        rotation='1 day',
        retention='7 days',
        level='DEBUG'
    )
    main()

執行結果
執行結果
因為目前是每個 Tick 畫一個點,若改成 1 分均線或 5 分均線,就能畫出較為近似一般看到的線圖,這會在後續的文章中實作。


團隊系列文:

CSScoke - 金魚都能懂的這個網頁畫面怎麼切 - 金魚都能懂了你還怕學不會嗎
Clarence - LINE bot 好好玩 30 天玩轉 LINE API
Hina Hina - 陣列大亂鬥
King Tzeng - IoT沒那麼難!新手用JavaScript入門做自己的玩具
Vita Ora - 好 Js 不學嗎 !? JavaScript 入門中的入門。
TaTaMo - 用Python開發的網頁不能放到Github上?Lektor說可以!!


上一篇
Day-14 券商串接:串接元大期貨行情 API(一)
下一篇
Day-16 券商串接:串接元大期貨行情 API(三)
系列文
Python 程式交易 30 天新手入門30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 則留言

0
大歐派蘿莉
iT邦新手 2 級 ‧ 2019-10-01 16:04:07

感謝樓主的這份教學
實際跑了一下發現 Line 154 接收時間下面那個變數應該是 clX 才對~
如果 log 檔裡面有亂碼,要在 loguru.logger.add 增加一個參數 encoding='UTF-8'

阿斬 iT邦新手 5 級 ‧ 2019-10-01 21:47:46 檢舉

感謝大大提醒,忘了改到 Markdown 裡面 XD

阿斬 iT邦新手 5 級 ‧ 2019-10-01 21:49:33 檢舉

不加 encoding='UTF-8' 是因為有些資料進來是 Big5,所以這個就留給文字編輯器去自行判斷,我用的 EmEditor 能有效解決這問題

0
KWJP
iT邦新手 5 級 ‧ 2020-03-07 09:15:58

感謝提供教學.
實際跑了後發現產生出來的code(這個月為TXFC0)白天可抓到期貨資料,但下午3點後跑只會回傳白天最後一筆資料,請問如果要抓3點後的PM期貨資料,code產生的邏輯是否有不同?謝謝

[2020/3/11]不好意思,已跟元大問到答案,PM盤要連另外一個port跟request type.打擾了:)

阿斬 iT邦新手 5 級 ‧ 2020-04-21 18:54:29 檢舉

恭喜解決,沒注意到被登出,所以沒收到提醒

我要留言

立即登入留言