有需要的可以自己行学习修改调试。
支持信用账户(具体要自己调一下参数)
稳定运行多年 支持最新客户端
该脚本将包含:
- 核心UI自动化: 使用
pywinauto
,win32api
,pyautogui
控制同花顺。 - 百度OCR验证码识别: 处理操作过程中可能出现的验证码。
- 同花顺基本操作: 查询资金/持仓、当日委托、下单、撤单。
- 中文注释和输出: 所有代码注释和打印信息将使用中文。
注意:
- 请务必替换百度OCR凭证!
- UI自动化脚本非常脆弱! 脚本中的控件ID和坐标可能需要根据您的同花顺版本和券商进行调整。
# -*- coding: utf-8 -*-
"""
简化版同花顺交易接口自动化脚本 (移除外部依赖)
版本: 2.0
最后更新: [当前日期]
功能:
- 通过 pywinauto 和 win32api 控制同花顺PC客户端。
- 使用百度OCR识别登录或操作过程中的验证码。
- 实现查询资金股份、当日委托。
- 实现买入、卖出、融资买入、撤单等操作。
- 包含针对不同券商客户端UI差异的适配提示。
核心依赖库:
pip install pywinauto pyautogui opencv-python baidu-aip pandas numpy -i https://pypi.tuna.tsinghua.edu.cn/simple
"""
import sys
import warnings
import pandas as pd
import numpy as np
import time
import os
import re
import datetime
import cv2 # OpenCV
import pyautogui # 屏幕截图和键盘/鼠标模拟
import pywinauto
import win32api
import win32con
import win32gui
import win32clipboard
from pywinauto.application import Application
import pywinauto.controls.win32_controls as win32_controls
import pywinauto.controls.hwndwrapper as hwndWrapper
import pywinauto.findwindows as findwindows
from aip import AipOcr # 百度OCR SDK
# --- 关闭警告 ---
warnings.filterwarnings("ignore")
# --- 全局配置 ---
# 百度OCR API 凭证 (!!!务必替换为自己的!!!)
BAIDU_CREDENTIALS = {
'APP_ID': 'YOUR_APP_ID', # <-- 替换为你的百度 App ID
'API_KEY': 'YOUR_API_KEY', # <-- 替换为你的百度 API Key
'SECRET_KEY': 'YOUR_SECRET_KEY' # <-- 替换为你的百度 Secret Key
}
# 默认文档路径 (用于保存临时截图和导出数据)
# 使用 os.path.expanduser('~') 获取当前用户的主目录
DOCUMENTS_PATH = os.path.join(os.path.expanduser('~'), 'Documents')
# 检查路径是否存在,如果不存在,尝试创建或使用备用路径
if not os.path.exists(DOCUMENTS_PATH):
# 尝试在脚本同级目录下创建 data 文件夹作为备用
DOCUMENTS_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ths_auto_data')
try:
os.makedirs(DOCUMENTS_PATH, exist_ok=True)
print(f"[信息] 用户文档目录不存在,已在脚本目录下创建备用路径: {DOCUMENTS_PATH}")
except Exception as e:
print(f"[严重错误] 无法创建备用数据路径 {DOCUMENTS_PATH}: {e}。请手动创建或修复权限。")
# 在极端情况下,可能需要硬编码一个已知可写的路径,但这不推荐
# DOCUMENTS_PATH = 'C:\\temp_ths_data' # 最后的备选方案
# os.makedirs(DOCUMENTS_PATH, exist_ok=True)
# --- 交易自动化核心类 ---
class TradeApiThsSimplified:
"""
同花顺交易接口自动化核心类 (简化版)
"""
def __init__(self):
"""初始化类实例"""
self.handG = None # 主窗口句柄
self.account_name = "未指定账户名" # 当前操作的账户名称 (用于UI适配)
# self.accoundN = "未指定账户ID" # 账户ID在此简化版中可能不需要
self.融资失败转普通 = 0 # 标记融资买入失败时是否尝试普通买入
# 融资列表加载仍然保留,因为它影响委托逻辑
self.融资列表 = []
try:
pathname = './data/dd.txt' # 假设脚本同级或指定数据目录有此文件
if os.path.exists(pathname):
# 指定 dtype=str 防止代码被解释为数字
df = pd.read_csv(pathname, sep='\t', dtype=str, encoding='gbk', errors='ignore')
if '证券代码' in df.columns:
self.融资列表 = list(df['证券代码'])
print(f"成功加载融资列表,共 {len(self.融资列表)} 个标的。")
else:
print(f"[警告] 融资列表文件 {pathname} 中缺少 '证券代码' 列。")
else:
print(f"[信息] 融资列表文件 {pathname} 不存在,将无法使用融资相关特性。")
except Exception as e:
print(f"[错误] 加载融资列表文件失败: {e}")
# --- UI 自动化基础方法 ---
def _获取当前时间戳(self, 格式='日期时间'):
"""获取格式化的当前时间字符串"""
now = datetime.datetime.now()
if 格式 == '日期时间':
return now.strftime('%Y-%m-%d %H:%M:%S')
elif 格式 == '日期':
return now.strftime('%Y-%m-%d')
elif 格式 == '日期时间_': # 用于文件名
return now.strftime('%Y%m%d_%H%M%S')
else:
return str(now)
def 根据句柄得到位置(self, 句柄):
"""获取控件相对于屏幕的位置和大小 [left, top, width, height]"""
if not isinstance(句柄, int) or not win32gui.IsWindow(句柄):
print(f"[警告] 根据句柄得到位置: 无效句柄 {句柄}")
return [0, 0, 0, 0]
try:
rect = win32gui.GetWindowRect(句柄)
return [rect[0], rect[1], rect[2] - rect[0], rect[3] - rect[1]]
except Exception as e:
print(f"[错误] 根据句柄得到位置失败: {e}")
return [0, 0, 0, 0]
def get_screenshot(self, pos, fname):
"""根据指定区域截图并保存。"""
try:
# 确保 pos 里的值都是整数
region_int = tuple(map(int, pos))
img = pyautogui.screenshot(region=region_int)
img.save(fname)
return True
except Exception as e:
print(f"[错误] 截图失败 (区域={pos}, 文件={fname}): {e}")
return False
def _baidu_ocr_internal(self, image_path, ocr_type='高精度'):
"""内部调用百度OCR进行识别。"""
if not (BAIDU_CREDENTIALS['APP_ID'] and BAIDU_CREDENTIALS['API_KEY'] and BAIDU_CREDENTIALS['SECRET_KEY'] and \
'YOUR_' not in BAIDU_CREDENTIALS['APP_ID']):
print("[错误] 百度OCR凭证未配置或包含占位符,请在 BAIDU_CREDENTIALS 中修改。")
return None
if not os.path.exists(image_path):
print(f"[错误] OCR识别失败:图片文件不存在 {image_path}")
return None
try:
client = AipOcr(BAIDU_CREDENTIALS['APP_ID'], BAIDU_CREDENTIALS['API_KEY'], BAIDU_CREDENTIALS['SECRET_KEY'])
except Exception as e:
print(f"[错误] 初始化百度OCR客户端失败: {e}")
return None
try:
with open(image_path, 'rb') as fp:
image_content = fp.read()
except Exception as e:
print(f"[错误] 读取图片文件失败 {image_path}: {e}")
return None
options = {}
results = None
print(f"正在使用百度OCR ({ocr_type}) 识别: {os.path.basename(image_path)}") # 只打印文件名
try:
func = None
if ocr_type == '普通': func = client.basicGeneral
elif ocr_type == '高精度': func = client.basicAccurate
elif ocr_type == '普通带位置': func = client.general
elif ocr_type == '高精度带位置': func = client.accurate
else:
print(f"[错误] 无效的OCR识别类型: {ocr_type}")
return None
results = func(image_content, options)
if results and 'error_code' in results:
print(f"[错误] 百度OCR API错误: Code={results['error_code']}, Msg={results.get('error_msg', 'N/A')}")
elif results and 'words_result' in results:
pass # 成功,由调用者处理结果
# print(f"百度OCR识别成功。")
else:
print("[错误] 百度OCR调用返回为空或格式不正确。")
results = None # 确保返回None
return results
except Exception as e:
print(f"[错误] 调用百度OCR API时发生异常: {e}")
return None
def 验证码处理(self):
"""检测并处理同花顺操作中可能出现的验证码弹窗。"""
out = 1 # 默认成功
handtG = 0 # 验证码窗口句柄
hand_list = pywinauto.findwindows.find_windows(top_level_only=True, class_name='#32770', title='', parent=None)
for handtt in hand_list:
try:
# 控件ID 2393 是常见的验证码提示文本控件
handt_a = win32gui.GetDlgItem(handtt, 2393)
static_wrapper = win32_controls.StaticWrapper(handt_a)
strG3 = static_wrapper.texts()[0] if static_wrapper.texts() else ""
# 检查关键字,更具鲁棒性
if '检测到您正在' in strG3 or '验证码' in strG3 or '安全验证' in strG3:
handtG = handtt
print('[信息] 发现验证码提示窗口')
break
except Exception:
continue
if handtG > 0:
try:
# 控件ID: 2405 (图片), 2404 (输入框), 1 (确定), 2 (取消)
句柄_图片 = win32gui.GetDlgItem(handtG, 2405)
handt_edit = win32gui.GetDlgItem(handtG, 2404)
pos = self.根据句柄得到位置(句柄_图片)
if pos[2] <= 10 or pos[3] <= 10: # 增加对无效截图区域的检查
print("[错误] 无法获取验证码图片位置或大小无效。")
self.button_click(win32gui.GetDlgItem(handtG, 2)) # 尝试点取消
return 0
ts = self._获取当前时间戳('日期时间_')
fname = os.path.join(DOCUMENTS_PATH, f"code_temp_{ts}.png")
if not self.get_screenshot(pos, fname):
self.button_click(win32gui.GetDlgItem(handtG, 2))
return 0
i = 0; is_run = True; typeP = '高精度'; textG = ''
while i < 3 and is_run:
i += 1
result_ocr = self._baidu_ocr_internal(fname, typeP)
if result_ocr and 'words_result' in result_ocr and result_ocr['words_result']:
raw_text = result_ocr['words_result'][0]['words']
# 进一步清理识别结果:只保留字母和数字
cleaned_text = re.sub(r'[^a-zA-Z0-9]', '', raw_text)
if len(cleaned_text) >= 4 : # 验证码长度通常至少4位
textG = cleaned_text
print(f'[信息] 验证码识别成功 ({typeP}): {textG} (原始: {raw_text})')
is_run = False
else:
print(f'[调试] 验证码识别结果过短或无效 ({typeP}): "{cleaned_text}" (来自 "{raw_text}"), 重试...')
typeP = '普通' if typeP == '高精度' else '高精度' # 切换精度
time.sleep(0.5)
else:
print(f'[调试] 验证码识别失败或无结果 ({typeP}), 重试...')
typeP = '普通' if typeP == '高精度' else '高精度'
time.sleep(0.5)
# 清理截图文件
if os.path.exists(fname):
try: os.remove(fname)
except Exception as e: print(f"[警告] 删除临时验证码截图失败: {e}")
if textG and len(textG) >= 4:
content = textG[:4] # 取前4位
print(f'[信息] 输入验证码: {content}')
self.edit_write(handt_edit, content)
time.sleep(0.3)
self.button_click(win32gui.GetDlgItem(handtG, 1)) # 点击确定
time.sleep(1.5)
if not win32gui.IsWindow(handtG):
print('[信息] 验证码窗口已关闭,可能输入成功。')
out = 1
else:
# 检查错误提示 (控件ID: 2406)
try:
hand_msg = win32gui.GetDlgItem(handtG, 2406)
strG3 = win32_controls.StaticWrapper(hand_msg).texts()[0]
if '验证码错误' in strG3:
print(f'[错误] 验证码错误提示: {strG3}')
else:
print(f'[警告] 未知验证码状态,提示信息: {strG3}')
out = 0
except Exception:
print('[警告] 无法读取验证码错误提示,或窗口已变化。')
out = 0 # 不确定时认为失败
finally:
if out == 0: # 如果失败,尝试点取消关闭窗口
self.button_click(win32gui.GetDlgItem(handtG, 2))
else:
print('[错误] 验证码识别失败,无法获取有效验证码文本。')
out = 0
try: self.button_click(win32gui.GetDlgItem(handtG, 2))
except Exception: pass
except Exception as ex:
print(f'[错误] 处理验证码窗口时发生异常: {ex}')
out = 0
try:
if win32gui.IsWindow(handtG):
self.button_click(win32gui.GetDlgItem(handtG, 2))
except Exception: pass
return out
def 另存为处理(self, name):
"""自动化处理“另存为”对话框。"""
content = ''
time.sleep(0.2)
i = 0
handA = []
while not handA and i < 50: # 等待5秒
i += 1
handA = pywinauto.findwindows.find_windows(top_level_only=True, class_name='#32770', title='另存为', parent=None)
if not handA: time.sleep(0.1)
if not handA:
print(f'[错误] 超时:未找到“另存为”窗口 ({name})')
return ''
hand_dialog = handA[0]
try:
# 查找文件名输入框 Edit 控件 (尝试多种方式)
handt_edit = 0
try: # 优先使用 pywinauto 的可靠查找
app_dialog = Application().connect(handle=hand_dialog, timeout=5) # 增加超时
dlg_wrapper = app_dialog.window(handle=hand_dialog)
# 查找 "文件名(&N):" 标签旁边的 Edit 控件
edit_control = dlg_wrapper.child_window(class_name="Edit", control_id=1001) # 1001 是常见的ID
if not edit_control.exists(timeout=1): # 检查是否存在
# 尝试按 AutomationId (如果知道的话)
# edit_control = dlg_wrapper.child_window(auto_id="1001", control_type="Edit")
# 备用:查找第一个可见可用的 Edit
edit_control = dlg_wrapper.child_window(class_name="Edit", visible=True, enabled=True, found_index=0)
if edit_control.exists(timeout=1):
handt_edit = edit_control.handle
# print("[调试] 使用 pywinauto 找到文件名输入框")
else:
raise Exception("Pywinauto 未找到文件名输入框")
except Exception as e_find:
print(f"[调试] Pywinauto 查找输入框失败 ({e_find}), 回退到 GetDlgItem...")
# 回退逻辑 (需要验证ID 1152 或 1001 是否适用)
edit_id_to_try = 1001
if '华泰信用' in self.account_name or '东海信用' in self.account_name:
edit_id_to_try = 1152 # 特定券商的ID?
try:
handt_edit = win32gui.GetDlgItem(hand_dialog, edit_id_to_try)
if not handt_edit: raise Exception(f"GetDlgItem 未找到 ID {edit_id_to_try}")
except Exception as e_getdlg:
print(f"[错误] 使用GetDlgItem查找文件名输入框失败: {e_getdlg}")
try: self.button_click(win32gui.GetDlgItem(hand_dialog, 2)) # 点取消
except: pass
return ''
# 构建完整的文件名路径
content = name + self._获取当前时间戳('日期时间_') + '.txt'
full_path = os.path.join(DOCUMENTS_PATH, content)
# 输入文件名
if not self.edit_write(handt_edit, full_path):
print("[错误] 写入文件名失败。")
try: self.button_click(win32gui.GetDlgItem(hand_dialog, 2)) # 点取消
except: pass
return ''
# 点击“保存”按钮 (ID 1)
self.button_click(win32gui.GetDlgItem(hand_dialog, 1))
time.sleep(0.5)
# 处理可能的“确认另存为”(覆盖)提示框 (非顶层)
i_confirm = 0; handAA = []
while not handAA and i_confirm < 20: # 等待2秒
i_confirm += 1
handAA = pywinauto.findwindows.find_windows(top_level_only=False, class_name='#32770', title='确认另存为')
if not handAA: time.sleep(0.1)
if handAA:
print('[信息] 发现“确认另存为”提示框,点击“是”。')
try:
# “是(&Y)”按钮 ID 通常是 6
self.button_click(win32gui.GetDlgItem(handAA[0], 6))
time.sleep(0.5)
except Exception as ex_confirm:
print(f"[错误] 点击“确认另存为”的是按钮失败: {ex_confirm}")
# 检查另存为窗口是否关闭
time.sleep(0.5) # 等待窗口关闭
if not win32gui.IsWindow(hand_dialog):
print(f"[信息] 文件 '{content}' 应已保存。")
return content
else:
print("[警告] “另存为”窗口未关闭,保存可能失败。尝试点击取消。")
try: self.button_click(win32gui.GetDlgItem(hand_dialog, 2)) # 点取消
except: pass
return ''
except Exception as ex:
print(f'[错误] 处理“另存为”对话框时发生异常: {ex}')
try:
if win32gui.IsWindow(hand_dialog):
self.button_click(win32gui.GetDlgItem(hand_dialog, 2))
except: pass
return ''
def 获取文本数据(self, content, name="数据"):
"""从保存的文本文件中读取数据并转换为DataFrame。"""
if not content: return pd.DataFrame()
pathname = os.path.join(DOCUMENTS_PATH, content)
time.sleep(0.3)
if not os.path.exists(pathname):
print(f"[错误] 获取文本数据: 文件不存在 {pathname} ({name})")
return pd.DataFrame()
df = pd.DataFrame()
try:
# 使用 'gb18030' 编码尝试读取,更兼容GBK
df = pd.read_csv(pathname, sep='\t', encoding='gb18030', errors='ignore', skip_blank_lines=True, dtype=str)
# 移除完全是NaN的行和列
df.dropna(axis=0, how='all', inplace=True)
df.dropna(axis=1, how='all', inplace=True)
if df.empty:
print(f"[警告] 获取文本数据: 文件为空或无法解析 {pathname} ({name})")
else:
# 清理列名中的空白字符
df.columns = df.columns.str.strip()
# 移除最后一列如果是空表头 (常见于导出错误)
if df.columns[-1] == '':
df = df.iloc[:, :-1]
print(f"获取文本数据成功 ({name}),共 {len(df)} 行。")
except Exception as ex:
print(f'[错误] 读取或解析文件 {pathname} ({name}) 失败: {ex}')
df = pd.DataFrame() # 出错则返回空表
finally:
# 尝试删除临时文件
try:
if os.path.exists(pathname): os.remove(pathname)
except Exception as e_del:
print(f"[警告] 删除临时文件失败 {pathname}: {e_del}")
return df
def edit_write(self, handt_edit, content):
"""向指定的Edit控件写入文本内容。"""
if not isinstance(handt_edit, int) or not win32gui.IsWindow(handt_edit):
print(f"[错误] edit_write: 无效的控件句柄 {handt_edit}")
return False
content = str(content)
try:
edit_wrapper = win32_controls.EditWrapper(handt_edit)
edit_wrapper.set_focus()
time.sleep(0.1)
# 使用 set_text 通常比 type_keys 更可靠,特别是对于中文
edit_wrapper.set_edit_text(content)
time.sleep(0.1)
# 验证
if edit_wrapper.text() == content:
return True
else:
# 回退到 type_keys (可能需要特殊处理)
print("[调试] set_text后内容不匹配,尝试 type_keys...")
edit_wrapper.type_keys('^a{DEL}', pause=0.05) # 全选删除
edit_wrapper.type_keys(content, with_spaces=True, pause=0.01)
time.sleep(0.1)
return edit_wrapper.text() == content
except Exception as e:
print(f"[错误] edit_write 失败 (句柄={handt_edit}): {e}")
return False
def get_hand_(self, handG, hand_data):
"""根据控件ID列表逐层获取控件句柄 (脆弱!)"""
handt = handG
if not isinstance(handG, int): return 0
try:
for id_ in hand_data:
if not win32gui.IsWindow(handt): return 0
handt = win32gui.GetDlgItem(handt, int(id_)) # 确保ID是整数
return handt if win32gui.IsWindow(handt) else 0
except Exception:
# print(f"[调试] get_hand_ 失败 (ID路径: {hand_data}): {e}")
return 0
def button_click(self, hand_click):
"""模拟点击按钮控件。"""
if not isinstance(hand_click, int) or not win32gui.IsWindow(hand_click):
# print(f"[调试] button_click: 无效的按钮句柄 {hand_click}")
return False
try:
# 优先使用 pywinauto 的 click_input
button_wrapper = win32_controls.ButtonWrapper(hand_click)
button_wrapper.click_input() # 模拟真实点击
# button_wrapper.click() # 备选方案
time.sleep(0.1)
return True
except Exception as e_click:
print(f"[调试] button_click (pywinauto) 失败: {e_click}, 回退到 PostMessage...")
try: # 回退到 PostMessage
win32gui.SetFocus(hand_click) # 直接设置焦点
time.sleep(0.05)
# 发送 BM_CLICK 消息可能更直接
# win32api.SendMessage(hand_click, win32con.BM_CLICK, 0, 0)
# 或使用原 PostMessage 方式
win32gui.PostMessage(hand_click, win32con.WM_LBUTTONDOWN, win32con.MK_LBUTTON, 0)
time.sleep(0.05)
win32gui.PostMessage(hand_click, win32con.WM_LBUTTONUP, win32con.MK_LBUTTON, 0)
time.sleep(0.1)
return True
except Exception as e_post:
print(f"[错误] button_click (PostMessage) 失败: {e_post}")
return False
def find_pop(self, click_confirm=True):
"""查找常见的提示、确认弹窗,并自动处理。"""
hand_list = pywinauto.findwindows.find_windows(top_level_only=True, class_name='#32770', parent=None)
message_content = ''
for handtt in hand_list:
window_title = win32gui.GetWindowText(handtt)
pop_handled = False
try:
title_text = window_title
# 检查常见标题关键字
if any(keyword in title_text for keyword in ['提示', '确认', '信息', '错误']):
# 尝试获取主要消息文本 (ID 1004, 1040, 65535 等)
message_handle = 0
possible_msg_ids = [1004, 1040, 65535, 2393, 1768]
for msg_id in possible_msg_ids:
try:
h = win32gui.GetDlgItem(handtt, msg_id)
if h and win32gui.IsWindowVisible(h): # 确保可见
message_handle = h
break
except Exception: continue
if message_handle:
# 使用更健壮的方式获取文本
try:
message_content = win32gui.GetWindowText(message_handle)
except Exception:
try: # 备用方案
message_content = win32_controls.StaticWrapper(message_handle).texts()[0]
except:
message_content = "[无法获取消息内容]"
else:
message_content = title_text # 无特定消息控件,用标题代替
print(f"[信息] 发现弹窗 ({title_text}): {message_content[:100]}...") # 截断过长消息
pop_handled = True
if click_confirm:
# 尝试点击默认或常用按钮 (ID: 1=确定, 2=取消/关闭, 6=是, 7=否)
clicked = False
# 优先点 '是'(6) 或 '确定'(1),然后是 '取消/关闭'(2)
for btn_id in [6, 1, 2, 7]:
try:
button_handle = win32gui.GetDlgItem(handtt, btn_id)
if button_handle and win32gui.IsWindowVisible(button_handle):
if self.button_click(button_handle):
print(f"[信息] 自动点击弹窗按钮 (ID: {btn_id})")
clicked = True
time.sleep(0.3)
break
except Exception: continue
if not clicked: print("[警告] 未能自动点击弹窗按钮。")
if pop_handled: break # 通常一次只处理一个主要弹窗
except Exception as ex:
# print(f"[调试] find_pop 检查窗口 {handtt} 时出错: {ex}")
pass
return message_content
def find_pop_lcw(self):
"""专门处理“另存为”相关的弹窗,通常是点击“否”或“取消”。"""
# 1. 处理“确认另存为” (点击“否”,按钮ID 7 或 “取消” ID 2)
try:
hand_list_confirm = pywinauto.findwindows.find_windows(top_level_only=False, class_name='#32770', title='确认另存为')
for handtt in hand_list_confirm:
print("[信息] 发现“确认另存为”窗口,尝试点击“否”或取消。")
clicked = False
for btn_id in [7, 2]: # 优先点'否'(7)
try:
h = win32gui.GetDlgItem(handtt, btn_id)
if h and win32gui.IsWindowVisible(h):
if self.button_click(h): clicked = True; break
except Exception: continue
if not clicked: print("[警告] 未能自动点击“确认另存为”的否/取消按钮。")
time.sleep(0.5)
except Exception as ex: print(f"[错误] 处理“确认另存为”弹窗失败: {ex}")
# 2. 处理残留的“另存为”窗口 (点击“取消”,按钮ID 2)
try:
hand_list_saveas = pywinauto.findwindows.find_windows(top_level_only=True, class_name='#32770', title='另存为')
for handtt in hand_list_saveas:
print("[信息] 发现残留的“另存为”窗口,尝试点击取消。")
try: self.button_click(win32gui.GetDlgItem(handtt, 2))
except Exception as e_cancel: print(f"[错误] 点击“另存为”取消按钮失败: {e_cancel}")
time.sleep(0.5)
except Exception as ex: print(f"[错误] 处理“另存为”弹窗失败: {ex}")
# 3. 处理其他可能的简单“提示”窗口
try: self.find_pop(click_confirm=True)
except Exception as ex: print(f"[错误] 处理通用提示弹窗失败: {ex}")
# --- 同花顺特定业务逻辑 ---
def 初始化(self, window_title='网上股票交易系统5.0'):
"""查找同花顺主窗口并设置句柄 self.handG。"""
try:
# 增加超时以应对启动慢的情况
self.handG = pywinauto.findwindows.find_window(title=window_title, timeout=10)
print(f"成功找到同花顺窗口: '{window_title}', 句柄: {self.handG}")
app = Application().connect(handle=self.handG)
app_window = app.window(handle=self.handG)
if app_window.is_minimized(): app_window.restore()
app_window.set_focus() # 确保窗口获得焦点
time.sleep(0.5)
return True
except pywinauto.findwindows.WindowNotFoundError:
print(f"错误:未找到同花顺客户端窗口,标题:'{window_title}'。请确保同花顺已运行且标题匹配。")
except Exception as e:
print(f"错误:初始化查找同花顺窗口时发生异常: {e}")
self.handG = None
return False
def 获取_资金股份(self, 只获取资金=1):
"""获取账户的资金信息和持仓信息(可选)。"""
if not self.handG or not win32gui.IsWindow(self.handG):
print("错误:同花顺主窗口句柄无效。")
return (None, pd.DataFrame()) if 只获取资金 == 0 else None
start_time = time.time()
self.find_pop_lcw(); self.find_pop() # 清理弹窗
try:
# 发送 F4 (查询/交易界面), F5 (刷新)
for vk_code in [win32con.VK_F4, win32con.VK_F5]:
win32gui.PostMessage(self.handG, win32con.WM_KEYDOWN, vk_code, 0)
win32gui.PostMessage(self.handG, win32con.WM_KEYUP, vk_code, 0)
time.sleep(0.3 if vk_code == win32con.VK_F4 else 0.5) # F5后多等一会儿
# 定位资金信息父面板 (!!! ID 需要适配 !!!)
hand_data_parent = [59648, 59649] # [主区域ID, 查询结果面板ID] - 需要用工具确认
handt_asset_panel = self.get_hand_(self.handG, hand_data_parent)
if not handt_asset_panel:
print("错误:未能定位到资金信息面板,请使用 Spy++ 等工具检查控件ID路径 [59648, 59649] 是否正确。")
return (None, pd.DataFrame()) if 只获取资金 == 0 else None
# 资金控件ID列表 [净资产, 总资产, 可用资金] (!!! ID 需要适配 !!!)
asset_ids = {'华泰信用': [10008, 10008, 10020], '东海信用': [10001, 10000, 10003],
'浙商信用': [20006, 20007, 20003], '默认': [1015, 1015, 1016]}
id_list = asset_ids.get(self.account_name, asset_ids['默认'])
asset_values = []
for ctrl_id in id_list:
try:
h_ctrl = win32gui.GetDlgItem(handt_asset_panel, ctrl_id)
# 优先使用 GetWindowText,更通用
text_value = win32gui.GetWindowText(h_ctrl) if h_ctrl else '0.0'
# text_value = win32_controls.StaticWrapper(h_ctrl).texts()[0] if h_ctrl else '0.0'
cleaned_value = re.sub(r'[^\d.-]', '', text_value) # 清理字符
numeric_value = float(cleaned_value) / 10000.0 if cleaned_value else 0.0
except ValueError: numeric_value = 0.0
except Exception as e_get:
print(f"[警告] 获取资金控件文本失败 (ID={ctrl_id}): {e_get}")
numeric_value = 0.0
asset_values.append(round(numeric_value, 4)) # 保留4位小数以提高精度
asset_data = [self._获取当前时间戳()] + asset_values
namelist = ['时间', '净资产(万)', '总资产(万)', '可用资金(万)']
asset_df = pd.DataFrame([asset_data], columns=namelist)
print(f"获取资金信息: 净资产={asset_values[0]:.4f}万, 总资产={asset_values[1]:.4f}万, 可用={asset_values[2]:.4f}万")
position_df = pd.DataFrame()
if 只获取资金 == 0:
print("正在获取持仓信息...")
try:
# 定位持仓列表控件 (!!! ID 需要适配 !!!)
hand_data_pos = [1047, 200, 1047] # [Tab?, Grid父容器?, Grid?] - 需要确认
handt_position_list = self.get_hand_(handt_asset_panel, hand_data_pos)
if not handt_position_list:
print("错误:未能定位到持仓列表控件,请检查控件ID路径 [1047, 200, 1047]。")
else:
hwndWrapper.HwndWrapper(handt_position_list).set_focus()
time.sleep(0.2)
# 尝试使用“另存为”获取持仓
pyautogui.hotkey('ctrl', 's')
time.sleep(0.2)
self.验证码处理()
content = self.另存为处理('股份持仓')
if content:
position_df = self.获取文本数据(content, '股份持仓')
# else: # 导出失败时不再尝试复制,因为复制更不可靠
# print("[警告] 通过“另存为”获取持仓失败。")
if not position_df.empty:
# 标准化列名 (保持与原逻辑一致,但需注意映射准确性)
target_columns = ['证券代码', '证券名称', '证券数量', '可卖数量', '成本价', '当前价']
col_map = {'默认': {'证券代码': '证券代码', '证券名称': '证券名称', '股票余额': '证券数量', '可用余额': '可卖数量', '成本价': '成本价', '市价': '当前价'},
'浙商信用': {'证券代码': '证券代码', '证券名称': '证券名称', '股票数量': '证券数量', '可用数量': '可卖数量', '成本价': '成本价', '市价': '当前价'},
'东海信用': {'证券代码': '证券代码', '证券名称': '证券名称', '今余额': '证券数量', '可卖量': '可卖数量', '成本价': '成本价', '最新价': '当前价'}}
current_map = col_map.get(self.account_name, col_map['默认'])
rename_dict = {k: v for k, v in current_map.items() if k in position_df.columns}
position_df = position_df.rename(columns=rename_dict)
# 检查目标列是否存在
valid_cols = [col for col in target_columns if col in position_df.columns]
if len(valid_cols) < 4: # 至少需要代码、名称、数量、价格中的一些
print(f"[错误] 持仓数据缺少关键列。可用列: {position_df.columns.tolist()}")
position_df = pd.DataFrame()
else:
position_df = position_df[valid_cols]
# 数据类型转换和清理
for col in ['证券数量', '可卖数量']:
if col in position_df.columns:
position_df[col] = pd.to_numeric(position_df[col].astype(str).str.replace(',', '', regex=False), errors='coerce').fillna(0).astype(int)
for col in ['成本价', '当前价']:
if col in position_df.columns:
position_df[col] = pd.to_numeric(position_df[col], errors='coerce').fillna(0.0)
if '证券数量' in position_df.columns:
position_df = position_df[position_df['证券数量'] > 0].copy()
if not position_df.empty:
position_df.insert(0, '时间', self._获取当前时间戳())
print(f'获取持仓成功,共 {len(position_df)} 条记录。')
else:
print('获取持仓数据成功,但无有效持仓记录。')
else:
print("获取持仓数据失败或持仓为空。")
except Exception as ex:
print(f'[错误] 获取持仓信息时发生异常: {ex}')
position_df = pd.DataFrame()
elapsed = time.time() - start_time
print(f"--- 获取资金/股份 耗时: {elapsed:.2f} 秒 ---")
return (asset_df, position_df) if 只获取资金 == 0 else asset_df
except Exception as e_main:
print(f"[错误] 获取资金股份主流程异常: {e_main}")
return (None, pd.DataFrame()) if 只获取资金 == 0 else None
def 获取_当日委托(self):
"""获取当日委托列表。"""
if not self.handG or not win32gui.IsWindow(self.handG):
print("错误:同花顺主窗口句柄无效。")
return pd.DataFrame()
start_time = time.time()
self.find_pop_lcw(); self.find_pop()
try:
# F1 (买入界面,通常委托查询也在此附近), F5 (刷新)
win32gui.PostMessage(self.handG, win32con.WM_KEYDOWN, win32con.VK_F1, 0)
win32gui.PostMessage(self.handG, win32con.WM_KEYUP, win32con.VK_F1, 0); time.sleep(0.3)
win32gui.PostMessage(self.handG, win32con.WM_KEYDOWN, win32con.VK_F5, 0)
win32gui.PostMessage(self.handG, win32con.WM_KEYUP, win32con.VK_F5, 0); time.sleep(0.5)
# 定位委托查询面板 (!!! ID 需要适配 !!!)
hand_data_panel = [59648, 59649]
handt_panel = self.get_hand_(self.handG, hand_data_panel)
if not handt_panel: print("错误:未能定位到委托查询面板。"); return pd.DataFrame()
hwndWrapper.HwndWrapper(handt_panel).set_focus()
# 切换到“当日委托”标签页 (!!! 快捷键需要适配 !!!)
if '华泰信用' in self.account_name:
pyautogui.hotkey('alt', 'r') # 华泰快捷键?
print("[信息] 尝试发送 Alt+R 切换到当日委托 (华泰)。")
else:
# 通用 F8 尝试
win32gui.PostMessage(handt_panel, win32con.WM_KEYDOWN, win32con.VK_F8, 0)
win32gui.PostMessage(handt_panel, win32con.WM_KEYUP, win32con.VK_F8, 0)
print("[信息] 尝试发送 F8 切换到当日委托。")
time.sleep(0.5)
# 定位委托列表控件 (!!! ID 需要适配 !!!)
hand_data_list = [1047, 200, 1047]
handt_order_list = self.get_hand_(handt_panel, hand_data_list)
if not handt_order_list: print("错误:未能定位到当日委托列表控件。"); return pd.DataFrame()
hwndWrapper.HwndWrapper(handt_order_list).set_focus(); time.sleep(0.2)
# 使用“另存为”导出数据
pyautogui.hotkey('ctrl', 's'); time.sleep(0.2)
self.验证码处理()
content = self.另存为处理('当日委托')
if not content: print("错误:导出当日委托失败。"); return pd.DataFrame()
df = self.获取文本数据(content, '当日委托')
if not df.empty:
# 标准化列名
target_columns = ['委托时间', '证券代码', '证券名称', '操作', '委托编号', '委托数量', '成交数量', '委托价格', '状态说明']
common_map = {'委托时间': '委托时间', '时间': '委托时间', '证券代码': '证券代码', '代码': '证券代码',
'证券名称': '证券名称', '名称': '证券名称', '买卖标志': '操作', '操作': '操作',
'委托类别': '备注', '备注': '备注', '合同编号': '委托编号', '委托编号': '委托编号',
'委托数量': '委托数量', '数量': '委托数量', '成交数量': '成交数量',
'委托价格': '委托价格', '价格': '委托价格', '状态说明': '状态说明', '委托状态': '状态说明'}
rename_dict = {k: v for k, v in common_map.items() if k in df.columns and v not in df.columns} # 避免覆盖已有目标列
df = df.rename(columns=rename_dict)
# 合并操作和备注
if '操作' in df.columns and '备注' in df.columns:
df['操作'] = df['操作'].astype(str).fillna('') + df['备注'].astype(str).fillna('')
df = df.drop(columns=['备注'], errors='ignore')
elif '备注' in df.columns and '操作' not in df.columns:
df = df.rename(columns={'备注': '操作'})
final_cols = [col for col in target_columns if col in df.columns]
if not final_cols or '委托编号' not in final_cols:
print(f"[错误] 当日委托数据解析失败,缺少关键列。原始列: {df.columns.tolist()}")
return pd.DataFrame()
df = df[final_cols]
# 数据类型转换
for col in ['委托数量', '成交数量']:
if col in df.columns: df[col] = pd.to_numeric(df[col].astype(str).str.replace(',', '', regex=False), errors='coerce').fillna(0).astype(int)
if '委托价格' in df.columns: df['委托价格'] = pd.to_numeric(df['委托价格'], errors='coerce').fillna(0.0)
df = df.fillna('') # 填充NaN为空字符串
print(f'获取当日委托成功,共 {len(df)} 条记录。')
else:
print("获取当日委托数据失败或列表为空。")
df = pd.DataFrame(columns=target_columns)
elapsed = time.time() - start_time
print(f"--- 获取当日委托 耗时: {elapsed:.2f} 秒 ---")
return df
except Exception as e_main:
print(f"[错误] 获取当日委托主流程异常: {e_main}")
return pd.DataFrame()
def 融资买入点击(self):
"""尝试点击下拉列表切换到“融资买入”模式 (脆弱!)"""
if not self.handG or not win32gui.IsWindow(self.handG): return False
try:
# 定位买卖方向 ComboBox (!!! ID和坐标需要适配 !!!)
# 假设在 F1 面板 [59648, 59649] 下,控件 ID 1008
handt_panel = self.get_hand_(self.handG, [59648, 59649])
if not handt_panel: print("错误:未能定位到买入界面面板 (融资买入点击)。"); return False
combo_handle = win32gui.GetDlgItem(handt_panel, 1008) # 假设ID
if not combo_handle: print("错误:未能定位买卖方向ComboBox (ID 1008)。"); return False
# 获取当前选择,如果已经是融资买入则跳过
try:
current_text = win32_controls.ComboBoxWrapper(combo_handle).selected_text()
if "融资买入" in current_text:
# print("[调试] 当前已是融资买入模式。")
return True
except Exception: pass # 获取失败则继续尝试切换
win32_controls.ComboBoxWrapper(combo_handle).click(); time.sleep(0.3)
# 根据账户名计算点击坐标 (!!! 非常脆弱 !!!)
y_offset = {'银河信用': 150, '中信信用': 100, '浙商信用': 125, '东海信用': 100, '默认': 78}
pos = [35, y_offset.get(self.account_name, y_offset['默认'])]
self.按位置点击(combo_handle, pos); time.sleep(0.2)
# 验证切换结果 (检查 ComboBox 文本)
time.sleep(0.5) # 等待文本更新
try:
selected_text = win32_controls.ComboBoxWrapper(combo_handle).selected_text()
if "融资买入" in selected_text:
print("[信息] 成功切换到融资买入模式。")
return True
else:
print(f"[错误] 切换融资买入失败,当前选项: '{selected_text}'")
return False
except Exception as e_verify:
print(f"[错误] 验证融资买入切换失败: {e_verify}")
return False
except Exception as e_main:
print(f"[错误] 切换融资买入模式时发生异常: {e_main}")
return False
def 按位置点击(self, handt, pos):
"""在控件内部的相对坐标处模拟鼠标点击 (脆弱!)"""
if not isinstance(handt, int) or not win32gui.IsWindow(handt): return
try:
client_pos = win32gui.ClientToScreen(handt, tuple(map(int, pos)))
# 使用pyautogui点击可能更兼容
pyautogui.click(x=client_pos[0], y=client_pos[1])
# win32api.SetCursorPos(client_pos)
# time.sleep(0.05)
# win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN | win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
time.sleep(0.2)
except Exception as e:
print(f"[错误] 按位置点击失败 (句柄={handt}, 位置={pos}): {e}")
def 委托(self, 方向方向, 股票代码, 委托价格, 委托数量):
"""执行买入、卖出、融资买入等委托操作。"""
if not self.handG or not win32gui.IsWindow(self.handG): return 0
start_time = time.time()
try:
股票代码 = str(股票代码)[:6]
委托数量 = int(委托数量)
委托价格 = round(float(委托价格), 3 if 股票代码.startswith(('688','11','5')) else 2)
except ValueError: print("错误:委托参数类型无效。"); return 0
委托方向数据 = {'0': '买入', '1': '卖出', '2': '融资买入', '11': '全部卖出'}
委托方向 = 委托方向数据.get(str(方向方向)) or 方向方向 # 也接受直接传入中文
if 委托方向 not in 委托方向数据.values(): print(f"错误:无效委托方向'{方向方向}'"); return 0
self.find_pop_lcw(); self.find_pop() # 清理弹窗
if 委托数量 <= 0 and 委托方向 != '全部卖出': print("提示:委托数量为0,不执行。"); return 0
if 股票代码.startswith('688') and 委托数量 < 200 and '买' in 委托方向:
print(f"提示:科创板买入不足200股({委托数量}),不执行。"); return 0
# --- 切换界面 ---
vk_code = None
if 委托方向 in ['买入', '融资买入']: vk_code = win32con.VK_F1
elif 委托方向 in ['卖出', '全部卖出']: vk_code = win32con.VK_F2
if vk_code:
win32gui.PostMessage(self.handG, win32con.WM_KEYDOWN, vk_code, 0)
win32gui.PostMessage(self.handG, win32con.WM_KEYUP, vk_code, 0); time.sleep(0.3)
else: print("错误:无法确定委托界面快捷键。"); return 0
if 委托方向 == '融资买入':
if not self.融资买入点击():
if self.融资失败转普通:
print("警告:切换融资买入失败,尝试执行普通买入。")
委托方向 = '买入' # 注意: 数量检查逻辑可能需要调整
else:
print("错误:切换融资买入失败,取消委托。"); return 0
time.sleep(0.2)
# --- 定位和填写 ---
# (!!! IDs 需要适配 !!!)
handt_panel = self.get_hand_(self.handG, [59648, 59649])
if not handt_panel: print("错误:未能定位委托面板。"); return 0
id_list = {'code': 1032, 'price': 1033, 'volume': 1034, 'submit': 1006}
id_max_vol = {'买入': 1018, '卖出': 1038, '融资买入': 1040, '全部卖出': 1038}
try:
# 填代码
handt_code = win32gui.GetDlgItem(handt_panel, id_list['code'])
if not self.edit_write(handt_code, 股票代码): return 0
time.sleep(0.4) # 等待价格联动
# 填价格
handt_price = win32gui.GetDlgItem(handt_panel, id_list['price'])
if not self.edit_write(handt_price, str(委托价格)): return 0
time.sleep(0.1)
# 处理数量
handt_volume = win32gui.GetDlgItem(handt_panel, id_list['volume'])
final_volume_str = "0"; not_trade_vol = 0; max_available_vol = -1
max_vol_id = id_max_vol.get(委托方向)
if max_vol_id:
try:
h_max = win32gui.GetDlgItem(handt_panel, max_vol_id)
text = win32gui.GetWindowText(h_max) if h_max else ''
if text: max_available_vol = int(float(re.sub(r'[^\d.]', '', text)))
except Exception: pass # 获取失败则为 -1
if 委托方向 == '全部卖出':
final_volume_str = str(max_available_vol) if max_available_vol >= 0 else "0"
else: # 买入、卖出、融资买入
if max_available_vol >= 0:
final_volume = min(委托数量, max_available_vol)
final_volume_str = str(final_volume)
if 委托方向 == '融资买入': not_trade_vol = 委托数量 - final_volume
else:
final_volume_str = str(委托数量) # 未获取到最大值,按原数委托
print(f"[警告] 未获取到最大可交易数量({委托方向}),按请求数量委托。")
final_volume_int = int(float(final_volume_str)) if final_volume_str else 0
if final_volume_int <= 0: print(f"提示:最终计算可委托数量为0,取消。"); return 0
if not self.edit_write(handt_volume, final_volume_str): return 0
# 点击下单
hand_click_submit = win32gui.GetDlgItem(handt_panel, id_list['submit'])
if not self.button_click(hand_click_submit): print("错误:点击下单按钮失败。"); return 0
time.sleep(0.5)
# --- 处理结果 ---
委托成功 = False; 委托编号 = ""; final_message = ""
for _ in range(3): # 检查几次弹窗
pop_msg = self.find_pop(click_confirm=True)
if pop_msg:
final_message += pop_msg + " | "
if any(k in pop_msg for k in ['成功提交', '合同编号', '已受理', '成功']):
委托成功 = True
extracted_no = "".join(filter(str.isdigit, pop_msg))
if len(extracted_no) > 5: 委托编号 = extracted_no
else: break
time.sleep(0.2)
委托金额 = round(委托价格 * final_volume_int / 10000.0, 2)
log_prefix = f"{self.account_name} - {委托方向} - {股票代码}"
log_details = f"价格:{委托价格}, 数量:{final_volume_int}, 金额:{委托金额:.2f}万"
if 委托成功:
委托编号 = 委托编号 if 委托编号 else "未知(成功)"
print(f"{log_prefix} - {log_details} --- 成功 --- 回报: {final_message.strip()}")
result = 委托编号
else:
print(f"{log_prefix} - {log_details} --- 失败 --- 回报: {final_message.strip()}")
result = 0
# 处理融资不足转普通 (仅提示)
if not_trade_vol > 0 and 委托方向 == '融资买入' and self.融资失败转普通:
print(f"提示:{self.account_name} 融资买入额度不足 {not_trade_vol} 股,考虑普通买入...")
elapsed = time.time() - start_time
print(f"--- 委托操作 {委托方向} 耗时: {elapsed:.2f} 秒 ---")
return result
except Exception as e_main:
print(f"[错误] 执行委托时发生异常: {e_main}")
return 0
def 撤单(self, 委托编号, 股票代码=''):
"""根据委托编号撤销指定的未成交委托。"""
if not self.handG or not win32gui.IsWindow(self.handG): return False
if not 委托编号: print("错误:撤单需要提供委托编号。"); return False
start_time = time.time(); 委托编号 = str(委托编号)
print(f"准备撤单: 股票代码={股票代码}, 委托编号={委托编号}")
self.find_pop_lcw(); self.find_pop()
try:
# F3 (撤单界面), F5 (刷新)
win32gui.PostMessage(self.handG, win32con.WM_KEYDOWN, win32con.VK_F3, 0)
win32gui.PostMessage(self.handG, win32con.WM_KEYUP, win32con.VK_F3, 0); time.sleep(0.3)
win32gui.PostMessage(self.handG, win32con.WM_KEYDOWN, win32con.VK_F5, 0)
win32gui.PostMessage(self.handG, win32con.WM_KEYUP, win32con.VK_F5, 0); time.sleep(0.5)
# 定位撤单面板和列表 (!!! IDs 需要适配 !!!)
handt_panel = self.get_hand_(self.handG, [59648, 59649])
if not handt_panel: print("错误:未能定位撤单面板。"); return False
hwndWrapper.HwndWrapper(handt_panel).set_focus()
handt_cancel_list = self.get_hand_(handt_panel, [1047, 200, 1047]) # 假设列表ID路径
if not handt_cancel_list: print("错误:未能定位可撤单列表控件。"); return False
hwndWrapper.HwndWrapper(handt_cancel_list).set_focus(); time.sleep(0.2)
# 导出可撤单列表查找委托行 (更可靠的方式)
pyautogui.hotkey('ctrl', 's'); time.sleep(0.2)
self.验证码处理()
content = self.另存为处理('可撤单列表')
if not content: print("错误:导出可撤单列表失败。"); return False
df = self.获取文本数据(content, '可撤单列表')
if df.empty: print("错误:可撤单列表为空或读取失败。"); return False
target_col = next((col for col in ['合同编号', '委托编号'] if col in df.columns), None)
if not target_col: print("错误:可撤单列表缺少编号列。"); return False
df[target_col] = df[target_col].astype(str)
dfG = df[df[target_col] == 委托编号]
if dfG.empty: print(f"提示:未找到委托编号 {委托编号} (可能已完成或已撤)。"); return True # 找不到也算"成功"
loc = dfG.index[0]
print(f"找到委托 {委托编号} 在列表第 {loc} 行。")
# 点击选中行并撤单 (!!! 坐标点击非常脆弱 !!!)
# header_h=20; row_h=22; y_pos = header_h + loc * row_h + row_h // 2; x_pos = 10
# self.按位置点击(handt_cancel_list, [x_pos, y_pos]); time.sleep(0.3)
# 尝试通过 pywinauto 选中行 (更优选择)
try:
list_wrapper = hwndWrapper.HwndWrapper(handt_cancel_list)
# 假设是 ListView 或类似控件
list_wrapper.select(loc) # 按行号选中
# 或者如果支持文本查找:
# list_wrapper.get_item(委托编号, column=?).select() # ?是委托编号列号
time.sleep(0.3)
except Exception as e_select:
print(f"[警告] Pywinauto 选中行失败: {e_select}, 回退到坐标点击...")
header_h=20; row_h=22; y_pos = header_h + loc * row_h + row_h // 2; x_pos = 10
self.按位置点击(handt_cancel_list, [x_pos, y_pos]); time.sleep(0.3)
# 点击“撤单”按钮 (!!! ID 需要适配 !!!)
hand_cancel_button = win32gui.GetDlgItem(handt_panel, 1099) # 假设ID
if not self.button_click(hand_cancel_button): print("错误:点击撤单按钮失败。"); return False
time.sleep(0.5)
# 处理结果
final_message = ""; 撤单发出 = False
for _ in range(3):
pop_msg = self.find_pop(click_confirm=True)
if pop_msg:
final_message += pop_msg + " | "
if any(k in pop_msg for k in ['已提交', '成功', '已发送']): 撤单发出 = True
else: break
time.sleep(0.2)
if 撤单发出: print(f"撤单指令已发出: {委托编号} --- 回报: {final_message.strip()}"); result = True
else: print(f"撤单指令发出失败: {委托编号} --- 回报: {final_message.strip()}"); result = False
elapsed = time.time() - start_time
print(f"--- 撤单操作 {委托编号} 耗时: {elapsed:.2f} 秒 ---")
return result
except Exception as e_main:
print(f"[错误] 执行撤单时发生异常: {e_main}")
return False
def test(self):
"""执行简单的测试流程。"""
print("\n" + "="*20 + " 开始执行测试 " + "="*20)
asset, positions = self.获取_资金股份(只获取资金=0)
if asset is not None: print("\n--- 初始资金 ---\n", asset)
if not positions.empty: print("\n--- 初始持仓 ---\n", positions.head())
print("\n--- 测试买入 (000001 平安银行) ---")
order_id_buy = self.委托(0, '000001', 10.01, 100) # 使用较低价格确保大概率不成交,便于撤单
if order_id_buy:
print(f"买入委托提交,编号: {order_id_buy}")
time.sleep(1)
orders = self.获取_当日委托()
if not orders.empty:
print("\n--- 当前委托 ---")
print(orders[orders['委托编号'] == str(order_id_buy)])
print(f"\n--- 测试撤单 (编号: {order_id_buy}) ---")
self.撤单(order_id_buy)
else:
print("买入委托失败。")
print("\n--- 测试卖出 (持仓第一只) ---")
if not positions.empty and '证券代码' in positions.columns and '可卖数量' in positions.columns:
code_sell = positions['证券代码'].iloc[0]
vol_sell = positions['可卖数量'].iloc[0]
price_sell = positions['当前价'].iloc[0] * 1.05 if '当前价' in positions.columns else 100.0 # 较高价格确保不成交
if vol_sell >= 100: # 至少卖100股
vol_sell = 100
print(f"尝试卖出 {code_sell} {vol_sell} 股 @ {price_sell:.2f}")
order_id_sell = self.委托(1, code_sell, price_sell, vol_sell)
if order_id_sell:
print(f"卖出委托提交,编号: {order_id_sell}")
time.sleep(1)
print(f"\n--- 测试撤单 (编号: {order_id_sell}) ---")
self.撤单(order_id_sell)
else: print("卖出委托失败。")
else: print(f"持仓 {code_sell} 可卖数量不足100,跳过卖出测试。")
else: print("无持仓或缺少必要列,跳过卖出测试。")
print("\n--- 再次获取资金 ---")
asset_final = self.获取_资金股份(只获取资金=1)
if asset_final is not None: print("\n--- 最终资金 ---\n", asset_final)
print("="*20 + " 测试执行完毕 " + "="*20 + "\n")
# --- 主程序入口 ---
if __name__ == '__main__':
print("--- 同花顺自动化脚本启动 ---")
# --- 配置账户信息 ---
# !!! 请根据您的实际情况修改 !!!
账户配置 = {
'account_name': '模拟账户_默认', # 用于UI适配 (例如: '华泰信用', '浙商普通', '默认')
'window_title': '网上股票交易系统5.0' # 同花顺主窗口标题 (需要完全匹配)
}
# --- 初始化交易对象 ---
交易接口 = TradeApiThsSimplified()
交易接口.account_name = 账户配置['account_name']
# --- 连接到同花顺客户端 ---
if 交易接口.初始化(window_title=账户配置['window_title']):
print("成功连接到同花顺客户端。")
# --- 执行测试流程 ---
try:
交易接口.test()
except Exception as e_test:
print(f"\n[严重错误] 测试过程中发生未捕获异常: {e_test}")
import traceback
traceback.print_exc()
else:
print("未能连接到同花顺客户端,程序退出。请检查同花顺是否已启动且窗口标题正确。")
print("--- 程序结束 ---")
代码总结:
- 目的: 该脚本旨在通过模拟用户操作(UI自动化)与同花顺PC客户端进行交互,实现自动化的信息查询和交易执行。
- 核心技术:
pywinauto
&win32api/gui
: 用于查找窗口、获取控件句柄、读取/写入控件内容、发送键盘/鼠标消息。这是实现窗口自动化的基础。pyautogui
: 用于屏幕截图(验证码)和模拟键盘快捷键、鼠标点击(作为win32api
的补充或替代)。baidu-aip
(百度OCR): 用于识别验证码图片中的文字,是处理登录或操作中安全验证的关键。pandas
&numpy
: 用于处理从同花顺导出或复制的表格数据(如持仓、委托列表),方便数据分析和操作。os
,re
,time
,datetime
: 标准库,用于文件操作、正则表达式(文本清理)、时间控制和日期处理。
- 主要功能:
- 初始化 (
初始化
): 查找并连接到指定标题的同花顺主窗口。 - 验证码处理 (
验证码处理
): 自动检测验证码弹窗,截图,调用百度OCR识别,输入验证码并确认。 - 数据导出与解析 (
另存为处理
,获取文本数据
): 通过模拟"另存为"操作将表格数据(资金、持仓、委托)保存到文本文件,然后读取并解析为 DataFrame。这是相对稳定的数据获取方式。 - 数据查询 (
获取_资金股份
,获取_当日委托
): 封装了查询操作,调用基础方法获取资金、持仓或委托信息,并进行数据标准化处理。 - 交易执行 (
委托
): 实现买入、卖出、融资买入等下单操作。根据类型切换界面,填写代码、价格、数量(会检查可交易量),点击下单,并处理确认弹窗。 - 撤单 (
撤单
): 根据委托编号撤销未成交订单。切换到撤单界面,导出列表找到对应订单,模拟选中并点击撤单,处理确认。 - UI交互辅助: 提供了如
button_click
,edit_write
等封装好的底层UI操作函数。
- 初始化 (
- 关键配置与注意点:
- 百度OCR凭证 (
BAIDU_CREDENTIALS
): 必须替换为用户自己的有效 App ID, API Key, Secret Key。 - 窗口标题 (
window_title
): 需要与用户实际运行的同花顺客户端窗口标题完全一致。 - 控件ID和坐标: 脚本中大量使用了硬编码的控件ID(如
[59648, 59649]
,1032
,1099
)和相对坐标(用于点击下拉选项)。这些值极度依赖于具体的同花顺版本和券商定制界面,很可能需要用户使用 Spy++ 或类似工具自行查找并修改才能正常工作。这是此类UI自动化脚本最脆弱的部分。 - 账户名称 (
account_name
): 用于区分不同券商的界面差异(如不同的控件ID、快捷键、导出列名)。需要正确设置。 - 文件路径 (
DOCUMENTS_PATH
): 脚本会尝试在用户文档目录下读写临时文件,请确保有相应权限。如果失败,会尝试在脚本目录创建ths_auto_data
子目录。 - 错误处理: 脚本包含基本的
try...except
块,但可能不够详尽。实际使用中可能需要更完善的错误捕获和日志记录。
- 百度OCR凭证 (
- 简化之处:
- 移除了与
basic
,redis_api
,spider_api
相关的代码,使其成为一个独立的UI自动化工具。 - 去掉了后台任务、跟单系统等复杂逻辑。
- 简化了日志输出,主要使用
print
。
- 移除了与
使用建议:
- 仔细检查并修改配置: 特别是百度OCR凭证、窗口标题和账户名称。
- 准备控件检查工具: 如 Spy++, inspect.exe, 或 pywinauto 的
print_control_identifiers()
来检查和调整控件ID/路径。 - 小范围测试: 先使用
test()
函数或单独调用查询函数进行测试,确认基本功能正常后再尝试交易操作。 - 注意风险: UI自动化本质上不如官方API稳定,界面变动可能导致脚本失效。交易操作涉及资金风险,请务必谨慎测试和使用。