同花顺 模拟点击 实盘交易多年稳定运行脚本 利用谷歌大模型重构后分享

有需要的可以自己行学习修改调试。
支持信用账户(具体要自己调一下参数)
稳定运行多年 支持最新客户端

该脚本将包含:

  1. 核心UI自动化: 使用 pywinauto, win32api, pyautogui 控制同花顺。
  2. 百度OCR验证码识别: 处理操作过程中可能出现的验证码。
  3. 同花顺基本操作: 查询资金/持仓、当日委托、下单、撤单。
  4. 中文注释和输出: 所有代码注释和打印信息将使用中文。

注意:

  • 请务必替换百度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("--- 程序结束 ---")

代码总结:

  1. 目的: 该脚本旨在通过模拟用户操作(UI自动化)与同花顺PC客户端进行交互,实现自动化的信息查询和交易执行。
  2. 核心技术:
    • pywinauto & win32api/gui: 用于查找窗口、获取控件句柄、读取/写入控件内容、发送键盘/鼠标消息。这是实现窗口自动化的基础。
    • pyautogui: 用于屏幕截图(验证码)和模拟键盘快捷键、鼠标点击(作为 win32api 的补充或替代)。
    • baidu-aip (百度OCR): 用于识别验证码图片中的文字,是处理登录或操作中安全验证的关键。
    • pandas & numpy: 用于处理从同花顺导出或复制的表格数据(如持仓、委托列表),方便数据分析和操作。
    • os, re, time, datetime: 标准库,用于文件操作、正则表达式(文本清理)、时间控制和日期处理。
  3. 主要功能:
    • 初始化 (初始化): 查找并连接到指定标题的同花顺主窗口。
    • 验证码处理 (验证码处理): 自动检测验证码弹窗,截图,调用百度OCR识别,输入验证码并确认。
    • 数据导出与解析 (另存为处理, 获取文本数据): 通过模拟"另存为"操作将表格数据(资金、持仓、委托)保存到文本文件,然后读取并解析为 DataFrame。这是相对稳定的数据获取方式。
    • 数据查询 (获取_资金股份, 获取_当日委托): 封装了查询操作,调用基础方法获取资金、持仓或委托信息,并进行数据标准化处理。
    • 交易执行 (委托): 实现买入、卖出、融资买入等下单操作。根据类型切换界面,填写代码、价格、数量(会检查可交易量),点击下单,并处理确认弹窗。
    • 撤单 (撤单): 根据委托编号撤销未成交订单。切换到撤单界面,导出列表找到对应订单,模拟选中并点击撤单,处理确认。
    • UI交互辅助: 提供了如 button_click, edit_write 等封装好的底层UI操作函数。
  4. 关键配置与注意点:
    • 百度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 块,但可能不够详尽。实际使用中可能需要更完善的错误捕获和日志记录。
  5. 简化之处:
    • 移除了与 basic, redis_api, spider_api 相关的代码,使其成为一个独立的UI自动化工具。
    • 去掉了后台任务、跟单系统等复杂逻辑。
    • 简化了日志输出,主要使用 print

使用建议:

  • 仔细检查并修改配置: 特别是百度OCR凭证、窗口标题和账户名称。
  • 准备控件检查工具: 如 Spy++, inspect.exe, 或 pywinauto 的 print_control_identifiers() 来检查和调整控件ID/路径。
  • 小范围测试: 先使用 test() 函数或单独调用查询函数进行测试,确认基本功能正常后再尝试交易操作。
  • 注意风险: UI自动化本质上不如官方API稳定,界面变动可能导致脚本失效。交易操作涉及资金风险,请务必谨慎测试和使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值