利用python3.5 +TK 開發股票自動交易伴侶

# -*- encoding: utf8 -*-
# version 1.11

import tkinter.messagebox,os
from tkinter import *
from tkinter.ttk import *
from tkinter import Menu
import datetime
import threading
import pickle
import time
import tushare as ts
import pywinauto
import pywinauto.clipboard
import pywinauto.application
NUM_OF_STOCKS = 5  # 自定義股票數量
is_start = False
is_monitor = True
set_stocks_info = []
actual_stocks_info = []
consignation_info = []
is_ordered = [1] * NUM_OF_STOCKS  # 1:未下單  0:已下單
is_dealt = [0] * NUM_OF_STOCKS  # 0: 未成交   負整數:賣出數量, 正整數:買入數量
stock_codes = [''] * NUM_OF_STOCKS

class OperationThs:
    def __init__(self):
        try:
            self.__app = pywinauto.application.Application()
            self.__app.connect(title='網上股票交易系統5.0')
            top_hwnd = pywinauto.findwindows.find_window(title='網上股票交易系統5.0')
            dialog_hwnd = pywinauto.findwindows.find_windows(top_level_only=False, class_name='#32770', parent=top_hwnd)[0]
            wanted_hwnds = pywinauto.findwindows.find_windows(top_level_only=False, parent=dialog_hwnd)
            print('wanted_hwnds length', len(wanted_hwnds))
            if len(wanted_hwnds) not in (99,97,96,98,100,101):
                tkinter.messagebox.showerror('錯誤', '沒法得到「同花順雙向委託界面」的窗口句柄,請將同花順交易系統切換到「雙向委託界面」!')
                exit()
            self.__main_window = self.__app.window_(handle=top_hwnd)
            self.__dialog_window = self.__app.window_(handle=dialog_hwnd)
        except:
            pass
    def __buy(self, code, quantity):
        """買函數
        :param code: 代碼, 字符串
        :param quantity: 數量, 字符串
        """
        self.__dialog_window.Edit1.SetFocus()
        time.sleep(0.2)
        self.__dialog_window.Edit1.SetEditText(code)
        time.sleep(0.2)
        if quantity != '0':
            self.__dialog_window.Edit3.SetEditText(quantity)
            time.sleep(0.2)
        self.__dialog_window.Button1.Click()
        time.sleep(0.2)
    def __sell(self, code, quantity):
        """
        賣函數
        :param code: 股票代碼, 字符串
        :param quantity: 數量, 字符串
        """
        self.__dialog_window.Edit4.SetFocus()
        time.sleep(0.2)
        self.__dialog_window.Edit4.SetEditText(code)
        time.sleep(0.2)
        if quantity != '0':
            self.__dialog_window.Edit6.SetEditText(quantity)
            time.sleep(0.2)
        self.__dialog_window.Button2.Click()
        time.sleep(0.2)
    def __closePopupWindow(self):
        """
        關閉一個彈窗。
        :return: 若是有彈出式對話框,返回True,不然返回False
        """
        popup_hwnd = self.__main_window.PopupWindow()
        if popup_hwnd:
            popup_window = self.__app.window_(handle=popup_hwnd)
            popup_window.SetFocus()
            popup_window.Button.Click()
            return True
        return False

    def __closePopupWindows(self):
        """
        關閉多個彈出窗口
        :return:
        """
        while self.__closePopupWindow():
            time.sleep(0.5)
    def order(self, code, direction, quantity):
        """
        下單函數
        :param code: 股票代碼, 字符串
        :param direction: 買賣方向, 字符串
        :param quantity: 買賣數量, 字符串
        """
        if direction == 'B':
            self.__buy(code, quantity)
        if direction == 'S':
            self.__sell(code, quantity)
        self.__closePopupWindows()
    def maxWindow(self):
        """
        最大化窗口
        """
        if self.__main_window.GetShowState() != 3:

            self.__main_window.Maximize()
        self.__main_window.SetFocus()

    def minWindow(self):
        """
        最小化窗體
        """
        if self.__main_window.GetShowState() != 2:
            self.__main_window.Minimize()

    def refresh(self, t=0.5):
        """
        點擊刷新按鈕
        :param t:刷新後的等待時間
        """
        self.__dialog_window.Button5.Click()
        time.sleep(t)
    def getMoney(self):
        """
        獲取可用資金
        """
        return float(self.__dialog_window.Static19.WindowText())
    @staticmethod
    def __cleanClipboardData(data, cols=11):
        """
        清洗剪貼板數據
        :param data: 數據
        :param cols: 列數
        :return: 清洗後的數據,返回列表
        """
        lst = data.strip().split()[:-1]
        matrix = []
        for i in range(0, len(lst) // cols):
            matrix.append(lst[i * cols:(i + 1) * cols])
        return matrix[1:]

    def __copyToClipboard(self):
        """
        拷貝持倉信息至剪貼板
        :return:
        """
        self.__dialog_window.CVirtualGridCtrl.RightClick(coords=(30, 30))
        self.__main_window.TypeKeys('C')

    def __getCleanedData(self):
        """
        讀取ListView中的信息
        :return: 清洗後的數據
        """
        self.__copyToClipboard()
        data = pywinauto.clipboard.GetData()
        return self.__cleanClipboardData(data)

    def __selectWindow(self, choice):
        """
        選擇tab窗口信息
        :param choice: 選擇個標籤頁。持倉,撤單,委託,成交
        :return:
        """
        rect = self.__dialog_window.CCustomTabCtrl.ClientRect()
        x = rect.width() // 8
        y = rect.height() // 2
        if choice == 'W':
            x = x
        elif choice == 'E':
            x *= 3
        elif choice == 'R':
            x *= 5
        elif choice == 'A':
            x *= 7
        self.__dialog_window.CCustomTabCtrl.ClickInput(coords=(x, y))
        time.sleep(0.5)

    def __getInfo(self, choice):
        """
        獲取股票信息
        """
        self.__selectWindow(choice=choice)
        return self.__getCleanedData()

    def getPosition(self):
        """
        獲取持倉
        :return:
        """
        return self.__getInfo(choice='W')
    @staticmethod
    def getDeal(code, pre_position, cur_position):
        """
        獲取成交數量
        :param code: 需檢查的股票代碼, 字符串
        :param pre_position: 下單前的持倉
        :param cur_position: 下單後的持倉
        :return: 0-未成交, 正整數是買入的數量, 負整數是賣出的數量
        """
        if pre_position == cur_position:
            return 0
        pre_len = len(pre_position)
        cur_len = len(cur_position)
        if pre_len == cur_len:
            for row in range(cur_len):
                if cur_position[row][0] == code:
                    return int(float(cur_position[row][1]) - float(pre_position[row][1]))
        if cur_len > pre_len:
            return int(float(cur_position[-1][1]))

    def withdraw(self, code, direction):
        """
        指定撤單
        :param code: 股票代碼
        :param direction: 方向 B, S
        :return:
        """
        row_pos = []
        info = self.__getInfo(choice='R')
        if direction == 'B':
            direction = '買入'
        elif direction == 'S':
            direction = '賣出'
        if info:
            for index, element in enumerate(info):
                if element[0] == code:
                    if element[1] == direction:
                        row_pos.append(index)
        if row_pos:
            for row in row_pos:
                self.__dialog_window.CVirtualGridCtrl.ClickInput(coords=(7, 28 + 16 * row))
            self.__dialog_window.Button12.Click()
            self.__closePopupWindows()

    def withdrawBuy(self):
        """
        撤買
        :return:
        """
        self.__selectWindow(choice='R')
        self.__dialog_window.Button8.Click()
        self.__closePopupWindows()

    def withdrawSell(self):
        """
        撤賣
        :return:
        """
        self.__selectWindow(choice='R')
        self.__dialog_window.Button9.Click()
        self.__closePopupWindows()

    def withdrawAll(self):
        """
        全撤
        :return:
        """
        self.__selectWindow(choice='R')
        self.__dialog_window.Button7.Click()
        self.__closePopupWindows()

def getStockData():
    """
    獲取股票實時數據
    :return:股票實時數據
    """
    global stock_codes
    code_name_price = []
    try:
        df = ts.get_realtime_quotes(stock_codes)
        df_len = len(df)
        for stock_code in stock_codes:
            is_found = False
            for i in range(df_len):
                actual_code = df['code'][i]
                if stock_code == actual_code:
                    code_name_price.append((actual_code, df['name'][i], float(df['price'][i])))
                    is_found = True
                    break
            if is_found is False:
                code_name_price.append(('', '', 0))
    except:
        code_name_price = [('', '', 0)] * NUM_OF_STOCKS  # 網絡不行,返回空
    return code_name_price
def monitor():
    """
    實時監控函數
    """
    global actual_stocks_info, consignation_info, is_ordered, is_dealt, set_stocks_info
    count = 1
    pre_position = []
    try:
        operation = OperationThs()
        operation.maxWindow()
        pre_position = operation.getPosition()
        # print(pre_position)
        while is_monitor:
            if is_start:
                actual_stocks_info = getStockData()
                for row, (actual_code, actual_name, actual_price) in enumerate(actual_stocks_info):
                    if actual_code and is_start and is_ordered[row] == 1 and actual_price > 0 \
                            and set_stocks_info[row][1] and set_stocks_info[row][2] > 0 \
                            and set_stocks_info[row][3] and set_stocks_info[row][4] \
                            and datetime.datetime.now().time() > set_stocks_info[row][5]:
                        if (set_stocks_info[row][1] == '>' and actual_price > set_stocks_info[row][2]) or \
                                (set_stocks_info[row][1] == '<' and float(actual_price) < set_stocks_info[row][2]):
                            operation.maxWindow()
                            operation.order(actual_code, set_stocks_info[row][3], set_stocks_info[row][4])
                            dt = datetime.datetime.now()
                            is_ordered[row] = 0
                            operation.refresh()
                            cur_position = operation.getPosition()
                            is_dealt[row] = operation.getDeal(actual_code, pre_position, cur_position)
                            consignation_info.append(
                                (dt.strftime('%x'), dt.strftime('%X'), actual_code,
                                 actual_name, set_stocks_info[row][3],
                                 actual_price, set_stocks_info[row][4], '已委託', is_dealt[row]))
                            pre_position = cur_position
            if count % 200 == 0:
                operation.refresh()
            time.sleep(3)
            count += 1

    except:
        tkinter.messagebox.showerror('錯誤', '請先打開「同花順雙向委託界面」後在打開自動交易系統!')
        sys.exit()

class StockGui:
    global is_monitor
    def __init__(self):
        self.window = Tk()
        self.window.title("自動化交易系統-同花順")
        # 左上角圖標
        self.window.iconbitmap('e:\ico.ico')
        self.window.resizable(0, 0)
        frame1 = Frame(self.window)
        frame1.pack(padx=10, pady=10)
        Label(frame1, text="股票代碼", width=8, justify=CENTER).grid(
            row=1, column=1, padx=5, pady=5)
        Label(frame1, text="股票名稱", width=8, justify=CENTER).grid(
            row=1, column=2, padx=5, pady=5)
        Label(frame1, text="實時價格", width=8, justify=CENTER).grid(
            row=1, column=3, padx=5, pady=5)
        Label(frame1, text="關係", width=4, justify=CENTER).grid(
            row=1, column=4, padx=5, pady=5)
        Label(frame1, text="設訂價格", width=8, justify=CENTER).grid(
            row=1, column=5, padx=5, pady=5)
        Label(frame1, text="方向", width=4, justify=CENTER).grid(
            row=1, column=6, padx=5, pady=5)
        Label(frame1, text="數量", width=8, justify=CENTER).grid(
            row=1, column=7, padx=5, pady=5)
        Label(frame1, text="時間可選", width=8, justify=CENTER).grid(
            row=1, column=8, padx=5, pady=5)
        Label(frame1, text="委託", width=6, justify=CENTER).grid(
            row=1, column=9, padx=5, pady=5)
        Label(frame1, text="成交", width=6, justify=CENTER).grid(
            row=1, column=10, padx=5, pady=5)
        self.rows = NUM_OF_STOCKS
        self.cols = 10
        self.variable = []
        for row in range(self.rows):
            self.variable.append([])
            for col in range(self.cols):
                self.variable[row].append(StringVar())
        for row in range(self.rows):
            Entry(frame1, textvariable=self.variable[row][0],
                  width=8).grid(row=row + 2, column=1, padx=5, pady=5)
            Entry(frame1, textvariable=self.variable[row][1], state=DISABLED,
                  width=8).grid(row=row + 2, column=2, padx=5, pady=5)
            Entry(frame1, textvariable=self.variable[row][2], state=DISABLED, justify=RIGHT,
                  width=8).grid(row=row + 2, column=3, padx=5, pady=5)
            Combobox(frame1, values=('<', '>'), textvariable=self.variable[row][3],
                     width=2).grid(row=row + 2, column=4, padx=5, pady=5)
            Spinbox(frame1, from_=0, to=999, textvariable=self.variable[row][4], justify=RIGHT,
                    increment=0.01, width=6).grid(row=row + 2, column=5, padx=5, pady=5)
            Combobox(frame1, values=('B', 'S'), textvariable=self.variable[row][5],
                     width=2).grid(row=row + 2, column=6, padx=5, pady=5)
            Spinbox(frame1, from_=0, to=10000, textvariable=self.variable[row][6], justify=RIGHT,
                    increment=100, width=6).grid(row=row + 2, column=7, padx=5, pady=5)
            Entry(frame1, textvariable=self.variable[row][7],
                  width=8).grid(row=row + 2, column=8, padx=5, pady=5)
            Entry(frame1, textvariable=self.variable[row][8], state=DISABLED, justify=CENTER,
                  width=6).grid(row=row + 2, column=9, padx=5, pady=5)
            Entry(frame1, textvariable=self.variable[row][9], state=DISABLED, justify=RIGHT,
                  width=6).grid(row=row + 2, column=10, padx=5, pady=5)

        frame3 = Frame(self.window)
        frame3.pack(padx=10, pady=10)
         # 建立菜單功能
        self.menuBar = Menu(self.window)
        self.window.config(menu=self.menuBar)
        # tearoff=0 表明將菜單項最上面的一條虛線去掉,默認是存在的
        self.fileMenu = Menu(self.menuBar,tearoff=0)
        # 建立一個名爲「幫助」的菜單項
        self.menuBar.add_cascade(label="幫助",menu=self.fileMenu)
        # 在「幫助」項下添加一個名爲「關於」的選項
        self.fileMenu.add_command(label="關於",command =self.about)
        # 增長一條橫線
        self.fileMenu.add_separator()
        # 在「幫助」項下添加一個名爲「退出」的選項,並綁定執行函數
        self.fileMenu.add_command(label="退出",command=self.close)
        # 增長第二個導航欄
        # self.helpMenu = Menu(self.menuBar,tearoff=0)
        # self.menuBar.add_cascade(label="Help", menu=self.helpMenu)
        # self.helpMenu.add_command(label="About")
        self.start_bt = Button(frame3, text="開始", command=self.start)
        self.start_bt.pack(side=LEFT)
        self.set_bt = Button(frame3, text='重置買賣', command=self.setFlags)
        self.set_bt.pack(side=LEFT)
        Button(frame3, text="歷史記錄", command=self.displayHisRecords).pack(side=LEFT)
        Button(frame3, text='保存', command=self.save).pack(side=LEFT)
        self.load_bt = Button(frame3, text='載入', command=self.load)
        self.load_bt.pack(side=LEFT)
        self.window.protocol(name="WM_DELETE_WINDOW", func=self.close)
        self.window.after(100, self.updateControls)
        self.window.mainloop()

    def displayHisRecords(self):
        """
        顯示歷史信息
        """
        global consignation_info
        tp = Toplevel()
        tp.title('歷史記錄')
        tp.iconbitmap('e:\ico.ico')
        tp.resizable(0, 1)
        scrollbar = Scrollbar(tp)
        scrollbar.pack(side=RIGHT, fill=Y)
        col_name = ['日期', '時間', '證券代碼', '證券名稱', '方向', '價格', '數量', '委託', '成交']
        tree = Treeview(
            tp, show='headings', columns=col_name, height=30, yscrollcommand=scrollbar.set)
        tree.pack(expand=1, fill=Y)
        scrollbar.config(command=tree.yview)
        for name in col_name:
            tree.heading(name, text=name)
            tree.column(name, width=70, anchor=CENTER)

        for msg in consignation_info:
            tree.insert('', 0, values=msg)
    def save(self):
        """
        保存設置
        """
        global set_stocks_info, consignation_info
        self.getItems()
        with open('stockInfo.dat', 'wb') as fp:
            pickle.dump(set_stocks_info, fp)
            pickle.dump(consignation_info, fp)
    def load(self):
        """
        載入設置
        """
        global set_stocks_info, consignation_info
        try:
            with open('stockInfo.dat', 'rb') as fp:
                set_stocks_info = pickle.load(fp)
                consignation_info = pickle.load(fp)
            for row in range(self.rows):
                for col in range(self.cols):
                    if col == 0:
                        self.variable[row][col].set(set_stocks_info[row][0])
                    elif col == 3:
                        self.variable[row][col].set(set_stocks_info[row][1])
                    elif col == 4:
                        self.variable[row][col].set(set_stocks_info[row][2])
                    elif col == 5:
                        self.variable[row][col].set(set_stocks_info[row][3])
                    elif col == 6:
                        self.variable[row][col].set(set_stocks_info[row][4])
                    elif col == 7:
                        temp = set_stocks_info[row][5].strftime('%X')
                        if temp == '01:00:00':
                            self.variable[row][col].set('')
                        else:
                            self.variable[row][col].set(temp)
        except Exception :
            tkinter.messagebox.showerror('錯誤', "沒有找到配置保存文件,請先進行股票買賣配置信息保存!")
    def setFlags(self):
        """
        重置買賣標誌
        """
        global is_start, is_ordered
        if is_start is False:
            is_ordered = [1] * NUM_OF_STOCKS
            tkinter.messagebox.showinfo('重置成功', "重置成功!")
    def updateControls(self):
        """
        實時股票名稱、價格、狀態信息
        """
        global actual_stocks_info, is_start
        if is_start:
            for row, (actual_code, actual_name, actual_price) in enumerate(actual_stocks_info):
                if actual_code:
                    self.variable[row][1].set(actual_name)
                    self.variable[row][2].set(str(actual_price))
                    if is_ordered[row] == 1:
                        self.variable[row][8].set('監控中')
                    elif is_ordered[row] == 0:
                        self.variable[row][8].set('已委託')
                    self.variable[row][9].set(str(is_dealt[row]))
                else:
                    self.variable[row][1].set('')
                    self.variable[row][2].set('')
                    self.variable[row][8].set('')
                    self.variable[row][9].set('')
        self.window.after(3000, self.updateControls)
    @staticmethod
    def __pickCodeFromItems(items_info):
        """
        提取股票代碼
        :param items_info: UI下各項輸入信息
        :return:股票代碼列表
        """
        stock_codes = []
        for item in items_info:
            stock_codes.append(item[0])
        return stock_codes

    def start(self):
        """
        啓動中止
        """
        global is_start, stock_codes, set_stocks_info
        if is_start is False:
            is_start = True
        else:
            is_start = False

        if is_start:
            self.getItems()
            stock_codes = self.__pickCodeFromItems(set_stocks_info)
            self.start_bt['text'] = '中止'
            self.set_bt['state'] = DISABLED
            self.load_bt['state'] = DISABLED
            tkinter.messagebox.showinfo('成功','啓動成功!')
        else:
            self.start_bt['text'] = '開始'
            self.set_bt['state'] = NORMAL
            self.load_bt['state'] = NORMAL
    def about(self):
        tkinter.messagebox.showinfo("關於",'\r此係統僅適應於同花順網上交易5.0,使用時請先登錄同花順網上交易系統並切換到「同花順雙向委託界面」。\r 版本號:v 1.0.0 \r 做者:水域\r 發佈日期:2017.01.11')
    def close(self):
        """
        關閉程序時,中止monitor線程
        """
        global is_monitor
        is_monitor = False
        self.window.quit()

    def getItems(self):
        """
        獲取UI上用戶輸入的各項數據,
        """
        global set_stocks_info
        set_stocks_info = []
        # 獲取買賣價格數量輸入項等
        for row in range(self.rows):
            set_stocks_info.append([])
            for col in range(self.cols):
                temp = self.variable[row][col].get().strip()
                if col == 0:
                    if len(temp) == 6 and temp.isdigit():  # 判斷股票代碼是否爲6位數
                        set_stocks_info[row].append(temp)
                    else:
                        set_stocks_info[row].append('')
                elif col == 3:
                    if temp in ('>', '<'):
                        set_stocks_info[row].append(temp)
                    else:
                        set_stocks_info[row].append('')
                elif col == 4:
                    try:
                        price = float(temp)
                        if price > 0:
                            set_stocks_info[row].append(price)  # 把價格轉爲數字
                        else:
                            set_stocks_info[row].append(0)
                    except ValueError:
                        set_stocks_info[row].append(0)
                elif col == 5:
                    if temp in ('B', 'S'):
                        set_stocks_info[row].append(temp)
                    else:
                        set_stocks_info[row].append('')
                elif col == 6:
                    if temp.isdigit() and int(temp) >= 0:
                        set_stocks_info[row].append(str(int(temp) // 100 * 100))
                    else:
                        set_stocks_info[row].append('')
                elif col == 7:
                    try:
                        set_stocks_info[row].append(datetime.datetime.strptime(temp, '%H:%M:%S').time())
                    except ValueError:
                        set_stocks_info[row].append(datetime.datetime.strptime('1:00:00', '%H:%M:%S').time())

if __name__ == '__main__':
    # StockGui()
    t1 = threading.Thread(target=StockGui)
    t1.start()
    t2 = threading.Thread(target=monitor)
    t2.start()
相關文章
相關標籤/搜索