|
| 1 | +# coding:utf8 |
| 2 | +from __future__ import division |
| 3 | +import sys |
| 4 | +import os |
| 5 | +import subprocess |
| 6 | +import tempfile |
| 7 | +import time |
| 8 | +import traceback |
| 9 | +import win32api |
| 10 | +import win32gui |
| 11 | +from io import StringIO |
| 12 | + |
| 13 | +import pandas as pd |
| 14 | +import pyperclip |
| 15 | +import win32com.client |
| 16 | +import win32con |
| 17 | +from PIL import ImageGrab |
| 18 | + |
| 19 | +from . import helpers |
| 20 | +from .log import log |
| 21 | + |
| 22 | + |
| 23 | +def findWindowSubwindowEqualText(tt): |
| 24 | + hwnds = [] |
| 25 | + def findSub(hwnd,param): |
| 26 | + if win32gui.FindWindowEx(hwnd,0,None,tt)!=0: |
| 27 | + param.append(hwnd) |
| 28 | + win32gui.EnumWindows(findSub, hwnds) |
| 29 | + return hwnds |
| 30 | + |
| 31 | +class GJClientTrader(): |
| 32 | + def __init__(self): |
| 33 | + self.LoginTitle = ['用户登录'] |
| 34 | + self.Title = '网上股票交易系统5.0' |
| 35 | + |
| 36 | + def prepare(self, config_path=None, user=None, password=None, exe_path='C:\\全能行证券交易终端\\xiadan.exe'): |
| 37 | + """ |
| 38 | + 登陆银河客户端 |
| 39 | + :param config_path: 银河登陆配置文件,跟参数登陆方式二选一 |
| 40 | + :param user: 银河账号 |
| 41 | + :param password: 银河明文密码 |
| 42 | + :param exe_path: 银河客户端路径 |
| 43 | + :return: |
| 44 | + """ |
| 45 | + if config_path is not None: |
| 46 | + account = helpers.file2dict(config_path) |
| 47 | + user = account['user'] |
| 48 | + password = account['password'] |
| 49 | + self.login(user, password, exe_path) |
| 50 | + |
| 51 | + def login(self, user, password, exe_path): |
| 52 | + if self._has_main_window(): |
| 53 | + self._get_handles() |
| 54 | + log.info('检测到交易客户端已启动,连接完毕') |
| 55 | + return |
| 56 | + if not self._has_login_window(): |
| 57 | + if not os.path.exists(exe_path): |
| 58 | + raise FileNotFoundError('在 {} 未找到应用程序,请用 exe_path 指定应用程序目录'.format(exe_path)) |
| 59 | + subprocess.Popen(exe_path) |
| 60 | + # 检测登陆窗口 |
| 61 | + for _ in range(30): |
| 62 | + if self._has_login_window(): |
| 63 | + break |
| 64 | + time.sleep(1) |
| 65 | + else: |
| 66 | + raise Exception('启动客户端失败,无法检测到登陆窗口') |
| 67 | + log.info('成功检测到客户端登陆窗口') |
| 68 | + |
| 69 | + # 登陆 |
| 70 | + # self._set_trade_mode() |
| 71 | + self._set_login_name(user) |
| 72 | + self._set_login_password(password) |
| 73 | + for _ in range(10): |
| 74 | + self._set_login_verify_code() |
| 75 | + self._click_login_button() |
| 76 | + time.sleep(3) |
| 77 | + self._check_verify_code_wrong() |
| 78 | + if not self._has_login_window(): |
| 79 | + log.info('no login window, login success') |
| 80 | + break |
| 81 | + self._click_login_verify_code() |
| 82 | + |
| 83 | + for _ in range(60): |
| 84 | + if self._has_main_window(): |
| 85 | + self._get_handles() |
| 86 | + break |
| 87 | + time.sleep(1) |
| 88 | + else: |
| 89 | + raise Exception('启动交易客户端失败') |
| 90 | + log.info('客户端登陆成功') |
| 91 | + |
| 92 | + def _set_login_verify_code(self): |
| 93 | + verify_code_image = self._grab_verify_code() |
| 94 | + image_path = tempfile.mktemp() + '.jpg' |
| 95 | + verify_code_image.save(image_path) |
| 96 | + result = helpers.recognize_verify_code(image_path, 'gj_client') |
| 97 | + time.sleep(0.2) |
| 98 | + self._input_login_verify_code(result) |
| 99 | + time.sleep(0.4) |
| 100 | + |
| 101 | + def _set_trade_mode(self): |
| 102 | + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x4f4d) |
| 103 | + win32gui.SendMessage(input_hwnd, win32con.BM_CLICK, None, None) |
| 104 | + |
| 105 | + def _set_login_name(self, user): |
| 106 | + time.sleep(0.5) |
| 107 | + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3F3) |
| 108 | + win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, user) |
| 109 | + |
| 110 | + def _set_login_password(self, password): |
| 111 | + time.sleep(0.5) |
| 112 | + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3F4) |
| 113 | + win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, password) |
| 114 | + |
| 115 | + def _has_login_window(self): |
| 116 | + for title in self.LoginTitle: |
| 117 | + self.login_hwnd = win32gui.FindWindow(None, title) |
| 118 | + if self.login_hwnd != 0: |
| 119 | + return True |
| 120 | + return False |
| 121 | + |
| 122 | + def _input_login_verify_code(self, code): |
| 123 | + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3eb) |
| 124 | + win32gui.SendMessage(input_hwnd, win32con.WM_SETTEXT, None, code) |
| 125 | + |
| 126 | + def _click_login_verify_code(self): |
| 127 | + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x5db) |
| 128 | + rect = win32gui.GetWindowRect(input_hwnd) |
| 129 | + self._mouse_click(rect[0] + 5, rect[1] + 5) |
| 130 | + |
| 131 | + @staticmethod |
| 132 | + def _mouse_click(x, y): |
| 133 | + win32api.SetCursorPos((x, y)) |
| 134 | + win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, x, y, 0, 0) |
| 135 | + win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, x, y, 0, 0) |
| 136 | + |
| 137 | + def _click_login_button(self): |
| 138 | + time.sleep(1) |
| 139 | + input_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x3ee) |
| 140 | + win32gui.SendMessage(input_hwnd, win32con.BM_CLICK, None, None) |
| 141 | + |
| 142 | + def _check_verify_code_wrong(self): |
| 143 | + hwnds = findWindowSubwindowEqualText("提示") |
| 144 | + if len(hwnds)==0: |
| 145 | + log.info('Right verify code') |
| 146 | + elif len(hwnds)==1: |
| 147 | + win32gui.SetForegroundWindow(hwnds[0]) |
| 148 | + button = win32gui.FindWindowEx(hwnds[0],0,None,"确定") |
| 149 | + win32gui.SendMessage(button, win32con.BM_CLICK, None, None) |
| 150 | + log.info('Wrong verify code') |
| 151 | + else: |
| 152 | + raise Exception("_check_verify_code_wrong too many windows %d"%len(hwnds)) |
| 153 | + |
| 154 | + def _has_main_window(self): |
| 155 | + try: |
| 156 | + self._get_handles() |
| 157 | + except: |
| 158 | + return False |
| 159 | + return True |
| 160 | + |
| 161 | + def _grab_verify_code(self): |
| 162 | + verify_code_hwnd = win32gui.GetDlgItem(self.login_hwnd, 0x5db) |
| 163 | + self._set_foreground_window(self.login_hwnd) |
| 164 | + time.sleep(1) |
| 165 | + rect = win32gui.GetWindowRect(verify_code_hwnd) |
| 166 | + return ImageGrab.grab(rect) |
| 167 | + |
| 168 | + def _get_handles(self): |
| 169 | + trade_main_hwnd = win32gui.FindWindow(0, self.Title) # 交易窗口 |
| 170 | + operate_frame_hwnd = win32gui.GetDlgItem(trade_main_hwnd, 59648) # 操作窗口框架 |
| 171 | + operate_frame_afx_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59648) # 操作窗口框架 |
| 172 | + hexin_hwnd = win32gui.GetDlgItem(operate_frame_afx_hwnd, 129) |
| 173 | + scroll_hwnd = win32gui.GetDlgItem(hexin_hwnd, 200) # 左部折叠菜单控件 |
| 174 | + tree_view_hwnd = win32gui.GetDlgItem(scroll_hwnd, 129) # 左部折叠菜单控件 |
| 175 | + |
| 176 | + # 获取委托窗口所有控件句柄 |
| 177 | + win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F1, 0) |
| 178 | + time.sleep(0.5) |
| 179 | + |
| 180 | + # 买入相关 |
| 181 | + entrust_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 委托窗口框架 |
| 182 | + self.buy_stock_code_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1032) # 买入代码输入框 |
| 183 | + self.buy_price_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1033) # 买入价格输入框 |
| 184 | + self.buy_amount_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1034) # 买入数量输入框 |
| 185 | + self.buy_btn_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1006) # 买入确认按钮 |
| 186 | + self.refresh_entrust_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 32790) # 刷新持仓按钮 |
| 187 | + entrust_frame_hwnd = win32gui.GetDlgItem(entrust_window_hwnd, 1047) # 持仓显示框架 |
| 188 | + entrust_sub_frame_hwnd = win32gui.GetDlgItem(entrust_frame_hwnd, 200) # 持仓显示框架 |
| 189 | + self.position_list_hwnd = win32gui.GetDlgItem(entrust_sub_frame_hwnd, 1047) # 持仓列表 |
| 190 | + win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F2, 0) |
| 191 | + time.sleep(0.5) |
| 192 | + |
| 193 | + # 卖出相关 |
| 194 | + sell_entrust_frame_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 委托窗口框架 |
| 195 | + self.sell_stock_code_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1032) # 卖出代码输入框 |
| 196 | + self.sell_price_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1033) # 卖出价格输入框 |
| 197 | + self.sell_amount_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1034) # 卖出数量输入框 |
| 198 | + self.sell_btn_hwnd = win32gui.GetDlgItem(sell_entrust_frame_hwnd, 1006) # 卖出确认按钮 |
| 199 | + |
| 200 | + # 撤单窗口 |
| 201 | + win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F3, 0) |
| 202 | + time.sleep(0.5) |
| 203 | + cancel_entrust_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 59649) # 撤单窗口框架 |
| 204 | + self.cancel_stock_code_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 3348) # 卖出代码输入框 |
| 205 | + self.cancel_query_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 3349) # 查询代码按钮 |
| 206 | + self.cancel_buy_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 30002) # 撤买 |
| 207 | + self.cancel_sell_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 30003) # 撤卖 |
| 208 | + |
| 209 | + chexin_hwnd = win32gui.GetDlgItem(cancel_entrust_window_hwnd, 1047) |
| 210 | + chexin_sub_hwnd = win32gui.GetDlgItem(chexin_hwnd, 200) |
| 211 | + self.entrust_list_hwnd = win32gui.GetDlgItem(chexin_sub_hwnd, 1047) # 委托列表 |
| 212 | + |
| 213 | + # 资金股票 |
| 214 | + win32api.PostMessage(tree_view_hwnd, win32con.WM_KEYDOWN, win32con.VK_F4, 0) |
| 215 | + time.sleep(0.5) |
| 216 | + self.capital_window_hwnd = win32gui.GetDlgItem(operate_frame_hwnd, 0xE901) # 资金股票窗口框架 |
| 217 | + |
| 218 | + def balance(self): |
| 219 | + return self.get_balance() |
| 220 | + |
| 221 | + def get_balance(self): |
| 222 | + self._set_foreground_window(self.capital_window_hwnd) |
| 223 | + time.sleep(0.3) |
| 224 | + data = self._read_clipboard() |
| 225 | + return self.project_copy_data(data)[0] |
| 226 | + |
| 227 | + def buy(self, stock_code, price, amount, **kwargs): |
| 228 | + """ |
| 229 | + 买入股票 |
| 230 | + :param stock_code: 股票代码 |
| 231 | + :param price: 买入价格 |
| 232 | + :param amount: 买入股数 |
| 233 | + :return: bool: 买入信号是否成功发出 |
| 234 | + """ |
| 235 | + amount = str(amount // 100 * 100) |
| 236 | + price = str(price) |
| 237 | + |
| 238 | + try: |
| 239 | + win32gui.SendMessage(self.buy_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入买入代码 |
| 240 | + win32gui.SendMessage(self.buy_price_hwnd, win32con.WM_SETTEXT, None, price) # 输入买入价格 |
| 241 | + time.sleep(0.2) |
| 242 | + win32gui.SendMessage(self.buy_amount_hwnd, win32con.WM_SETTEXT, None, amount) # 输入买入数量 |
| 243 | + time.sleep(0.2) |
| 244 | + win32gui.SendMessage(self.buy_btn_hwnd, win32con.BM_CLICK, None, None) # 买入确定 |
| 245 | + time.sleep(0.3) |
| 246 | + except: |
| 247 | + traceback.print_exc() |
| 248 | + return False |
| 249 | + return True |
| 250 | + |
| 251 | + def sell(self, stock_code, price, amount, **kwargs): |
| 252 | + """ |
| 253 | + 买出股票 |
| 254 | + :param stock_code: 股票代码 |
| 255 | + :param price: 卖出价格 |
| 256 | + :param amount: 卖出股数 |
| 257 | + :return: bool 卖出操作是否成功 |
| 258 | + """ |
| 259 | + amount = str(amount // 100 * 100) |
| 260 | + price = str(price) |
| 261 | + |
| 262 | + try: |
| 263 | + win32gui.SendMessage(self.sell_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入卖出代码 |
| 264 | + win32gui.SendMessage(self.sell_price_hwnd, win32con.WM_SETTEXT, None, price) # 输入卖出价格 |
| 265 | + win32gui.SendMessage(self.sell_price_hwnd, win32con.BM_CLICK, None, None) # 输入卖出价格 |
| 266 | + time.sleep(0.2) |
| 267 | + win32gui.SendMessage(self.sell_amount_hwnd, win32con.WM_SETTEXT, None, amount) # 输入卖出数量 |
| 268 | + time.sleep(0.2) |
| 269 | + win32gui.SendMessage(self.sell_btn_hwnd, win32con.BM_CLICK, None, None) # 卖出确定 |
| 270 | + time.sleep(0.3) |
| 271 | + except: |
| 272 | + traceback.print_exc() |
| 273 | + return False |
| 274 | + return True |
| 275 | + |
| 276 | + def cancel_entrust(self, stock_code, direction): |
| 277 | + """ |
| 278 | + 撤单 |
| 279 | + :param stock_code: str 股票代码 |
| 280 | + :param direction: str 'buy' 撤买, 'sell' 撤卖 |
| 281 | + :return: bool 撤单信号是否发出 |
| 282 | + """ |
| 283 | + direction = 0 if direction == 'buy' else 1 |
| 284 | + |
| 285 | + try: |
| 286 | + win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 |
| 287 | + time.sleep(0.2) |
| 288 | + win32gui.SendMessage(self.cancel_stock_code_hwnd, win32con.WM_SETTEXT, None, stock_code) # 输入撤单 |
| 289 | + win32gui.SendMessage(self.cancel_query_hwnd, win32con.BM_CLICK, None, None) # 查询代码 |
| 290 | + time.sleep(0.2) |
| 291 | + if direction == 0: |
| 292 | + win32gui.SendMessage(self.cancel_buy_hwnd, win32con.BM_CLICK, None, None) # 撤买 |
| 293 | + elif direction == 1: |
| 294 | + win32gui.SendMessage(self.cancel_sell_hwnd, win32con.BM_CLICK, None, None) # 撤卖 |
| 295 | + except: |
| 296 | + traceback.print_exc() |
| 297 | + return False |
| 298 | + time.sleep(0.3) |
| 299 | + return True |
| 300 | + |
| 301 | + @property |
| 302 | + def position(self): |
| 303 | + return self.get_position() |
| 304 | + |
| 305 | + def get_position(self): |
| 306 | + win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 |
| 307 | + time.sleep(0.1) |
| 308 | + self._set_foreground_window(self.position_list_hwnd) |
| 309 | + time.sleep(0.1) |
| 310 | + data = self._read_clipboard() |
| 311 | + return self.project_copy_data(data) |
| 312 | + |
| 313 | + @staticmethod |
| 314 | + def project_copy_data(copy_data): |
| 315 | + reader = StringIO(copy_data) |
| 316 | + df = pd.read_csv(reader, sep = '\t') |
| 317 | + return df.to_dict('records') |
| 318 | + |
| 319 | + def _read_clipboard(self): |
| 320 | + for _ in range(15): |
| 321 | + try: |
| 322 | + win32api.keybd_event(17, 0, 0, 0) |
| 323 | + win32api.keybd_event(67, 0, 0, 0) |
| 324 | + win32api.keybd_event(67, 0, win32con.KEYEVENTF_KEYUP, 0) |
| 325 | + win32api.keybd_event(17, 0, win32con.KEYEVENTF_KEYUP, 0) |
| 326 | + time.sleep(0.2) |
| 327 | + return pyperclip.paste() |
| 328 | + except Exception as e: |
| 329 | + log.error('open clipboard failed: {}, retry...'.format(e)) |
| 330 | + time.sleep(1) |
| 331 | + else: |
| 332 | + raise Exception('read clipbord failed') |
| 333 | + |
| 334 | + @staticmethod |
| 335 | + def _project_position_str(raw): |
| 336 | + reader = StringIO(raw) |
| 337 | + df = pd.read_csv(reader, sep = '\t') |
| 338 | + return df |
| 339 | + |
| 340 | + @staticmethod |
| 341 | + def _set_foreground_window(hwnd): |
| 342 | + import pythoncom |
| 343 | + pythoncom.CoInitialize() |
| 344 | + shell = win32com.client.Dispatch('WScript.Shell') |
| 345 | + shell.SendKeys('%') |
| 346 | + win32gui.SetForegroundWindow(hwnd) |
| 347 | + |
| 348 | + @property |
| 349 | + def entrust(self): |
| 350 | + return self.get_entrust() |
| 351 | + |
| 352 | + def get_entrust(self): |
| 353 | + win32gui.SendMessage(self.refresh_entrust_hwnd, win32con.BM_CLICK, None, None) # 刷新持仓 |
| 354 | + time.sleep(0.2) |
| 355 | + self._set_foreground_window(self.entrust_list_hwnd) |
| 356 | + time.sleep(0.2) |
| 357 | + data = self._read_clipboard() |
| 358 | + return self.project_copy_data(data) |
0 commit comments