一、为什么要实现自动化交易
在瞬息万变的金融市场中,越来越多的散户投资者开始尝试构建自己的交易策略:有人通过技术指标捕捉趋势突破,有人利用基本面分析挖掘低估标的,还有人设计出复杂的网格交易或均值回归模型。然而,一个残酷的现实是——90%以上的个人策略最终因“执行脱节”而失效。
也许你会说我可以手动下单进行交易,作者本人亲自实验表明手动交易会有很多意想不到的情况出现。
-
1.时间与精力冲突
许多短线策略需要全天候盯盘(如分时均线突破、盘口挂单监测),但散户难以像职业交易员一样保持高强度专注。一位投资者曾自嘲:“凌晨三点起床盯外盘,结果白天上班时错过A股最佳卖点,策略再好也成了纸上谈兵。” -
2.人性的弱点:情绪化操作
即使策略逻辑清晰,手动执行时仍可能因恐惧(过早止损)、贪婪(延迟止盈)或从众心理(跟风追涨)而偏离计划。数据显示,超过70%的投资者承认曾在关键节点因情绪干扰做出错误决策。 -
3.复杂策略的“执行悖论”
高频调仓、多品种联动、动态止盈止损等操作对人工执行近乎苛刻。例如,一个简单的“股债动态平衡策略”需每月按比例调整持仓,但手动计算并下单极易出错;又如网格交易需在数百个价格档位挂单,人力根本无法完成。
而目前市面上并没有特别成熟且门槛低的工具可以帮我们实现各种复杂的自动化交易需求。尽管部分券商提供“条件单”功能,但其仅支持价格触发、定时交易等基础场景,无法适配个性化策略(如结合量价背离与新闻情绪分析的综合模型);而专业量化平台(如QMT)的高资金门槛和编程要求,又将普通投资者拒之门外。
对于散户而言,找到一种低门槛、高适配性、稳定可靠的自动化工具,意味着真正从“纸上谈兵”迈入“实战盈利”的质变——这不仅是效率的提升,更是投资认知的升维。
二、现有实现方法介绍
1.界面自动化工具(如pywinauto操作同花顺) 本系列教程将实现!
实现逻辑:通过模拟人工操作(点击、输入)控制交易软件界面。例如,用Python的pywinauto
库识别同花顺窗口控件,自动填写价格、数量并触发下单。
优势有,
-
低门槛:无需券商特殊权限,依赖现有交易软件即可实现。
-
灵活性:可适配多种交易软件(如同花顺、通达信、东方财富)
劣势有,
-
延迟风险:依赖本地软件运行,网络或硬件故障可能中断交易。
-
功能受限:无法实现高频交易或复杂策略的动态调整。
2.券商量化终端(如QMT、Ptrade) 本系列教程将实现!
实现逻辑:使用券商提供的专业量化平台(如中金QMT),通过内置API或策略编辑器编写交易逻辑,直接对接交易所系统执行订单。
优势有,
-
极速交易:支持高频操作,延迟低至毫秒级。
-
策略多样性:内置网格交易、算法拆单等模板,支持Python/Lua编程扩展。
-
风控完善:提供持仓监控、异常熔断等机制。
劣势有,
-
学习成本:需掌握量化框架及编程语言。
-
准入门槛高:通常需满足资金量要求(如QMT面向机构或高净值客户)。开通指南→
3. 同花顺内置条件单/策略交易 很简单也不实用,就不实现了~
实现逻辑:在同花顺软件中预设触发条件(如价格突破、定时交易),由本地或云端服务器监控并执行。
优势有,
-
无需编程:图形化界面操作,适合非技术用户。
-
基础功能免费:支持止盈止损、网格交易等常见策略。
劣势有,
-
功能局限:仅支持简单条件触发,无法自定义复杂逻辑。
三、进入本篇正题:如何通过pywinauto操作同花顺
首先是,前期准备工作:
环境搭建
-
1.安装Python环境(推荐Python 3.8+),配置好pip包管理工具。安装
pywinauto
库。 -
同花顺客户端配置
-
Inspect.exe(Windows SDK自带):用于查看窗口控件的属性(如类名、自动化ID)。
-
Spy++(Visual Studio工具):分析窗口层级结构。
-
2.安装辅助工具:
-
-
确保同花顺客户端为最新版本,避免因界面更新导致脚本失效。
-
登录账户并熟悉手动下单流程,明确需要自动化的操作步骤(如买入、卖出、撤单)。
-
关闭不必要的弹窗或提示(如风险提示),减少自动化干扰。
测试账户准备
-
强烈建议使用模拟账户,避免因脚本错误造成真实资金损失。
接着,明确需要学习的核心知识(如果你真想学会下面的每一个步骤都要亲自去学习尝试,遇到问题请问ai或查资料):
第一部分 pywinauto基础操作
-
连接应用程序
可以通过进程ID或窗口标题或者软件安装地址绑定同花顺客户端:
from pywinauto import Application
app = Application().connect(r'C:\APPS\同花顺软件\同花顺\xiadan.exe') # 根据实际窗口标题修改
注意这个界面才是下单界面,不是行情展示的界面窗口。
-
窗口与控件识别
-
使用
Inspect.exe(点击查看这是什么东西)
定位目标控件属性(如class_name
、automation_id
、name
)。 -
通过层级结构定位控件:
main_window = app.window(title="同花顺")
buy_button = main_window.child_window(title="买入", control_type="Button")
-
模拟操作
输入文本:edit_box.set_text("100")
点击按钮:button.click()
快捷键操作:type_keys("%s")
第二部分 同花顺界面结构分析
-
关键窗口与控件
登录窗口:账号输入框、密码输入框、验证码区域、登录按钮。
交易界面:股票代码输入框、买入/卖出选项卡、价格/数量输入框、下单按钮。
弹窗处理:确认对话框、错误提示框(如“价格超出涨跌幅限制”)。
动态控件应对
部分控件(如持仓列表)可能随数据变化刷新,需通过
wait
方法等待控件就绪:
buy_button.wait("exists", timeout=10).click() # 等待10秒直至控件出现
第三部分 同花顺界面中需要输入二维码情况的解决
-
Tesseract工具的使用(点击查看安装教程)
在操作账户的时候有时候会跳出输入“图形验证码”窗口,上面显示一些不规则(字符稍加扭曲变换得到的)的字符。tesseract-ocr(Optical Character Recognition)能够扫描字符,根据字符形状将其翻译成电子文本的过程。因此需要安装tesseract-ocr自动识别验证码。在Python中的额使用大概是这样:
def captcha_recognize(img_path):
import pytesseract
from PIL import Image
im = Image.open(img_path).convert("L")
# 1. threshold the image
threshold = 200
table = []
for i in range(256):
if i < threshold:
table.append(0)
else:
table.append(1)
out = im.point(table, "1")
# 2. recognize with tesseract
num = pytesseract.image_to_string(out)
return num
第四部分 异常处理与稳定性优化
-
网络延迟容错
添加重试机制,应对因网络卡顿导致的控件加载失败:
from pywinauto.timings import TimeoutError
try:
buy_button.click()
except TimeoutError:
print("控件未找到,尝试重新定位...")
-
日志记录
记录操作过程和错误信息,便于后期排查:
import logging
logging.basicConfig(filename="trade.log", level=logging.INFO)
-
防重复点击
通过状态标记避免因脚本卡顿导致的重复下单:
if not is_order_submitted:
submit_button.click()
is_order_submitted = True
接着,我们以获取当前账户的资金状况和持仓状况为例(在交易中实时获取这个两个信息是十分必要的),来进行一个实践。
注:代码逻辑参考于easytrader,下面的函数都是从相关的类中抽取出来的,所以会带有self.
首先我们看到,资金账户信息需要再在左边菜单栏切换到【查询F4】【资金股票】栏目后的右边展示,因此我们要先进行栏目的选择切换并获取右边的信息。
@property
def balance(self):
self._switch_left_menus(["查询[F4]", "资金股票"])
return self._get_balance_from_statics()
@perf_clock
def _switch_left_menus(self, path, sleep=0.2):
self.close_pop_dialog()
self._get_left_menus_handle().get_item(path).select()
self._app.top_window().type_keys('{F5}')
self.wait(sleep)
@perf_clock
def close_pop_dialog(self):
try:
if self._main.wrapper_object() != self._app.top_window().wrapper_object():
w = self._app.top_window()
if w is not None:
w.close()
self.wait(0.2)
except (
findwindows.ElementNotFoundError,
timings.TimeoutError,
RuntimeError,
) as ex:
pass
@functools.lru_cache()
def _get_left_menus_handle(self):
count = 2
while True:
try:
handle = self._main.child_window(
control_id=129, class_name="SysTreeView32"
)
if count <= 0:
return handle
# sometime can't find handle ready, must retry
handle.wait("ready", 2)
return handle
# pylint: disable=broad-except
except Exception as ex:
logger.exception("error occurred when trying to get left menus")
count = count - 1
随后我们要从右边保存信息,由上面第五行的_get_balance_from_statics()函数实现,_config中的信息是对应所需元素的contro_id.
def _get_balance_from_statics(self):
result = {}
for key, control_id in self._config.BALANCE_CONTROL_ID_GROUP.items():
result[key] = float(
self._main.child_window(
control_id=control_id, class_name="Static"
).window_text()
)
return result
BALANCE_CONTROL_ID_GROUP = {
"资金余额": 1012,
"可用金额": 1016,
"可取金额": 1017,
"总资产": 1015,
}
具体应用方式:
import easytrader
user = easytrader.use('universal_client')
user.enable_type_keys_for_editor()
user.connect(r'C:\APPS\同花顺软件\同花顺\xiadan.exe')
user.enable_type_keys_for_editor()
user.balance#获取资金账户信息
结果:
以上就获取到了balance信息,接下来获取position持仓信息,由于同花顺在这部分设置了验证码机制,所以这里我们要加上验证码截图、验证码识别和验证码输入的部分。方法是通过pytesseract
调用Tesseract引擎进行识别。
不同于上面的获取资金账户的信息,上面的账户信息直接就显示在控件中,因此获取到控件后可以直接得到想要的信息,但资金账户这个界面的信息是放在了一个grid中,获取的方式有两种,一是直接复制并从剪切板中获取信息,一种是邮件保存为文件再从文件中读取,第二种方式的稳定性更高,并且可以实现多次读取,因此讲解第二种方式的获取方法。请逐层函数的去看就
@property
def position(self):
self._switch_left_menus(["查询[F4]", "资金股票"])
return self._get_grid_data(self._config.COMMON_GRID_CONTROL_ID)
@perf_clock
def _switch_left_menus(self, path, sleep=0.2):
self.close_pop_dialog()
self._get_left_menus_handle().get_item(path).select()
self._app.top_window().type_keys('{F5}')
self.wait(sleep)
def _get_grid_data(self, control_id):
return self.grid_strategy_instance.get(control_id)
def get(self, control_id: int) -> List[Dict]:
grid = self._get_grid(control_id)
# ctrl+s 保存 grid 内容为 xls 文件
self._set_foreground(grid) # setFocus buggy, instead of SetForegroundWindow
grid.type_keys("^s", set_foreground=False)
if self._trader.is_exist_pop_dialog():
count = 5
while count > 0:
if (
self._trader.app.top_window().child_window(class_name="Static", title='提示').exists(timeout=1)
):
file_path = "tmp.png"
found = False
self._trader.app.top_window().child_window(class_name='Static',title ='')\
.capture_as_image().save(
file_path
) # 保存验证码
captcha_num = captcha_recognize(file_path).strip() # 识别验证码
captcha_num = "".join(captcha_num.split())
logger.info("captcha result-->" + captcha_num)
self._trader.app.top_window().child_window(
class_name="Edit",control_id=2404
).set_text(
""
)#删除原有的值
self._trader.app.top_window().child_window(
class_name="Edit",control_id=2404
).type_keys(
captcha_num
) # 模拟输入验证码
self._trader.app.top_window()\
.child_window(class_name="Button", title='确定').click()
# pywinauto.keyboard.send_keys("{ENTER}") # 模拟发送enter,点击确定
if self._trader.app.window(class_name="#32770", title='另存为').exists(timeout=2):
logger.info("验证码正确:" + captcha_num)
found = True
count = -1
else:
count = count - 1
if not found and count==0:
break
else:
Copy._need_captcha_reg = False
break
self._trader.wait(0.2)
if(count<=0 and (not found)):
self._trader.app.top_window()\
.child_window(class_name="Button", title='取消').click()
return self.get(control_id)
self._trader.wait(0.5)
temp_path = tempfile.mktemp(suffix=".xls", dir=self.tmp_folder)
self._set_foreground(self._trader.app.top_window())
# alt+s保存,alt+y替换已存在的文件
self._trader.app.top_window()['Edit1'].set_text(temp_path)
self._trader.wait(0.1)
self._trader.app.top_window().type_keys("%{s}%{y}", set_foreground=False)
# Wait until file save complete otherwise pandas can not find file
self._trader.wait(0.2)
if self._trader.is_exist_pop_dialog():
self._trader.app.top_window().Button2.click()
self._trader.wait(0.2)
return self._format_grid_data(temp_path)
def _format_grid_data(self, data: str) -> List[Dict]:
with open(data, encoding="gbk", errors="replace") as f:
content = f.read()
df = pd.read_csv(
StringIO(content),
delimiter="\t",
dtype=self._trader.config.GRID_DTYPE,
na_filter=False,
)
return df.to_dict("records")
具体应用:
import easytrader
user = easytrader.use('universal_client')
user.enable_type_keys_for_editor()
user.connect(r'C:\APPS\同花顺软件\同花顺\xiadan.exe')
user.enable_type_keys_for_editor()
user.position#获取持仓信息
结果:
代码自动识别窗口并识别验证码后填入保存信息