简介这是一套基于Python的量化交易辅助工具包针对同花顺手机版模拟炒股环境利用uiautomator2库与图色识别方法实现自动下单、持仓查询等API操作适合量化交易爱好者与Python自动化开发者参考并预留了扩展到实盘交易的改造空间。该工具通过模拟点击、滑动与颜色特征匹配解决了非标准控件难以自动定位的问题整体更贴近移动端实操场景。压缩包共9个文件、约1.33MB包含核心Python脚本、依赖清单、使用说明、许可证协议及模拟器分辨率截图等素材结构精简既可快速了解自动化交易流程也能作为二次开发的基础样例。截至目前已有208人学习下载适合作为入门模拟盘量化交易的实操参考读者可从中掌握uiautomator2驱动Android设备、图色定位界面元素的基本思路。在此基础上还可结合自身策略进行修改为后续接入真实交易接口积累经验但需注意实盘交易所涉及的行情延迟、交易成本与合规要求开发者应当做好充分测试与风险控制。1. 量化交易工具把同花顺模拟盘变成可编程交易终端这套资源解决的是量化策略开发里最让人卡壳的一环策略算出了买卖点却下不了单。正规券商API大多有资金门槛或申请周期而手机版模拟炒股App只能手动点。这个基于uiautomator2驱动的Python API方案把同花顺App的模拟盘操作完全交给代码——下单、撤单、查持仓、看成交全都通过UI自动化加图色识别完成。适合策略已经跑通回测、需要低成本验证实盘逻辑的开发者也适合不想碰券商接口又想体验自动化交易的散户。工具本身不算新但把控件定位和图色识别结合得比较务实实盘扩展的接口也预留了整套拆下来大概需要半天时间能跑通。2. 环境与连接uiautomator2初始化与设备识别要点2.1 前置环境手机、电脑、驱动一次性配齐跑这套方案之前先把环境三件套准备好一台开启开发者模式的安卓手机、一条能传数据的USB线、一台装了Python 3.8以上版本的电脑。开发者模式在手机设置里连续点击版本号七次就能开启然后进开发者选项打开USB调试。这些是基础操作但很多人会忽略一个细节手机插上电脑后弹出的“允许USB调试吗”对话框必须勾选“始终允许”否则每次重连都要手动确认自动化脚本跑到一半卡在授权框上直接超时。电脑端需要装uiautomator2和weditor两个库。weditor是uiautomator2的可视化辅助工具用来截图查看控件树和坐标后面定位按钮全靠它。安装命令分开执行避免依赖冲突pip install uiautomator2 pip install weditoruiautomator2装完后要跑一次初始化把对应的agent和atx服务推到手机里。手机需要保持亮屏状态最好在开发者选项里开启“充电时不休眠”不然你盯半天发现agent没装上去。python -m uiautomator2 init这会向手机安装两个APK包atx-agent和uiautomator2的测试服务。初始化完成后用adb devices确认设备在线再用u2.connect()连接。如果连接报错最常见的原因是手机连着其他调试工具占用端口关掉Android Studio或ADB占用程序再试。提示如果你有多台安卓设备连接时建议直接指定序列号。序列号用adb devices查看比如emulator-5554或一长串设备ID避免插两台手机时脚本连错机器。2.2 设备连接与App拉起三行代码验证通路连接设备是所有自动化操作的第一步代码很简单但验证逻辑要完整。下面这段代码做了三件事连接设备、停止App旧进程、重新启动App。import uiautomator2 as u2 # 连接手机序列号从adb devices获取 d u2.connect(你的设备序列号) print(d.device_info) # 确认设备信息正常返回 # 停止并重新启动同花顺App d.app_stop(com.hexin.lottery) # 包名以实际安装的为准 d.app_start(com.hexin.lottery) d.sleep(5) # 等App完全加载d.device_info返回的字典里包含型号、系统版本、屏幕分辨率拿到分辨率后面算坐标才能写死。App的包名怎么查在终端跑d.app_current()如果当前正在前台运行这个App返回值里的package字段就是包名。不同版本的同花顺包名可能不一样不要凭记忆写死以实际输出为准。启动后加一个5秒的sleep等待页面加载完成。这个等待时间不是玄学App冷启动要加载行情数据和广告页等太短会导致后续点击落在广告弹窗上。如果你发现首页有弹窗干扰定位可以加一句d.press(back)把弹窗关掉再做后续操作。2.3 weditor定位不靠猜坐标看控件树和锚点首次写界面定位代码建议打开weditor辅助它可以实时截图并展示控件层级比一遍遍跑脚本看错误日志高效得多。python -m weditor浏览器会自动打开一个本地页面输入设备序列号连接后左侧能看到实时截图右侧是控件树。点选屏幕上任意位置下方能看到对应的resourceId、text、bounds坐标。肉眼记一下买入、卖出、持仓这几个关键按钮的坐标区间后面写定位逻辑时能少踩很多坑。但要注意同花顺这类行情App大量页面是自绘的控件树里经常拿不到理想的resourceId。文本定位d(text买入)可能因为页面层级混乱而失败坐标定位反而稳定。所以weditor的主要用途是帮你确立一个“锚点坐标”后续图色识别都在锚点附近的ROI区域里做而不是全屏扫描。3. 图色识别与坐标点击把“看盘”翻译成机器指令3.1 为什么放弃resourceId改用图色匹配刚开始做UI自动化的人通常会优先尝试控件定位——用resourceId或text找按钮干净又准。但实际跑起来你会发现同花顺App的控件树里大量元素没有暴露resourceIdtext信息也经常是空的。就算找到了页面一刷新控件节点就重建定位链路易断。图色识别的思路是反过来的不依赖控件树直接对截图做像素级判断——找颜色、找模板图、算坐标然后点击。这套方案最大的优点是兼容性好不挑App版本只要界面长什么样没变脚本就能继续跑。缺点是识别结果受分辨率、主题模式影响所以后面处理时要把截图统一缩放到固定尺寸再在限定区域里匹配。3.2 颜色识别用HSV空间判断红绿涨跌模拟盘里最常见的视觉元素是涨跌颜色红色代表上涨、绿色代表下跌国内行情软件惯例。判断当前是红是绿最稳的方法是转HSV颜色空间后做阈值过滤。RGB对光照敏感一份红在深浅不同光照下RGB值差异很大而HSV的色相维度能把颜色类型稳定区分出来。import cv2 import numpy as np def detect_market_color(screenshot_path, roi): 判断截图ROI区域内的主色调是红还是绿 roi格式: (x1, y1, x2, y2) img cv2.imread(screenshot_path) img_hsv cv2.cvtColor(img, cv2.COLOR_BGR2HSV) # 只取ROI区域减少干扰 x1, y1, x2, y2 roi roi_hsv img_hsv[y1:y2, x1:x2] # 红色在HSV中有两个区间需要分别处理 red_mask1 cv2.inRange(roi_hsv, (0, 70, 50), (10, 255, 255)) red_mask2 cv2.inRange(roi_hsv, (170, 70, 50), (180, 255, 255)) red_mask red_mask1 | red_mask2 # 绿色区间 green_mask cv2.inRange(roi_hsv, (35, 70, 50), (85, 255, 255)) red_pixels cv2.countNonZero(red_mask) green_pixels cv2.countNonZero(green_mask) if red_pixels green_pixels and red_pixels 500: return red elif green_pixels red_pixels and green_pixels 500: return green else: return unknownROI范围不需要全屏从weditor里记下来的价格区域坐标直接填进去。阈值500是经验值过滤掉少量噪点的干扰。如果你的手机分辨率是2K或者更高像素阈值可以适当上调到8001000。这个函数在实际项目中用在策略开盘前的状态校验——先看当前大盘或个股是红是绿再决定是否执行预设的买入逻辑。3.3 模板匹配定位“买入”“卖出”“撤单”按钮按钮定位用cv2.matchTemplate做模板匹配。做法是先从干净的首页截图里截取按钮图片存成模板代码运行时全屏找匹配位置得出中心坐标后点击。模板匹配对按钮缩放敏感所以模板图片的尺寸要和实际运行截图一致。如果你改了手机分辨率或系统的字体大小模板匹配率会明显下降要重新截图做模板。import cv2 import numpy as np import uiautomator2 as u2 def find_button_and_click(d, screenshot, template_path, threshold0.8): 在屏幕截图中查找模板返回中心坐标并点击 threshold: 匹配阈值越高越严格 # 读入截图和模板 img cv2.imread(screenshot) template cv2.imread(template_path) h, w template.shape[:2] # 模板匹配 result cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED) _, max_val, _, max_loc cv2.minMaxLoc(result) if max_val threshold: # 计算按钮中心点 center_x max_loc[0] w // 2 center_y max_loc[1] h // 2 # 点击前先截一张图存档方便回看 d.screenshot(before_click.png) d.click(center_x, center_y) print(f点击成功, 中心坐标({center_x}, {center_y}), 匹配度{max_val:.2f}) return True else: print(f未找到按钮, 最高匹配度{max_val:.2f}) return Falsethreshold0.8是兼顾准确率和召回率的经验值。低于这个值容易误点其他相似元素高于这个值遇到页面轻微变化比如按钮颜色变浅就找不到。模板匹配在按钮旁边有同样颜色的icon时容易翻车所以你截图模板时尽量截大一点把按钮连同周围的文字一起截进去特征越多匹配越准。注意点击前的截图存档是个好习惯脚本跑飞了能回头查“当时屏幕上到底长什么样”别省这一步。4. 交易动作封装与策略接入从单笔买入到循环执行4.1 TradeExecutor封装把UI操作变成函数接口自动化操作写到最后一定会走到封装这一步。不封装每个策略都要重复写“找按钮-点击-输数量-确认”的流程。封装成TradeExecutor类之后策略代码只需要调用buy(stock_code, price, amount)这样的方法内部细节全部屏蔽。import time import uiautomator2 as u2 class TradeExecutor: 交易执行器封装同花顺App的买入、卖出、查询操作 mode参数预留sim为模拟盘live为实盘需自行接入券商API def __init__(self, device, modesim): self.d device self.mode mode self.anchor self._get_anchor() def _get_anchor(self): # 从weditor获取的固定坐标锚点写死后基本不用动 return {buy_btn: (540, 820), sell_btn: (780, 820), price_input: (540, 580), amount_input: (540, 640)} def buy(self, stock_code, price, amount): 买入操作点击买入→输入价格→输入数量→确认 self.d.click(*self.anchor[buy_btn]) time.sleep(1) self._input_text(str(price)) self.d.click(*self.anchor[amount_input]) self._input_text(str(amount)) self.d.click(*self.anchor[confirm_btn]) time.sleep(2) # 截屏确认成交结果 self.d.screenshot(fbuy_{stock_code}_{int(time.time())}.png) def sell(self, stock_code, price, amount): 卖出操作逻辑与买入类似 pass def _input_text(self, text): # 有些输入框是自绘控件set_text不一定生效 # 备选方案是逐字符点击键盘具体看App实际情况 self.d.set_text(text, timeout3)封装的核心思路是尽量把坐标、延时、截图这些细节收敛到类内部外部调用时只传业务参数。_input_text单独抽出来是因为自绘App的输入框经常不响应set_text如果遇到这种情况需要改用adb shell输入后面在避坑部分细说。4.2 策略循环轮询信号触发交易动作策略接入的骨架是一个无限循环拉行情、算信号、执行交易、等待下一轮。模拟盘阶段循环间隔建议设置在30秒以上太快不仅容易被App风控还会让手机CPU占用飙升、操作截图延时变高。import time from executor import TradeExecutor executor TradeExecutor(u2.connect()) def get_signal(): 策略信号源替换成你自己的策略逻辑 返回值: buy, sell, hold 这里用真实行情数据源比如akshare或自建数据库 # 伪代码实际替换成策略计算 return buy def run_strategy(loop_interval30): while True: try: signal get_signal() if signal buy: executor.buy(000001, 10.5, 100) elif signal sell: executor.sell(000001, 11.0, 100) else: print(无信号继续等待) except Exception as e: # 异常时截屏存档方便排查 executor.d.screenshot(ferror_{int(time.time())}.png) print(f执行异常: {e}) time.sleep(loop_interval)策略循环里最容易被忽略的是异常处理。UI自动化操作失败太常见了——一个弹窗挡住按钮、网络延迟导致页面没刷新、模拟盘服务器卡顿都会让原本正常的代码抛出异常。如果不做兜底脚本会在半夜静默退出。所以每次异常时至少要做截屏把当时的现场留档第二天复盘才知道是什么情况。4.3 实盘扩展预留接口而不是推倒重来这套工具被定位成“可扩展到实盘”关键在于模拟盘执行器和实盘执行器可以共用一套策略逻辑。做法很简单把TradeExecutor定义成接口模拟盘和实盘分别实现策略代码只依赖接口方法。from abc import ABC, abstractmethod class TradeExecutorBase(ABC): 交易执行器抽象接口 abstractmethod def buy(self, stock_code, price, amount): pass abstractmethod def sell(self, stock_code, price, amount): pass class SimExecutor(TradeExecutorBase): 模拟盘执行器基于uiautomator2操作App # 实现细节与上一节相同 class LiveExecutor(TradeExecutorBase): 实盘执行器接入券商Python SDK而非UI自动化 def buy(self, stock_code, price, amount): # 调用券商API下单 pass策略代码里只需要改一行初始化时把SimExecutor换成LiveExecutor策略逻辑完全不用动。这个抽象层的价值在于你在模拟盘上验证得越充分切到实盘时心理越有底。但必须提醒一句实盘不是简单换个执行器就完事的实盘要考虑滑点、部分成交、资金不足、撤单速度这些模拟盘不会完整暴露。5. 避坑专题模拟盘自动化的五个高频翻车点5.1 手机一锁屏脚本就断连现象脚本运行几分钟后报device disconnected或者手机黑屏后重连不上。原因uiautomator2基于ADB通道通信手机灭屏进入休眠后WiFi或USB通道会被系统回收导致agent失联。解决开发者选项里打开“充电时保持唤醒”和“屏幕永不休眠”锁屏设置改为“无”。如果插着USB线还断检查一下系统是否有省电策略在后台杀掉atx-agent进程。长期跑建议用一台专门做自动化的旧手机不装微信QQ这些会抢占前台的应用系统越干净越稳定。5.2 输入框set_text失败数量价格输不进去现象执行d.set_text()后输入框没有内容或者输入了但提交时提示数据不合法。原因同花顺的部分输入框是自绘控件不走系统的EditText接口uiautomator2的set_text无法注入文本。另一个常见原因是输入框有校验逻辑直接set_text跳过了键盘事件的触发。解决改用adb shell input text逐字符输入。先保存当前输入法把ADBKeyBoard切换为默认输入法再做字符注入。偶尔遇到中文输入需求还要处理编码但证券代码和价格数字一般用英文输入法就够了。# 日常用英文输入即可数字直接输入 adb shell input text 10.50如果输入框聚焦后不弹键盘先点击输入框再用input text时序上要留0.5秒左右的间隔。这类问题无统一解只能基于你手机上的实际App行为做调试一次调通后就别再动它。5.3 点击坐标偏了误触其他按钮现象点击后跳转的页面不对比如点“买入”却进了个股行情页。原因App做了横竖屏适配或字体缩放按钮在屏幕上的绝对位置发生了偏移而你写的坐标是写死的。解决不要裸用绝对坐标搭配图色识别做二次校验。在点击“买入”前先截屏做模板匹配确认按钮在当前屏幕上的实际坐标再点击。模板匹配的坐标比写死坐标可靠得多。另外整个自动化过程中保持手机处于竖屏状态避免App自动切换横屏导致布局变化。5.4 红绿识别在个股行情页翻车现象detect_market_color返回的颜色和肉眼看到的不一致明明红色的票识别成绿色。原因行情页的背景色、涨跌幅百分比数字颜色都参加了像素统计红色或绿色的面积比只代表“区域里红色多还是绿色多”不代表“这只票在涨还是跌”。解决ROI区域不要框选整个页面只框选个股名称旁边的涨跌幅标签那一小块。同时把识别逻辑改严红色和绿色像素数同时超过阈值才输出判断两者都低时返回unknown不要在数据噪声高时强行下结论。另外实测发现夜间模式或暗色主题下颜色识别准确率会骤降建议App固定用默认主题跑自动化。5.5 撤单报错想撤掉旧单结果界面在第二层现象策略连续下单后要撤单点击“撤单”按钮却提示无效操作或者直接找不到这个按钮。原因同花顺模拟盘的撤单入口通常藏在“委托”或“当日委托”页面里不是所有页面上都有。UI自动化操作执行到一半页面状态和你预期不一致按钮自然找不到。解决设计撤单流程时先把页面路径打平。进“当日委托”页面后先截一张图用模板匹配定位“撤单”按钮再匹配弹窗上的“确认撤销”按钮分两步走。不要写一个大而全的点击函数页面层级一深就崩。模板匹配有时会同时匹配到多个“撤单”文本取匹配度最高的那一个并在代码里打印匹配数量供排查。6. 进阶验证让模拟盘结果可度量、可复盘工具跑到能稳定下单之后下一步是把模拟盘当成一个可重复的实验环境来用而不是随手下单。我会在每次策略运行结束后自动保存当天的持仓截图、成交列表截图和资金曲线截图文件名带上日期和策略参数。这样一周跑下来你能非常直观地看到哪一天的界面状态导致了操作卡顿哪一天的策略参数在真实市场环境下表现异常。比较实用的验证方式是跑A/B对照给同一套策略配两组参数在两个时间段分别跑模拟盘再用资金曲线和成交明细做对比。模拟盘的意义不在于替你赚钱而在于暴露策略在真实行情节奏下的行为——比如开盘瞬间的涨停价能否按预设价格买入跌停时卖出是否被堵单连续下单会不会触发模拟盘自己的风控限制。这些场景在历史回测里不会原样呈现只有让代码真实操作App一遍你才能理解“逻辑上能成交”和“实际上能成交”之间隔着多大的距离。我自己的习惯是每次改动工具代码后强制跑一遍冒烟脚本从启动App开始完成一次买入和一次卖出检查两张截图和日志里的时间线。整个过程不超过两分钟但能拦截掉90%的回归问题。曾有一次改错了一个坐标常量冒烟脚本没跑完就报错了省下了后面一整晚盯盘的功夫。从那以后我没再跳过这道工序也希望这套习惯能帮到你。本文还有配套的精品资源点击获取