# -*- 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()