ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

基于Python和图像识别的通达信客户端自动化交易实战

基于Python和图像识别的通达信客户端自动化交易实战 简介这是一份面向个人投资者的Python股票自动化交易辅助资源围绕华泰证券通达信版交易终端而设计目标是解决人工盯盘、手工下单的低效问题。程序支持一次监控5只股票按照预设的时间或价格条件自动执行买卖操作单次下单耗时控制在2秒以内适合具备一定Python基础、希望在不改动券商终端的前提下实现程序化交易的用户。资源包总计12个文件以Python源码为核心包含主程序脚本、WinAPI功能封装模块并且配有Markdown说明文档、开源许可证协议以及8张界面运行截图压缩后整体仅166KB体量轻盈、结构直观。目前已有3207人学习使用该资源。通过这份资料读者可以获得现成的窗口自动化交易脚本深入理解利用pywin32查找并操作通达信界面按钮、输入框等控件的实现思路学会将时间条件与价格条件组合后触发下单同时脚本中封装的WinAPI操作函数具备较强的通用性也能移植到其他Windows桌面应用的自动化控制项目中无论是用于个人实盘优化还是学习桌面自动化开发都很有参考价值。 做A股自动交易第一步不是写代码而是先想清楚你到底要走哪条路。市面上的量化平台、券商官方接口、第三方库多如牛毛但落到散户头上每一条路都有看得见摸得着的门槛。我自己折腾了一年多最后落到“用Python接管通达信客户端”这条路上也就是这个项目名字里的tdx部分——通达信的拼音缩写。这条路不高级甚至有点土但确实是我实测之后唯一一条从资金门槛、开发成本、落地速度三个维度都走得通的路。这篇文章把我从零搭起pyautotrade_tdx的全部过程、选型逻辑、核心实现以及上线后踩到的坑整理出来目标读者是那些已经会用Python、想在A股市场跑一点半自动或全自动策略但又被券商接口资金门槛卡住的朋友。内容偏实战代码片段都是核心思路直接抄就能用。1. 为什么最终选择“桌面客户端自动化”这条路1.1 散户自动化交易的三条现实路径对比先说大家最容易想到的几条路。第一条是券商官方API或量化平台。国内大部分券商的量化接口都有资金门槛常见的50万起步有的还要求交易频率和资产规模这对资金量不大的散户来说基本是劝退的。QMT、Ptrade这类客户端级别的量化终端通常也需要找客户经理单独开通权限很多营业部对小资金客户根本不下发权限。第二条是直接用第三方交易库比如网上流传的easytrader这类开源项目。这个方案的思路是让Python去控制同花顺、通达信、财富通的客户端本质上也是界面模拟操作。但它的问题在于底层实现比较旧对新版通达信客户端的兼容性不稳定而且它的设计目标是通用对你自己本地的网络环境、券商版本、行情延迟等情况没有个性化的处理能力。出了问题时你面对的是一个黑盒。第三条就是自己写基于Windows窗口句柄、图像识别、键鼠模拟这套组合拳实现对通达信客户端的完全控制。这条路没有资金门槛只要你能正常登录券商的通达信客户端就能跑。代价是你必须自己处理所有细节从窗口定位到价格识别从下单动作到异常恢复全部自己来。pyautotrade_tdx就是在这条路上长出来的。1.2 这个选择的代价与收益自己写桌面自动化真正麻烦的事情不是“点击”本身而是“点击之前要知道点什么”和“点击之后要怎么确认”。通达信不是给程序用的它没有公开的SDK界面上每一个按钮、每一个价格数字对程序来说都需要通过图像识别或者内存读取的方式来理解。我最终选择截图模板匹配还有一个原因是通达信客户端本身是一个GDI绘制的老程序很多区域用传统的UI Automation拿不到控件信息。Windows的消息机制在这个软件上能拿到的窗口句柄很有限真正实用的信息比如当前价格、买一卖一、持仓数量、可用资金全都在“画出来”的像素里。所以这条路一开始会很难但走通之后你获得的是一个完全可控、完全透明的交易通道。策略逻辑是你自己写的下单动作是你自己模拟的每一步都清清楚楚出了问题也有日志可以排查。这种掌控感是第三方库给不了的。2. 项目整体设计与技术选型2.1 依赖组件与工具链核心依赖就三样pywin32负责Windows窗口操作opencv-python负责图像识别pyautogui负责键鼠模拟。这三件套是桌面自动化的铁三角。除此之外我加了Pillow做截图处理numpy做图像数组转换loguru做日志记录。项目本身是一个常驻任务按设定好的策略循环运行。我把整个项目拆成了四个模块窗口管理、行情识别、交易执行、策略调度。2.2 代码结构按职责拆成四个模块模块职责核心依赖window_manager.py获取通达信窗口句柄、设置窗口位置、窗口置顶win32gui, win32conmarket_reader.py截取指定区域、模板匹配识别价格和按钮状态pyautogui, opencv, numpytrade_executor.py模拟键盘输入完成买卖、撤单、查询持仓pyautogui, win32apistrategy_engine.py策略逻辑、状态机控制、信号生成与校验纯Python模块划分的原则很简单每一层都不要跨职责。比如行情识别模块只负责把“当前价格是多少”回答出来它不关心这个价格触发买还是卖交易执行模块只负责把“下单动作做完整”它不关心为什么要下单。这样的好处是当某一个环节出了问题你可以单独测试那一段代码不用把整个策略跑起来才能复现。2.3 为什么用“截图模板匹配”而不是去读内存有一种更高效但更危险的做法是直接读取通达信进程的内存数据找到对应地址把价格数据直接dump出来。这种方案响应速度极快不需要任何图像识别而且数据精度高。但我只做了技术验证就放弃了原因有两个。第一是稳定性。通达信版本更新非常频繁每隔一段时间就会发新版每次版本更新内存布局都会变化你之前找到的所有偏移地址全部失效又要从头开始逆向。这是一个无底洞。第二是合规风险。读内存的手段本质上是绕过软件的正常交互机制去获取数据如果被软件的反作弊系统检测到可能导致账号被标记甚至影响正常交易。为了一个自动化项目去冒这种风险不值得。图像识别的方式虽然笨但它和人类看屏幕的原理完全一样软件层面没有任何异常行为。通达信只会觉得“有一个手速极快的用户在看盘、在操作”不会觉得“有程序在控制我”。上线几个月稳定性完全够用。3. 核心实现的四个关键环节3.1 窗口定位从混乱的桌面里找到通达信第一步要解决的问题是代码怎么在屏幕上找到通达信窗口。一个直接的办法是遍历所有顶层窗口根据窗口标题过滤出我们要的那个。import win32gui def find_tdx_window(): hwnd_list [] def callback(hwnd, extra): if win32gui.IsWindowVisible(hwnd): title win32gui.GetWindowText(hwnd) if 通达信 in title and 网上股票 in title: hwnd_list.append(hwnd) win32gui.EnumWindows(callback, None) if not hwnd_list: raise RuntimeError(未找到通达信窗口请检查客户端是否启动) return hwnd_list[0]标题过滤的条件要尽量精确。最开始我只用“通达信”三个字匹配结果把行情主窗口、交易窗口、甚至广告弹窗都匹配到了导致操作目标错乱。后来我在标题里加了“网上股票”这个关键词才稳定锁定到交易登录后的主窗口。找到窗口之后还要做一件事固定窗口位置和大小。图像识别的坐标体系依赖于窗口位置如果窗口被拖动或者最小化之前的坐标全部偏移。我的处理方式是在启动策略前强制把通达信窗口移动到屏幕左上角并设置一个固定分辨率。import win32con def move_window(hwnd, x0, y0, width1280, height800): win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, x, y, width, height, win32con.SWP_SHOWWINDOW)窗口位置固定后所有识别区域的坐标就变成了常量不用每次动态计算。这是整个项目里性价比最高的优化直接消灭了一整类坐标漂移的bug。3.2 行情数据的抓取点对点截图与模板识别行情数据在通达信里有多个来源自选股列表、个股分时图、盘口五档。我最终选择了自选股列表作为主要行情源理由很朴素列表视图在一屏内能展示几十只股票的实时价格一次截图就可以完成一轮全量扫描效率远高于逐只股票切页。识别流程分三步截图、模板匹配、坐标换算。import cv2 import numpy as np from PIL import ImageGrab def get_price_from_region(region, template_path): # region: (left, top, right, bottom) img ImageGrab.grab(bboxregion) img_cv cv2.cvtColor(np.array(img), cv2.COLOR_RGB2BGR) template cv2.imread(template_path) result cv2.matchTemplate(img_cv, template, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc cv2.minMaxLoc(result) if max_val 0.85: h, w template.shape[:2] center_x region[0] max_loc[0] w // 2 center_y region[1] max_loc[1] h // 2 return center_x, center_y return None真正落地的时候我并没有用模板匹配去识别具体价格数字因为数字变化太频繁模板匹配的泛化能力不够好。我的方案是用模板匹配定位“价格数字所在的区域锚点”找到锚点后固定截取一个价格区域再交给OCR或者像素分割的方式识别具体数字。你可能会问为什么要绕这么一圈因为通达信窗口在不同分辨率的屏幕上渲染的字号和间距不一样没法用一个绝对坐标去定位。但界面上有一些元素的图案是固定不变的比如“现价”列标题、“涨跌幅”列标题它们的图标不随行情变化。用这些固定图标作为锚点反推价格区域的位置就能自适应不同分辨率的屏幕。价格数字的识别我用的是pytesseract加数字白名单识别率在99%以上。关键是识别前要做图像预处理转灰度、二值化、放大两倍。通达信的默认字体是等宽的放大之后用--psm 7模式识别效果非常稳定。3.3 买卖动作的模拟从点击坐标到热键下单拿到行情数据之后策略引擎会判断该不该下单。如果信号触发交易执行模块就开始工作。下单动作我采用了双通道方案优先走通达信自带的热键下单热键失效时回退到鼠标点击。通达信的交易快捷键设计很简单买入Enter后可输入代码和价格卖出是CtrlEnter视券商版本而定。但在自动化场景里直接用键盘输入比模拟鼠标点击更可靠因为键盘输入不依赖界面元素的实际位置不会因为分辨率变化而失灵。import pyautogui import time def send_order(stock_code, price, volume, directionbuy): # direction: buy / sell pyautogui.hotkey(ctrl, enter if direction buy else alt, s) # 注意不同券商的通达信版本热键差异很大务必先手工验证 time.sleep(0.3) pyautogui.write(stock_code, interval0.05) pyautogui.press(tab) pyautogui.write(str(price), interval0.02) pyautogui.press(tab) pyautogui.write(str(volume), interval0.02) time.sleep(0.2) pyautogui.press(enter)有一个关键的细节输入数字时的间隔不能太短。最开始我用interval0连续写入结果通达信经常丢掉数字比如买入价12.35变成了12.5。后来我把每个字符的间隔调到20~50毫秒丢数据的概率才降到零。这个经验在后来写批量下单时非常重要宁可慢一点也要保证每一步都是可确认的。下单之后还有一道确认弹窗通常是“委托已提交”之类的提示。这个弹窗的处理逻辑会放在下一节的状态机里详细讲因为它牵扯到整个系统的成败。3.4 状态机设计防止同一个信号下两次单自动交易最危险的事情不是“该买的时候没买”而是“同一个信号被重复执行了三次”。原因在于策略引擎每轮扫描都会计算出信号如果上一轮已经下了买单但系统没有记录这个状态下一轮扫描时同样的条件依然成立就会再下一单。解决方案是在交易执行层引入状态机class TradeState: IDLE IDLE # 无持仓等待买入信号 BUY_SUBMITTED BUY_SUBMITTED # 买单已提交等待确认 HOLDING HOLDING # 持仓中等待卖出信号 SELL_SUBMITTED SELL_SUBMITTED # 卖单已提交等待确认 class OrderStateMachine: def __init__(self): self.state TradeState.IDLE def can_buy(self): return self.state TradeState.IDLE def can_sell(self): return self.state TradeState.HOLDING def on_buy_submitted(self): self.state TradeState.BUY_SUBMITTED def on_order_confirmed(self): if self.state TradeState.BUY_SUBMITTED: self.state TradeState.HOLDING elif self.state TradeState.SELL_SUBMITTED: self.state TradeState.IDLE def on_order_cancelled(self): if self.state TradeState.BUY_SUBMITTED: self.state TradeState.IDLE elif self.state TradeState.SELL_SUBMITTED: self.state TradeState.HOLDING这个状态机的核心思路是只有处于IDLE状态才能触发买入只有处于HOLDING状态才能触发卖出。任何一次下单动作都会立刻改变状态即使后续确认失败状态也会被转移到可回退的分支上。这样同一个信号永远不会产生两笔重复委托。另外还要处理一个极端情况下单后通达信一直没有弹确认窗可能是卡住了也可能是委托失败。我的做法是设置超时等待比如下单后10秒内没有捕捉到确认弹窗就强制执行“取消操作”并把状态回退到上一态同时记录一条WARN日志。宁可漏一次交易也不能让状态机卡死导致后续所有信号全部失效。4. 上线之后踩到的那些坑4.1 系统缩放与分辨率坐标漂移的元凶第一个坑在开发环境完全没暴露一换到常用电脑就出现了所有坐标全部偏了。排查到最后发现是Windows的显示缩放——开发机是100%缩放常用机器是125%缩放。pyautogui有自己的缩放适配机制但win32gui.GetWindowRect返回的是物理像素坐标两者直接混用就会错位。解决方法是统一用逻辑像素获取窗口位置后除以当前系统的缩放比例。代码里写死一个常量import ctypes def get_screen_scale(): ctypes.windll.shcore.SetProcessDpiAwareness(1) dc ctypes.windll.user32.GetDC(0) dpi ctypes.windll.gdi32.GetDeviceCaps(dc, 88) # LOGPIXELSX return dpi / 96.0然后把所有涉及坐标的都乘上这个比例。上线第一周就把这个坑踩平了后面再也没复现过。4.2 弹窗和消息框不可预知的“程序中断”实盘之后你才会发现通达信会弹出各种你根本想不到的窗口风险提示、合法合规告知、闲置提醒、“当前为非交易时间段”弹窗、甚至券商临时公告弹窗。这些弹窗会悬浮在客户端最上层遮挡行情区域导致图像识别结果异常。我的处理策略是投降主义不试图动态识别所有弹窗而是定时定点地主动处理。每次下单前先用一个“关闭弹窗”的通用函数遍历通达信窗口的所有子窗口句柄把有“确定”或“关闭”按钮的窗口直接发送回车或ESC键。def close_popups(hwnd): def enum_child(hwnd_child, param): title win32gui.GetWindowText(hwnd_child) if title in (确定, 关闭, 提示, 风险揭示): win32gui.PostMessage(hwnd_child, win32con.WM_KEYDOWN, win32con.VK_RETURN, 0) win32gui.PostMessage(hwnd_child, win32con.WM_KEYUP, win32con.VK_RETURN, 0) return True win32gui.EnumChildWindows(hwnd, enum_child, None)这个方案不完美但够用。它的价值在于把不确定性截断在交易动作之前。既然预测不了弹窗的内容那就保证在“点击买入按钮”这个动作发生时眼前一定是干净的。4.3 价格刷新延迟与撤单时机通达信的行情刷新频率是3秒左右一次这个延迟在手动看盘时完全无感但在自动化场景里会带来一个隐患策略在9:30:02判断“当前价格触碰止损线”于是发出卖出委托但真实的盘口价格在9:30:00就已经击穿止损线了只是界面数据还没刷新。所以说你看到的价格永远是三秒前的价格。对低频策略影响不大但如果策略逻辑里用到了“价格穿越”这种对时间敏感的触发条件就必须在策略层加入额外的缓冲逻辑——例如记录连续两个扫描周期都满足条件后再下单。撤单的情况更麻烦。通达信的撤单操作需要进入“当日委托”页面找到对应记录再点击撤单按钮。这个流程涉及多个步骤和多次窗口切换非常容易出错。我最终的方案是不到万不得已不撤单。策略设计的止损线不是通过撤单来实现的而是通过缩小单笔下单量来控制单笔风险。撤单逻辑只在极端情况例如误下单、接口异常下手动触发不作为日常自动交易的一环。4.4 崩溃恢复与无头监控程序跑着跑着可能就崩了。崩溃原因五花八门通达信卡死无响应、网络断线导致子线程抛异常、股票停牌导致数据缺失等等。写代码的时候还算顺利真正麻烦的是崩溃之后如何恢复。我的做法是三层防护底层是看门狗线程主程序内起一个心跳线程定期检测窗口句柄是否有效。如果通达信窗口丢失自动重启通达信并重新登录。中层是重启策略每次启动时从数据库读取上一次的持仓状态直接恢复到对应的状态机节点不依赖内存数据。上层是异常报警关键操作失败时把详细日志和截图发到自己的微信或邮箱。截图很重要回到工位上看到的不是一行错误日志而是一张当时的屏幕截图问题原因一眼就能定位。这三层加完之后系统才算是真正扛住了实盘强度。5. 关于实盘我的底线与你需要知道的事5.1 风控先于策略自动化交易的代码再完美也只是把人的指令机械地执行下去。真正决定账户盈亏的永远是策略本身和仓位管理。我的经验是先跑模拟盘一个月再跑实盘最小仓位一个月最后才上正常仓位。每个阶段都要记录实际成交价和信号触发价之间的偏差这个偏差就是滑点的真实水平它决定了你的策略在实盘中能不能活得下去。实盘最小仓位的测试阶段我建议用每笔1手或者100股起步。这个阶段的目的是验证交易链路的稳定性而不是赚钱。如果连续跑一个月没有出现一次漏单、一次重复下单、一次无法恢复的卡死再考虑逐步加仓。5.2 自动化交易的边界与合规意识桌面客户端自动化本身属于“使用软件模拟用户操作”的范畴它和量化平台提供的标准接口有本质区别。使用这种方案时你要清楚地知道自己踩在什么边界上你没有通过券商官方认可的接口做交易而是模拟了人工操作。所以必须做到两点。第一交易动作要合理合法不要做高频率的恶意下单、不要试图绕开交易税费、不要对券商系统进行任何压力测试。把自动化当作一个“替代手动重复操作”的工具而不是“薅系统羊毛”的漏洞这是底线。第二对策略盈亏完全负责自动化代码不会为你的亏损买单。策略出现连续回撤的时候第一时间做的是停止策略、复盘、修策略而不是指望自动化程序帮你力挽狂澜。工具只是工具决策的锅永远在人身上。5.3 一套稳妥的上线流程最后分享一个我自己验证过的上线流程照着做可以少走至少一个月的弯路。第一步人工和程序并行两周。程序开着跑但下单功能设为“只记录不执行”所有信号和模拟委托都写入日志。这一步用来校验行情识别的准确率和策略逻辑的正确性。第二步模拟账号跑两周。如果券商提供模拟账号就用模拟账号做全链路的委托测试确认下单、确认、持仓查询全流程通畅。第三步实盘最小仓位跑两周。每笔委托都设置上限单笔金额控制在总资金的1%~2%。第四步全自动运行之后依然要保留每日人工复盘的习惯。每天收盘后对照日志检查当天的所有委托记录确认每一笔交易都是策略有意触发的结果没有异常操作混入。我在这一步上吃过亏。有一次程序在非交易时间段弹出了委托窗口因为代码里只判断了星期几没有判断节假日结果在国庆假期里触发了一次买入委托。虽然券商端因为非交易时间自动拒单了但这个隐患让我意识到时间判断、持仓判断、状态判断每一层都不能少。自动化交易最怕的不是网络延迟不是滑点而是那些你以为写对了、但实际上没有覆盖到的场景。本文还有配套的精品资源点击获取
返回列表