简介:这份资源面向使用华泰证券通达信版的量化交易爱好者与Python开发者,提供一套可同时监控5只股票、按条件自动下单的桌面自动化交易脚本,单次下单耗时控制在2秒以内,适合希望把手工盯盘转为程序化执行的进阶用户。压缩包共12个文件,约166KB,以8张png界面截图配合1个pyw主程序、1个py模块、1个license与1个md说明文档构成,截图直观展示对买对卖界面与参数设置,pyw与py文件承载核心逻辑,md与license交代使用方式与授权信息。目前已有3221人学习下载。读者可据此了解基于pywin32封装winapi操作交易客户端的实现思路,掌握时间条件与价格条件的组合下单规则、股票数量按100股取整等细节,并借助截图与说明快速还原运行环境,为自建自动化交易工具提供可复用的参考脚本与排错线索。
1. 股票自动化交易:从手动盯盘到脚本接管的那条分界线
很多人第一次听到「股票自动化交易」,脑子里浮现的是高频量化、毫秒级抢单、机房托管。但真正落到普通开发者手里,最现实、最容易跑通的形态其实是另一回事:用 Python 把券商交易终端「接管」过来,让脚本替你完成下单、撤单、查持仓这些重复动作。pyautotrade_tdx这个方向讲的就是这件事——它不碰券商接口协议,也不要求你拿到机构级 API 权限,而是通过 Windows 桌面端的自动化手段,去驱动一个已经登录好的交易客户端。
这条路能解决什么问题?最典型的是三类人:一是做网格、定投、条件单,但券商自带的条件单功能太弱、参数不够灵活;二是手里有自己写的选股或信号脚本,最后一步「下单」却还得手动点;三是想验证某个交易逻辑,但不想为了回测去搭一整套仿真环境。它的边界也很清楚:不适合做高频,不适合对延迟敏感的策略,本质上是「用 UI 自动化换接口权限」的折中方案。
我一般会把这类方案定位成「半自动流水线的最后一公里」。信号、风控、仓位计算都可以在 Python 里做得干干净净,唯独下单这一步交给自动化去点。理解了这个定位,后面所有的技术选型、参数设置、避坑点才有落脚的地方。
2. 拆解 pyautotrade_tdx:它到底动了交易终端的哪些部分
2.1 为什么是 UI 自动化而不是接口对接
先说选型理由,这决定了后面所有实现细节。券商的官方交易接口通常只对机构或特定资金量客户开放,个人开发者想拿到下单权限,门槛高、审批慢、还有合规约束。而交易终端本身是个 Windows 桌面程序,它把「买入」「卖出」「撤单」这些操作暴露成了可见的窗口控件。UI 自动化的思路就是:既然人能点,脚本也能点。
常见做法有两类。一类是基于图像识别,截屏找按钮坐标再点击;另一类是基于 Windows 控件树,直接定位到具体的输入框和按钮对象。前者通用但脆弱,分辨率一变、主题一换就翻车;后者稳定但依赖控件可被识别。pyautotrade_tdx这类方案通常走的是控件树为主、图像识别兜底的混合路线,因为交易终端的控件结构相对固定,用控件定位能拿到更好的稳定性。
这里有个关键判断:如果你的交易终端是那种自绘界面、控件树里什么都读不到的,那控件方案直接出局,只能退回图像识别,稳定性会掉一个档次。动手前先用工具看一眼目标窗口的控件树,这一步能省掉后面大量返工。
2.2 环境准备与依赖安装
先把运行环境固定下来。自动化交易对环境的敏感度比普通脚本高得多,因为一旦中途出错,可能留下未成交的挂单。
# 建议用独立虚拟环境,避免和其他项目的依赖打架 python -m venv venv_trade venv_trade\Scripts\activate # 核心依赖:控件自动化 + 图像识别兜底 + 数据处理 pip install pywinauto # Windows 控件树操作主力 pip install pyautogui # 图像识别与鼠标键盘兜底 pip install opencv-python # 模板匹配,配合 pyautogui 用 pip install pandas # 持仓、委托数据的整理逻辑说明:pywinauto负责和交易终端的控件对话,是主路径;pyautogui和opencv-python是当控件定位失败时的备用手段;pandas用来把查到的持仓、委托整理成结构化数据,方便风控判断。参数上没什么可调的,重点是版本别乱升——pywinauto不同大版本对控件后端的支持差异不小,装完先跑一个最小连接测试再往下走。
提示:交易终端必须以管理员权限运行,脚本本身也建议用管理员权限启动,否则某些窗口控件会因为权限隔离读不到。
2.3 连接交易终端并读取持仓
这是整条链路的地基。读不到持仓,后面的仓位计算和风控全是空中楼阁。
from pywinauto import Application import pandas as pd # 通过窗口标题连接已登录的交易终端,backend 选 uia 还是 win32 要实测 app = Application(backend="uia").connect(title_re=".*交易.*") # 定位主窗口 main_win = app.window(title_re=".*交易.*") main_win.set_focus() # 切换到持仓查询页(具体控件名以实际终端为准,这里用占位说明) main_win.child_window(title="持仓", control_type="TabItem").click() # 等待数据刷新,交易终端查询有网络延迟,不能点完立刻读 main_win.wait_cpu_usage_lower(threshold=5, timeout=10) # 读取持仓表格,控件类型通常是 DataGrid 或 List table = main_win.child_window(control_type="DataGrid") rows = table.descendants(control_type="DataItem") positions = [] for row in rows: cells = row.descendants(control_type="Text") texts = [c.window_text() for c in cells] if len(texts) >= 4: positions.append({ "code": texts[0], # 证券代码 "name": texts[1], # 证券名称 "qty": texts[2], # 持仓数量 "available": texts[3], # 可用数量 }) df = pd.DataFrame(positions) print(df)逻辑说明:先用connect挂到已经打开的终端上,注意是connect不是start,因为交易终端需要人工登录,脚本不该负责登录环节。切到持仓页后必须等数据刷新,wait_cpu_usage_lower是个粗糙但有效的等待手段,比固定sleep靠谱。读取时按行遍历,把每个单元格的文本抽出来。
参数说明:backend这个参数是第一个必调项,uia和win32两套后端对同一窗口的识别结果可能完全不同,哪个能读到控件就用哪个。title_re用正则而不是精确匹配,是因为终端标题常带资金账号或版本号,写死会失效。control_type的值(DataGrid、DataItem、Text)都要以实际终端的控件树为准,不能照抄。
2.4 下单与撤单的控件操作
下单是风险最高的一步,参数校验必须在脚本里做,不能指望终端帮你拦。
def place_order(main_win, code, price, qty, direction="buy"): # 切到买入或卖出页 tab_name = "买入" if direction == "buy" else "卖出" main_win.child_window(title=tab_name, control_type="TabItem").click() # 填入证券代码 code_box = main_win.child_window(control_type="Edit", found_index=0) code_box.set_edit_text(code) # 填入价格 price_box = main_win.child_window(control_type="Edit", found_index=1) price_box.set_edit_text(str(price)) # 填入数量 qty_box = main_win.child_window(control_type="Edit", found_index=2) qty_box.set_edit_text(str(qty)) # 二次确认:下单前把三个字段读回来核对 assert code_box.window_text() == code assert price_box.window_text() == str(price) assert qty_box.window_text() == str(qty) # 点击下单按钮 main_win.child_window(title="买入下单", control_type="Button").click()逻辑说明:found_index是按控件出现顺序取第几个,因为交易终端的输入框往往没有独立的标题,只能靠顺序定位。填入之后立刻读回来做断言,这是防止「填错框」的最后一道防线——血泪经验,输入框顺序在某些终端里会随页面状态变化。断言通过才点下单。
参数说明:found_index是最容易出问题的参数,页面布局一变顺序就乱,所以每次终端升级后都要重新核对。价格和数量建议在调用这个函数之前就做好精度和范围校验,比如价格是否符合最小变动单位、数量是否是 100 的整数倍,这些校验放在业务层比放在 UI 层更清晰。
3. 把自动化串成策略:信号、风控、执行三段式
3.1 信号层与执行层解耦
新手最容易犯的错,是把「判断该不该买」和「怎么点按钮」写在一个函数里。这样一旦 UI 出问题,整个策略逻辑也跟着乱。正确做法是三层分离:信号层只产出「买/卖/持有」的意图,风控层决定「能不能执行、执行多少」,执行层只负责把指令变成控件操作。
# 信号层:纯计算,不碰 UI def generate_signal(df, fast=5, slow=20): df["ma_fast"] = df["close"].rolling(fast).mean() df["ma_slow"] = df["close"].rolling(slow).mean() if df["ma_fast"].iloc[-1] > df["ma_slow"].iloc[-1]: return "buy" elif df["ma_fast"].iloc[-1] < df["ma_slow"].iloc[-1]: return "sell" return "hold" # 风控层:决定仓位和是否放行 def risk_check(signal, positions, cash, max_position_pct=0.2): if signal == "hold": return None if signal == "buy": budget = cash * max_position_pct return {"action": "buy", "budget": budget} if signal == "sell": # 卖出只针对已有持仓 return {"action": "sell", "code": positions[0]["code"]} return None逻辑说明:信号层用双均线做示例,重点是它只吃 DataFrame、只吐字符串,完全不依赖 UI,这样回测和实盘能共用同一套逻辑。风控层拿到信号后,结合当前持仓和现金决定实际动作,max_position_pct控制单次最大仓位占比,避免一把梭。
参数说明:fast和slow是均线周期,属于策略参数,和自动化无关,按自己的逻辑调。max_position_pct是风控参数,建议从 0.1 到 0.2 起步,跑顺了再考虑放大。这个参数的意义在于,即使信号层抽风连续发买入,风控层也能把单次敞口压住。
3.2 用状态文件防止重复下单
自动化交易最怕的不是下错单,是同一笔单下两次。脚本崩溃重启、网络抖动重试,都可能触发重复执行。解决办法是引入一个持久化的状态文件,记录「今天已经执行过哪些动作」。
import json import os from datetime import date STATE_FILE = "trade_state.json" def load_state(): if not os.path.exists(STATE_FILE): return {"date": str(date.today()), "executed": []} with open(STATE_FILE, "r", encoding="utf-8") as f: state = json.load(f) # 跨天自动重置 if state.get("date") != str(date.today()): return {"date": str(date.today()), "executed": []} return state def mark_executed(state, key): state["executed"].append(key) with open(STATE_FILE, "w", encoding="utf-8") as f: json.dump(state, f, ensure_ascii=False) def already_done(state, key): return key in state["executed"]逻辑说明:状态文件按天记录,跨天自动清空。每次执行前用already_done检查,执行后用mark_executed落盘。key建议用「日期+代码+方向」拼成,比如20250101_600000_buy,这样同一标的同方向当天只执行一次。
参数说明:STATE_FILE的路径要放在脚本目录下,别放临时目录,否则重启就丢。写入用ensure_ascii=False是为了中文可读,方便排查。这个机制不能替代券商端的委托去重,但能挡住脚本层面的重复触发,是性价比很高的一道保险。
3.3 委托回报的轮询与对账
下单点完不代表成交,必须回头查委托状态,否则你的持仓数据和实际会对不上。
import time def wait_order_done(main_win, code, timeout=30): """轮询委托页,直到该代码的委托不再是'已报'状态""" deadline = time.time() + timeout while time.time() < deadline: main_win.child_window(title="委托", control_type="TabItem").click() main_win.wait_cpu_usage_lower(threshold=5, timeout=5) table = main_win.child_window(control_type="DataGrid") for row in table.descendants(control_type="DataItem"): texts = [c.window_text() for c in row.descendants(control_type="Text")] if texts and texts[0] == code: status = texts[-1] # 状态列通常在最后 if status not in ("已报", "待报"): return status time.sleep(2) return "timeout"逻辑说明:切到委托页,遍历每一行找目标代码,看状态列是否已经脱离「已报/待报」。脱离了就说明有了结果(成交、部成、已撤等)。超时返回timeout,交给上层决定是撤单还是继续等。
参数说明:timeout默认 30 秒,集合竞价或行情剧烈时可能不够,可以放宽到 60。time.sleep(2)是轮询间隔,太密会拖慢终端响应,太疏会错过快速成交,2 秒是个折中。状态列的判断值要以实际终端的文案为准,不同终端用词不一样。
4. 避坑与排查:那些让脚本半夜翻车的细节
4.1 控件定位突然失效
现象:昨天还能跑,今天一启动就报找不到控件,或者点到了错误的按钮。
原因:交易终端自动更新、切换了主题、或者弹出了公告窗口遮挡了主界面。控件树的结构和顺序都可能因此变化。
解决:第一,每次启动前先检测是否有弹窗,有就先关掉再操作主窗口。第二,把关键控件的定位从found_index改成更稳定的属性组合,比如同时匹配control_type和auto_id。第三,加一层「操作前截图」,出问题时能回看当时界面长什么样。我一般会在脚本里保留最近三次的截图,排查时非常省事。
4.2 填了价格但下单按钮点不动
现象:代码、价格、数量都填进去了,点下单没反应,或者提示「请输入有效价格」。
原因:交易终端对输入框有校验,set_edit_text直接写值可能不触发它的校验事件,终端内部认为这个框还是空的。
解决:改用模拟键盘输入的方式,先点击输入框获得焦点,再逐字符输入。pywinauto里可以用type_keys替代set_edit_text。另外注意价格精度,填10.00和10在某些终端里结果不同,按终端要求补齐小数位。
4.3 脚本跑着跑着持仓读成空
现象:持仓查询偶尔返回空列表,导致风控误判为「无持仓」而重复买入。
原因:查询页切换后数据还没从服务器返回,脚本读得太早。wait_cpu_usage_lower只能等本地 CPU 空闲,等不了网络往返。
解决:加一个「数据有效性」判断,比如表格行数为 0 时不要立刻采信,而是重试两到三次,每次间隔 1 秒。如果连续为空,宁可报错退出也不要继续下单。这个坑的代价很高,因为空持仓会直接导致重复建仓。
4.4 多显示器或分辨率变化导致坐标错位
现象:用图像识别兜底的那部分逻辑,换了显示器或改了缩放比例后全部点偏。
原因:图像识别依赖屏幕绝对坐标,分辨率、DPI 缩放、多屏排列一变,坐标全废。
解决:尽量不用图像识别,能用控件就用控件。实在要用,把模板匹配的置信度阈值调高,并在匹配失败时直接报错而不是「猜一个位置点下去」。另外把 Windows 的显示缩放固定为 100%,别用 125% 或 150%,能省掉大量玄学问题。
4.5 脚本异常退出留下未完成的委托
现象:脚本中途崩溃,已经报出去的单没人管,成交了也不知道。
原因:缺少异常兜底和启动时的对账逻辑。
解决:用try/finally包住主循环,异常时至少把当前状态写进日志。更重要的是,脚本每次启动时先做一次全量对账:读一遍当前委托和持仓,和状态文件比对,发现不一致就以终端实际数据为准。这是后悔药,能救回大部分崩溃后的烂摊子。
5. 让自动化交易更稳的几个进阶习惯
跑通最小闭环之后,真正决定这套东西能不能长期用的,不是策略多聪明,而是工程细节多扎实。分享几个我踩过坑之后固定下来的习惯。
第一个是「干跑模式」。在真正下单之前,让脚本完整走一遍信号、风控、控件定位、填单的流程,但最后一步不点下单按钮,只把「本来要下的单」打印出来。这个模式跑上一周,能暴露绝大部分控件和时序问题,而且零成本。实现上就是给place_order加一个dry_run参数,为真时在点击前 return。
第二个是日志要记「意图」而不只是「结果」。很多人只记「下单成功」,但排查时真正有用的是「为什么下这一单」——当时的信号是什么、持仓多少、风控放行的理由是什么。我一般会把这三样一起写进日志,格式用 JSON,方便后面用 pandas 分析。
第三个是给关键操作加「冷却时间」。同一标的的买入操作,即使信号反复触发,也强制间隔至少 N 分钟。这能挡住信号抖动导致的频繁交易,也能给人工干预留出窗口。冷却时间记在状态文件里,和去重逻辑共用一套机制。
第四个是定期核对。自动化跑久了容易产生「它一直在正常工作」的错觉。我的习惯是每周手动对一次账:把脚本记录的成交和终端里的实际成交逐笔比对,数量、价格、时间都要对上。对不上就说明某个环节有静默错误,越早发现越好。
最后说个验证方法:拿一个模拟账号或者极小资金账号,把上面整套流程跑满一个月,重点观察三件事——有没有重复下单、有没有漏单、持仓数据和终端是否始终一致。这三件事都稳了,再考虑放大资金。自动化交易这件事,慢就是快,急着上量往往就是翻车的开始。希望帮到你。
本文还有配套的精品资源,点击获取