From f2eeecdcb28abd98ffbbfb36197279e913f58c95 Mon Sep 17 00:00:00 2001 From: lintaogood Date: Mon, 28 Jul 2025 12:15:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=8A=A0=E9=A1=B9=E7=9B=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6=EF=BC=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 3 + .idea/MarsCodeWorkspaceAppSettings.xml | 6 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 7 + .idea/modules.xml | 8 + .idea/real_view.iml | 8 + .idea/vcs.xml | 6 + StatusStyleDelegate.py | 80 ++ log_style_manager.py | 99 ++ logger_utils.py | 82 ++ logger_utils_new.py | 73 ++ order_executor.ahk | 58 + order_executor.py | 172 +++ pyauto操控-测试.py | 113 ++ quote_manager.py | 111 ++ stock_monitor_pyside.py | 894 ++++++++++++++ strategy.py | 187 +++ strategy_monitor.py | 166 +++ 测试sina.py | 12 +- 短线速逆_历史.py | 820 +++++++++++++ 短线速逆_监控.py | 1050 +++++++++++++++++ 短线速逆_监控.spec | 38 + 类-界面.py | 79 ++ 自动下单PYautogui测试.py | 319 +++++ 24 files changed, 4395 insertions(+), 2 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/MarsCodeWorkspaceAppSettings.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/real_view.iml create mode 100644 .idea/vcs.xml create mode 100644 StatusStyleDelegate.py create mode 100644 log_style_manager.py create mode 100644 logger_utils.py create mode 100644 logger_utils_new.py create mode 100644 order_executor.ahk create mode 100644 order_executor.py create mode 100644 pyauto操控-测试.py create mode 100644 quote_manager.py create mode 100644 stock_monitor_pyside.py create mode 100644 strategy.py create mode 100644 strategy_monitor.py create mode 100644 短线速逆_历史.py create mode 100644 短线速逆_监控.py create mode 100644 短线速逆_监控.spec create mode 100644 类-界面.py create mode 100644 自动下单PYautogui测试.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..359bb53 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml diff --git a/.idea/MarsCodeWorkspaceAppSettings.xml b/.idea/MarsCodeWorkspaceAppSettings.xml new file mode 100644 index 0000000..05ed8ba --- /dev/null +++ b/.idea/MarsCodeWorkspaceAppSettings.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..5f1aa97 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..726c760 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/real_view.iml b/.idea/real_view.iml new file mode 100644 index 0000000..8437fe6 --- /dev/null +++ b/.idea/real_view.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/StatusStyleDelegate.py b/StatusStyleDelegate.py new file mode 100644 index 0000000..808279f --- /dev/null +++ b/StatusStyleDelegate.py @@ -0,0 +1,80 @@ +# StatusStyleDelegate.py +from PySide6.QtWidgets import QStyledItemDelegate +from PySide6.QtGui import QColor, QFont, QBrush +from PySide6.QtCore import Qt, QTimer +from PySide6.QtGui import QPalette + +class StatusStyleDelegate(QStyledItemDelegate): + def __init__(self, parent=None, table_view=None): + super().__init__(parent) + self.flash_cells = {} # 存储需要闪烁的单元格 {(row, col): timer} + self.flash_duration = 500 # 闪烁持续时间(毫秒) + self.updated_cells = set() # 存储需要高亮的单元格 (row, col) + self.table_view = table_view # 保存 QTableView 实例 + + def flash_cell(self, row, column): + # 如果单元格已经在闪烁,重置计时器 + if (row, column) in self.flash_cells: + self.flash_cells[(row, column)].stop() + + # 创建新的计时器 + timer = QTimer() + timer.setSingleShot(True) + timer.timeout.connect(lambda: self.end_flash(row, column)) + self.flash_cells[(row, column)] = timer + timer.start(500) + + self.updated_cells.add((row, column)) + if self.table_view: + self.table_view.viewport().update() + + def end_flash(self, row, column): + """1秒后清除高亮""" + if (row, column) in self.flash_cells: + del self.flash_cells[(row, column)] + self.updated_cells.discard((row, column)) + if self.table_view: + self.table_view.viewport().update() + + def initStyleOption(self, option, index): + super().initStyleOption(option, index) + + # 检查是否需要闪烁 + row = index.row() + col = index.column() + if (row, col) in self.updated_cells: + option.backgroundBrush = QBrush(QColor("yellow")) + + model = index.model() + status_index = model.index(index.row(), 7) + status = model.data(status_index, Qt.ItemDataRole.DisplayRole) + + if status == "已触发": + option.font = QFont("Arial", 14, QFont.Weight.Bold) + option.palette.setColor(QPalette.ColorRole.Text, QColor('red')) + option.backgroundBrush = QBrush(QColor('lightcoral')) # 背景色 + elif status == "错买": + option.font = QFont("Arial", 14, QFont.Weight.Bold) + option.palette.setColor(QPalette.ColorRole.Text, QColor('green')) # 文字颜色 + option.backgroundBrush = QBrush(QColor('lightgreen')) # 背景色 + elif status == "监控中": + option.font = QFont("Arial", 12, QFont.Weight.Normal) + option.palette.setColor(QPalette.ColorRole.Text, QColor('black')) + option.backgroundBrush = QBrush(QColor('white')) + else: + option.font = QFont("Arial", 12, QFont.Weight.Normal) + option.palette.setColor(QPalette.ColorRole.Text, QColor('black')) + option.backgroundBrush = QBrush(QColor('white')) + + # 新增对获利列的处理 + if index.column() == 6: # 检查是否为获利列 + option.font = QFont(option.font().family(), option.font().pointSize(), QFont.Weight.Bold) + profit_text = model.data(index, Qt.ItemDataRole.DisplayRole) + try: + profit_value = float(profit_text.replace('%', '')) # 移除百分号再转换 + if profit_value > 0: + option.palette.setColor(QPalette.ColorRole.Text, QColor('red')) + elif profit_value < 0: + option.palette.setColor(QPalette.ColorRole.Text, QColor('green')) + except ValueError: + pass # 如果转换失败,保持默认颜色 diff --git a/log_style_manager.py b/log_style_manager.py new file mode 100644 index 0000000..9dc9f5e --- /dev/null +++ b/log_style_manager.py @@ -0,0 +1,99 @@ +# log_style_manager.py + +from PySide6.QtGui import QTextDocument, QTextBlockFormat, QTextCharFormat, QFont, QColor +from PySide6.QtCore import Qt +from PySide6.QtGui import QTextCursor + + +class LogStyleManager: + """ + 日志样式管理器,统一控制 QTextEdit 的字体、颜色、行间距等 + """ + + def __init__(self, text_edit): + self.text_edit = text_edit + self.default_font = self.text_edit.font() + self.default_style = { + 'default': {'color': 'white', 'bold': False}, + 'info': {'color': 'cyan', 'bold': False}, + 'warning': {'color': 'yellow', 'bold': False}, + 'error': {'color': 'red', 'bold': False}, + 'trigger': {'color': '#28a745', 'bold': False}, + } + + def set_log_type_mapping(self, mapping=None): + """ + 设置日志类型与样式的映射关系 + :param mapping: dict,键为日志类型(如 'info'),值为对应的样式字典 + 样式字典支持: + - color: 颜色字符串 + - bold: 是否加粗 + - background: 背景颜色(可选) + - italic: 是否斜体 + """ + default_mapping = { + 'default': {'color': 'white', 'bold': False}, + 'info': {'color': 'cyan', 'bold': False}, + 'warning': {'color': 'yellow', 'bold': False}, + 'error': {'color': 'red', 'bold': True}, # 错误信息加粗 + 'trigger': {'color': '#28a745', 'bold': False}, + 'debug': {'color': 'gray', 'italic': True} # 新增 debug 类型 + } + + # 如果传入了新的映射规则,则更新默认样式 + if mapping is not None: + self.default_style = mapping + else: + self.default_style = default_mapping + + def set_global_font(self, font_family="微软雅黑", font_size=13): + """设置全局字体""" + self.default_font.setFamily(font_family) + self.default_font.setPointSize(font_size) + self.text_edit.setFont(self.default_font) + + def set_letter_spacing(self, spacing=5): + """设置字间距""" + self.default_font.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, spacing) + self.text_edit.setFont(self.default_font) + + def set_line_height(self, line_height=150): + """设置段落行高(百分比)""" + doc = self.text_edit.document() + block_format = QTextBlockFormat() + + block_format.setLineHeight(float(line_height), 0) + + block_format.setTopMargin(0) + block_format.setBottomMargin(0) + + cursor = QTextCursor(doc) + cursor.select(QTextCursor.SelectionType.Document) # 修改此处 + cursor.mergeBlockFormat(block_format) + cursor.clearSelection() + + def apply_log_style(self, log_type='default'): + """返回适用于当前日志类型的 QTextCharFormat""" + style = self.default_style.get(log_type.lower(), self.default_style['default']) + fmt = QTextCharFormat() + fmt.setForeground(QColor(style['color'])) + if style.get('bold', False): + fmt.setFontWeight(QFont.Weight.Bold) + return fmt + + def insert_log(self, message, log_type='default'): + """插入带样式的日志信息""" + fmt = self.apply_log_style(log_type) + + cursor = self.text_edit.textCursor() + cursor.movePosition(QTextCursor.End) + cursor.mergeCharFormat(fmt) + cursor.insertText(message + "\n") + self.text_edit.setTextCursor(cursor) + self.text_edit.ensureCursorVisible() + + def reset_styles(self): + """重置所有样式到默认状态""" + self.text_edit.setStyleSheet("") + self.text_edit.setFont(self.default_font) + self.set_line_height(100) diff --git a/logger_utils.py b/logger_utils.py new file mode 100644 index 0000000..860a033 --- /dev/null +++ b/logger_utils.py @@ -0,0 +1,82 @@ +# logger_utils.py + +import queue +import datetime +from colorama import Fore, Style, init + +# 初始化 colorama(仅 Windows 需要) +init(autoreset=True) + +# 日志等级颜色映射(控制台 + UI) +LOG_COLORS = { + 'default': Fore.CYAN, + 'info': Fore.CYAN, + 'loading': Fore.YELLOW, + 'warning': Fore.YELLOW, + 'error': Fore.RED, + 'trigger': Fore.GREEN, + 'debug': Fore.BLUE, +} + +# 【新增】UI 样式映射 +# LOG_STYLES = { +# 'default': {'color': 'blue'}, +# 'info': {'color': 'blue', 'bold': False}, +# 'loading': {'color': 'orange', 'bold': False}, +# 'warning': {'color': 'orange', 'bold': False}, +# 'error': {'color': 'red', 'bold': True}, +# 'trigger': {'color': 'green', 'bold': False}, +# 'debug': {'color': 'gray', 'bold': False}, +# } + +LOG_STYLES = { + 'default': {'foreground': 'blue'}, + 'info': {'foreground': 'blue'}, + 'loading': {'foreground': 'orange'}, + 'warning': {'foreground': 'orange'}, + 'error': {'foreground': 'red'}, + 'trigger': {'foreground': 'green'}, + 'debug': {'foreground': 'gray'}, +} + +class Logger: + def __init__(self): + self.log_queue = queue.Queue() + self.append_log_func = None # 主程序的日志回调函数 + + def set_append_log(self, func): + """设置主程序中的 append_log 函数""" + self.append_log_func = func + + def log(self, message, level='default'): + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + full_message = f"[{timestamp}] {message}" + + # 控制台带颜色输出 + color = LOG_COLORS.get(level.lower(), Fore.WHITE) + print(f"{color}{full_message}{Style.RESET_ALL}") + + # 触发UI更新(如果已注册) + if self.append_log_func: + log_type = level + self.append_log_func(full_message, log_type) + + def info(self, message): self.log(message, 'info') + def warning(self, message): self.log(message, 'warning') + def error(self, message): self.log(message, 'error') + def trigger(self, message): self.log(message, 'trigger') + def debug(self, message): self.log(message, 'debug') + + +# 全局日志实例 +global_logger = Logger() + +# 便捷方法,方便其他模块直接使用 +def setup_logger(append_log_func): + global_logger.set_append_log(append_log_func) + +def log_info(msg): global_logger.info(msg) +def log_warning(msg): global_logger.warning(msg) +def log_error(msg): global_logger.error(msg) +def log_trigger(msg): global_logger.trigger(msg) +def log_debug(msg): global_logger.debug(msg) diff --git a/logger_utils_new.py b/logger_utils_new.py new file mode 100644 index 0000000..3dffbde --- /dev/null +++ b/logger_utils_new.py @@ -0,0 +1,73 @@ +# logger_utils.py + +import queue +import datetime +from colorama import Fore, Style, init + +# 初始化 colorama(仅 Windows 需要) +init(autoreset=True) + +# 日志等级颜色映射(控制台 + UI) +# LOG_COLORS → 控制台日志颜色(命令行) +# LOG_STYLES → UI 日志样式(图形界面) +LOG_COLORS = { + 'default': Fore.CYAN, + 'info': Fore.CYAN, + 'warning': Fore.YELLOW, + 'error': Fore.RED, + 'trigger': Fore.GREEN, +} + +# 【新增】UI 样式映射 +LOG_STYLES = { + 'default': {'color': 'cyan'}, + 'info': {'color': 'cyan'}, + 'warning': {'color': 'yellow'}, + 'error': {'color': 'red'}, + 'trigger': {'color': '#28a745'}, +} + +class Logger: + def __init__(self): + self.log_queue = queue.Queue() + self.append_log_func = None # 主程序的日志回调函数 + + def set_append_log(self, func): + """设置主程序中的 append_log 函数""" + self.append_log_func = func + + def log(self, message, level='default'): + timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + full_message = f"[{timestamp}] {message}" + + # 控制台带颜色输出 + color = LOG_COLORS.get(level.lower(), Fore.WHITE) + print(f"{color}{full_message}{Style.RESET_ALL}") + + # 触发UI更新(如果已注册) + if self.append_log_func: + log_type = level.lower() + style = LOG_STYLES.get(log_type, LOG_STYLES['default']) + + # 调用主程序的 append_log 方法,只传递纯文本和日志类型 + self.append_log_func(full_message, log_type) + + def info(self, message): self.log(message, 'info') + def warning(self, message): self.log(message, 'warning') + def error(self, message): self.log(message, 'error') + def trigger(self, message): self.log(message, 'trigger') + def debug(self, message): self.log(message, 'debug') + + +# 全局日志实例 +global_logger = Logger() + +# 便捷方法,方便其他模块直接使用 +def setup_logger(append_log_func): + global_logger.set_append_log(append_log_func) + +def log_info(msg): global_logger.info(msg) +def log_warning(msg): global_logger.warning(msg) +def log_error(msg): global_logger.error(msg) +def log_trigger(msg): global_logger.trigger(msg) +def log_debug(msg): global_logger.debug(msg) diff --git a/order_executor.ahk b/order_executor.ahk new file mode 100644 index 0000000..9bd615e --- /dev/null +++ b/order_executor.ahk @@ -0,0 +1,58 @@ +#NoEnv +SendMode Input +SetWorkingDir %A_ScriptDir% +DetectHiddenWindows, On +SetTitleMatchMode, 2 + +; 获取命令行参数 +param1 = %1% + +if (param1 = "click") { + x := %2% + y := %3% + MouseMove, x, y + Click +} else if (param1 = "image_click") { + ; 图像识别点击功能 + imagePath := %2% + winTitle := %3% + confidence := %4% + + ; 设置搜索区域 + if (winTitle != "") { + WinGet, hwnd, ID, %winTitle% + if (!hwnd) { + ExitApp, 1 ; 未找到窗口,返回错误 + } + WinGetPos, winX, winY, winW, winH, ahk_id %hwnd% + searchX := winX + searchY := winY + searchW := winW + searchH := winH + } else { + ; 全屏搜索 + searchX := 0 + searchY := 0 + searchW := A_ScreenWidth + searchH := A_ScreenHeight + } + + ; 使用ImageSearch进行图像识别 + ImageSearch, foundX, foundY, searchX, searchY, searchX+searchW, searchY+searchH, *%confidence% %imagePath% + if (ErrorLevel = 0) { + ; 找到图像,计算中心点并点击 + centerX := foundX + centerY := foundY + MouseMove, centerX, centerY + Click + ExitApp, 0 ; 成功,返回0 + } else { + ExitApp, 1 ; 未找到图像,返回错误 + } +} else if (param1 = "type") { + Send, %2% +} else if (param1 = "press") { + Send, {%2%} +} + +ExitApp \ No newline at end of file diff --git a/order_executor.py b/order_executor.py new file mode 100644 index 0000000..0002fb2 --- /dev/null +++ b/order_executor.py @@ -0,0 +1,172 @@ +import subprocess +import time +import pygetwindow as gw +import win32gui +import win32con +import cv2 +import os +from logger_utils import log_info, log_warning, log_error, log_trigger +# 移除 pyautogui 导入 + + +class OrderExecutor: + def __init__(self, target_window_title="东方财富终端"): + self.target_window_title = target_window_title + self.order_window_title = 'FlashTradeDlgDlg' + self.order_count_window_title = '提示' + self.ahk_script_path = "D:/gp_data/order_executor.ahk" + + def run_ahk_script(self, command): + """执行AHK脚本命令""" + try: + # 尝试查找AHK安装路径 + ahk_path = None + # 常见安装路径 + possible_paths = [ + "C:\\Program Files\\AutoHotkey\\v2\\AutoHotkey.exe", # v2版本 + "C:\\Program Files\\AutoHotkey\\AutoHotkey.exe", + "AutoHotkey.exe" # 如果在PATH环境变量中 + ] + + for path in possible_paths: + if os.path.exists(path): + ahk_path = path + break + + if not ahk_path: + log_error("未找到AutoHotkey安装路径") + return False + + subprocess.Popen( + [ahk_path, '/restart', self.ahk_script_path, command], + creationflags=subprocess.CREATE_NO_WINDOW + ) + return True + except Exception as e: + log_error(f"执行AHK脚本失败: {e}") + return False + + def click_confirm_button(self, window_title, confirm_ratio=(0.8, 0.8), delay=0.3): + """ + 修改为使用AHK点击确认按钮 + """ + try: + time.sleep(delay) + window_list = gw.getWindowsWithTitle(window_title) + if not window_list: + log_warning(f"未找到行情软件 '{window_title}' 的窗口") + return False + + window = window_list[0] + left, top, width, height = window.left, window.top, window.width, window.height + x = left + int(width * confirm_ratio[0]) + y = top + int(height * confirm_ratio[1]) + + # 使用AHK执行点击 + command = f"click {x} {y}" + self.run_ahk_script(command) + log_trigger(f"AHK点击 '{window_title}' ,位置: ({x}, {y})") + return True + + except Exception as e: + log_error(f"点击确认按钮失败: {e}") + return False + + def click_button_by_image(self, image_path, window_title=None, + timeout=10, retry_interval=0.5, + confidence=0.8, move_duration=0.2): + """ + 修改为使用AHK进行图像识别点击 + """ + log_info(f"开始AHK图像识别: {image_path}") + + # 构建AHK命令 + command = f"image_click {image_path}" + if window_title: + command += f" {window_title}" + command += f" {confidence}" + + # 执行AHK命令 + success = self.run_ahk_script(command) + if success: + log_trigger(f"AHK成功识别并点击图像 '{image_path}'") + return True, (0, 0) # AHK不返回坐标,用(0,0)占位 + else: + log_warning(f"AHK未能识别图像 '{image_path}'") + return False, None + + def place_order(self, pure_code, auto_push): + """ + 完全使用AHK的下单函数 + """ + try: + # 获取所有匹配标题的窗口 + window_list = gw.getWindowsWithTitle(self.target_window_title) + if not window_list: + log_warning(f"未找到标题为 '{self.target_window_title}' 的窗口") + return False + + # 筛选逻辑:优先选择可见且活动的窗口 + target_window = None + + def is_window_visible(hwnd): + return win32gui.IsWindowVisible(hwnd) and win32gui.IsWindowEnabled(hwnd) + + for window in window_list: + hwnd = window._hWnd + if is_window_visible(hwnd) and window.isActive: + target_window = window + break + # 如果没有活动窗口,选择第一个可见窗口 + if not target_window: + for window in window_list: + hwnd = window._hWnd + if is_window_visible(hwnd): + target_window = window + break + # 如果都没有,选择第一个匹配的窗口 + if not target_window: + target_window = window_list[0] + + # 使用筛选后的目标窗口进行操作 + hwnd = target_window._hWnd + log_info(f"找到窗口句柄: {hwnd}") + target_window.restore() + target_window.maximize() + target_window.activate() + time.sleep(0.2) + + # 使用AHK点击中心位置 + self.click_confirm_button(self.target_window_title, (0.5, 0.5), 0.1) + + # 使用AHK输入代码 + self.run_ahk_script(f"type {pure_code}") + self.run_ahk_script("press enter") + time.sleep(0.1) + + # 判断是否自动下单 + if auto_push == 1: + self.run_ahk_script("type 21") + self.run_ahk_script("press enter") + time.sleep(0.1) + + # 点击全仓按钮 + success, pos = self.click_button_by_image( + image_path="../images/full_position.png", + window_title=self.order_window_title, + timeout=10, + retry_interval=0.3, + confidence=0.9 + ) + + if success: + log_info("已点击全仓按钮") + # 判断是否弹出仓位的框 + if self.is_window_exists(self.order_count_window_title, 0.5): + log_error(f"剩余金额不满足购买{pure_code}最低需求。") + else: + log_warning("未找到全仓按钮图像") + + except Exception as e: + log_error(f"下单失败: {str(e)}") + return False diff --git a/pyauto操控-测试.py b/pyauto操控-测试.py new file mode 100644 index 0000000..034dff0 --- /dev/null +++ b/pyauto操控-测试.py @@ -0,0 +1,113 @@ +import pyautogui +import win32gui +import win32con +import datetime +import os +import time +import tkinter as tk +import pygetwindow as gw + +class pyauto: + def __init__(self, master): + self.master = master + self.app_name = "东方财富证券" + self.hwnd = None + self.target_window_title = "东方财富证券" + # 添加测试按钮 + self.test_button = tk.Button( + master, + text="测试下单", + command=self.place_order, + font=('微软雅黑', 11), + width=12 + ) + self.test_button.pack() + + def click(self, x, y): + pyautogui.click(x, y) + + def double_click(self, x, y): + pyautogui.doubleClick(x, y) + + def place_order(self): + target_title = self.target_window_title # 假设目标窗口标题为交易窗口名称 + # 使用 pygetwindow 获取窗口对象 + window_list = gw.getWindowsWithTitle(target_title) + if not window_list: + raise Exception(f"未找到标题为 '{target_title}' 的窗口") + window = window_list[0] + # 先激活窗口 + window.restore() + window.activate() + # 使用 win32gui 获取客户区坐标 + hwnd = window._hWnd + left, top = win32gui.ClientToScreen(hwnd, (0, 0)) + right, bottom = win32gui.ClientToScreen(hwnd, win32gui.GetClientRect(hwnd)[2:]) + width = right - left + height = bottom - top + + # 调试输出窗口信息 + debug_info = f"选中窗口信息:\n" \ + f"标题: {window.title}\n" \ + f"位置: 左上角 ({left}, {top})\n" \ + f"位置: 右下角 ({right}, {bottom})\n" \ + f"宽度: {width}\n" \ + f"高度: {height}\n" \ + f"是否激活: {window.isActive}\n" + print(debug_info) # 同时在控制台输出 + # 获取窗口位置和尺寸 + left = window.left + top = window.top + print(left, top) + # 先确定在普通交易一栏 + b_x = left + 60 # 示例相对位置,可按需调整000931 + b_y = top + 70 # 示例相对位置,可按需调整 + pyautogui.click(x=b_x, y=b_y) + time.sleep(0.1) + buy_x = left + 90 # 示例相对位置,可按需调整 + buy_y = top + 112 # 示例相对位置,可按需调整 + pyautogui.click(x=buy_x, y=buy_y) + # 清空原始数据 + c_x = left + 312 + c_y = top + 471 + pyautogui.click(x=c_x, y=c_y) + # 输入代码 + b_x = left + 380 + b_y = top + 185 + pyautogui.doubleClick(x=b_x, y=b_y) + pyautogui.typewrite("002566") + time.sleep(0.1) + # 输入价格 + c_x = left + 591 + c_y = top + 335 + pyautogui.doubleClick(x=c_x, y=c_y) # 改为双击选中内容 + pyautogui.typewrite(str(7.04)) + # 输入数量 + c_x = left + 341 + c_y = top + 383 + pyautogui.doubleClick(x=c_x, y=c_y) # 改为双击选中内容 + pyautogui.typewrite(str(100)) + time.sleep(0.2) + # 买入点击 + c_x = left + 483 + c_y = top + 468 + pyautogui.click(x=c_x, y=c_y) + + # if order_type == 'buy': + # 假设买入按钮位置,这里可以根据窗口位置计算相对坐标 + + # + # + # # time.sleep(0.1) + # # 点击按钮位置 + # buy_b_x = left + 480 # 示例相对位置,可按需调整000931 + # buy_b_y = top + 471 + # pyautogui.click(x=buy_b_x, y=buy_b_y) + # # 示例:模拟输入数量 + # # pyautogui.typewrite(str(volume)) + + +if __name__ == "__main__": + root = tk.Tk() + app = pyauto(root) + root.mainloop() diff --git a/quote_manager.py b/quote_manager.py new file mode 100644 index 0000000..7b0f12c --- /dev/null +++ b/quote_manager.py @@ -0,0 +1,111 @@ +""" +行情数据管理模块 - 支持多数据源(Tushare/AKShare) +""" +import pandas as pd +import tushare as ts +import akshare as ak +from typing import List, Dict, Optional # 添加 Optional 导入 +import logging +import time +from enum import Enum, auto +import threading + +# 添加 Tushare Token 设置 +ts.set_token('9343e641869058684afeadfcfe7fd6684160852e52e85332a7734c8d') + +class DataSource(Enum): + TUSHARE = "tushare" + AKSHARE = "akshare" + LOCAL = "local" + +class QuoteManager: + _instance = None + _lock = threading.Lock() + + def __new__(cls): + with cls._lock: + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._init_manager() + return cls._instance + + def _init_manager(self): + self._cache = {} + self._cache_ttl = 60 + self._data_source = DataSource.TUSHARE + self.max_retries = 3 # 默认最大重试次数 + self.retry_interval = 2 # 默认重试间隔(秒) + + def set_retry_policy(self, max_retries: int, retry_interval: float = 2): + """设置重试策略""" + self.max_retries = max_retries + self.retry_interval = retry_interval + + def set_data_source(self, source: DataSource): + """设置数据源""" + self._data_source = source + + def get_realtime_quotes(self, codes: List[str]) -> Dict[str, pd.DataFrame]: + """获取实时行情(带重试机制)""" + last_error = None + for attempt in range(self.max_retries): + try: + if self._data_source == DataSource.TUSHARE: + return self._get_tushare_quotes(codes) + elif self._data_source == DataSource.AKSHARE: + return self._get_akshare_quotes(codes) + else: + raise ValueError("不支持的数据源") + except Exception as e: + last_error = e + if attempt < self.max_retries - 1: # 不是最后一次尝试 + time.sleep(self.retry_interval) + continue + raise Exception(f"获取行情失败(尝试{self.max_retries}次): {str(last_error)}") + + def _get_tushare_quotes(self, codes: List[str]) -> Dict[str, pd.DataFrame]: + """使用 Tushare 获取实时行情(带重试机制)""" + for attempt in range(self.max_retries): + try: + df = ts.realtime_quote(ts_code=','.join(codes)) + if df is None or df.empty: + raise Exception("返回数据为空") + return {row['TS_CODE']: row for _, row in df.iterrows()} + except Exception as e: + if attempt < self.max_retries - 1: + time.sleep(self.retry_interval) + continue + raise Exception(f"Tushare 行情获取失败(尝试{self.max_retries}次): {str(e)}") + + def _get_akshare_quotes(self, codes: List[str]) -> Dict[str, pd.DataFrame]: + """使用 AKShare 获取实时行情""" + # 这里需要实现 AKShare 的获取逻辑 + raise NotImplementedError("AKShare 实现待完成") + + def _convert_akshare_format(self, row) -> Dict: + """将AKShare数据格式转换为统一格式""" + return { + 'TS_CODE': row['代码'], + 'PRICE': row['最新价'], + 'OPEN': row['今开'], + 'PRE_CLOSE': row['昨收'], + 'HIGH': row['最高'], + 'LOW': row['最低'], + 'VOLUME': row['成交量'] + } + + def get_quote(self, code: str) -> Optional[Dict]: + """获取单个股票行情(兼容旧接口)""" + try: + quotes = self.get_realtime_quotes([code]) + if not quotes or code not in quotes: + return None + row = quotes[code] + return { + 'price': row['PRICE'], + 'avg_price': (row['OPEN'] + row['PRE_CLOSE']) / 2, + 'volume': row['VOLUME'] + } + except Exception as e: + logging.error(f"获取股票{code}行情失败: {str(e)}") + return None \ No newline at end of file diff --git a/stock_monitor_pyside.py b/stock_monitor_pyside.py new file mode 100644 index 0000000..3a95331 --- /dev/null +++ b/stock_monitor_pyside.py @@ -0,0 +1,894 @@ +import sys +import os +import time +import datetime +import chardet +import csv +import pandas as pd +import tushare as ts +import threading +import socket +from PySide6.QtCore import Qt, QModelIndex +from PySide6.QtCore import QTimer, QThread, Signal, QObject, QPropertyAnimation, QEasingCurve +from PySide6.QtWidgets import ( + QApplication, QMainWindow, QTableView, QPushButton, + QVBoxLayout, QHBoxLayout, QWidget, QLabel, QFileDialog, + QTextEdit, QCheckBox, QLineEdit, QMessageBox, QSplitter, + QMenuBar, QMenu, QHeaderView, QDialog +) +# 正确导入方式 +from PySide6.QtWidgets import QApplication, QTableView, QHeaderView, QStyledItemDelegate +from PySide6.QtGui import QAction, QFont, QTextCharFormat, QColor, QIcon, QStandardItemModel, QStandardItem, QPalette +from PySide6.QtGui import QTextCursor, QBrush +from logger_utils_new import log_info, log_warning, log_error, log_trigger, log_debug, setup_logger, LOG_STYLES +from StatusStyleDelegate import StatusStyleDelegate +from order_executor import OrderExecutor +from logger_utils_new import setup_logger +from log_style_manager import LogStyleManager + + +# 设置 Tushare Token +# ts.set_token('9343e641869058684afeadfcfe7fd6684160852e52e85332a7734c8d') +pro = ts.pro_api() + +from quote_manager import QuoteManager, DataSource + +class PriceUpdateWorker(QThread): + data_ready = Signal(dict) + update_finished = Signal() + + def __init__(self, stock_codes, parent=None): + super().__init__(parent) + self.stock_codes = stock_codes + # 引用行情数据 + self.quote_manager = QuoteManager() + self.quote_manager.set_data_source(DataSource.TUSHARE) + + def run(self): + try: + all_results = self.quote_manager.get_realtime_quotes(self.stock_codes) + self.data_ready.emit(all_results) + except Exception as e: + log_error(f"行情获取失败: {str(e)}") + finally: + self.update_finished.emit() + + +from typing import List +# 自定义模型以支持按业务优先级排序 +class CustomSortModel(QStandardItemModel): + def sort(self, column, order=Qt.SortOrder.AscendingOrder): + if column != 7: + return super().sort(column, order) + + status_priority = {"已触发": 0, "错买": 1, "监控中": 2} + + def get_sort_key(item): + if item is None: + return 3 + role = item.data(Qt.ItemDataRole.UserRole) + if role is not None: + return role + text = item.text() + return status_priority.get(text, 3) + + # 保存当前所有行 + all_rows = [self.takeRow(0) for _ in range(self.rowCount())] + + # 按照状态列排序 + all_rows.sort(key=lambda row_items: get_sort_key(row_items[7]), + reverse=(order == Qt.SortOrder.DescendingOrder)) + + # 清空并重新添加 + self.removeRows(0, self.rowCount()) + for row_items in all_rows: + self.appendRow(row_items) + + self.layoutChanged.emit() + + +class MainWindow(QMainWindow): + log_signal = Signal(str, str) # message, level + OUT_PATH = r"D:\gp_data" + ping_updated_signal = Signal(float) # 新增:用于线程安全地传递延迟值 + def __init__(self): + super().__init__() + self.setWindowTitle("股票价格监控系统") + self.resize(1200, 800) + self.capital_threshold = 60 + self.open_zf_threshold = 3.0 + # 初始化 delegate + self.delegate = StatusStyleDelegate(self) # 新增这行 + + # 初始化股票计数器标签 + self.total_stocks_label = QLabel("总数: 0") + self.triggered_stocks_label = QLabel("触发: 0") + self.total_stocks_label.setObjectName("total_stocks_label") + self.triggered_stocks_label.setObjectName("triggered_stocks_label") + self.trading_status_label = QLabel("当前状态:非交易时间") + self.trading_status_label.setObjectName("trading_status_label") + self.data_refresh_label = QLabel("数据状态:") + self.data_refresh_label.setObjectName("data_refresh_label") + + self.ping_updated_signal.connect(self.update_ping_ui) # 连接信号到 UI 更新函数 + + self.ping_label = QLabel("实时行情延迟: --ms") + self.ping_label.setObjectName("ping_label") + self.ping_light = self.create_status_light() + self.ping_light.setObjectName("ping_light") # 避免重复创建 + + self.price_update_worker = None # 用于保存当前线程实例 + self.current_all_results = {} # 缓存最新行情数据 + self.calculated_open_pct = set() # 记录已计算开盘涨幅的股票 + + # 初始化模型 + self.standard_model = CustomSortModel() + self.standard_model.setHorizontalHeaderLabels([ + "代码", "名称", "开盘涨幅", "流通盘", "当前价", "目标价", "获利%", "状态", "预警时间" + ]) + + # 表格视图 + self.table_view = QTableView() + self.table_view.setModel(self.standard_model) + self.table_view.setSortingEnabled(True) # 启用排序 + self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Fixed) + + # 设置每列宽度 + widths = [140, 120, 100, 100, 80, 80, 80, 80, 220] + for col_index, width in enumerate(widths): + self.table_view.horizontalHeader().resizeSection(col_index, width) + + # 设置委托(确保 self.status_delegate 已在 __init__ 中初始化) + # 修改前: + # self.status_delegate = StatusStyleDelegate() + # 修改后: + self.status_delegate = StatusStyleDelegate(parent=self, table_view=self.table_view) + for col in range(8): # 对前8列应用 + self.table_view.setItemDelegateForColumn(col, self.status_delegate) + + # 新增大流通盘颜色样式 + self.large_cap_delegate = StatusStyleDelegate() + self.table_view.setItemDelegateForColumn(3, self.large_cap_delegate) # 第4列为流通盘列 + + # 在 init_ui() 之前或之后都可以 + self.load_stylesheet() + + # 加载UI布局 + self.init_ui() + + # 初始化日志样式管理器 + self.log_style_manager = LogStyleManager(self.log_box) + self.log_style_manager.set_global_font(font_family="微软雅黑", font_size=12) + self.log_style_manager.set_letter_spacing(spacing=1) # 设置字间距 + self.log_style_manager.set_line_height(line_height=120) # 设置行高为 1.2 倍 + + # 设置日志区域段落格式 + # self.setup_log_paragraph_format() + + # 其它初始化 + # font = self.log_box.font() + # font.setLetterSpacing(QFont.SpacingType.AbsoluteSpacing, 1) # 设置每个字符之间固定间距为 1 像素 + # self.log_box.setFont(font) + + # 初始化变量 + self.order_executor = OrderExecutor() # 初始化下单执行器 + self.data_rows = [] + self.stock_codes = [] + self.auto_push_var = False + setup_logger(self.append_log) + + # 初始化日志文件路径(关键修复点) + if not hasattr(self, 'log_file'): + self.log_file = os.path.join(self.OUT_PATH, f"monitor_{datetime.datetime.now().strftime('%Y%m%d')}.log") + + # 启动定时器 + self.timer = QTimer() + self.timer.timeout.connect(self.update_prices) + self.timer.start(5000) + + # 启动状态更新线程 + self.start_status_update() + self.start_ping_update() # 启动 Ping 更新 + # print(type(self.standard_model)) # 应该输出 + + def load_stylesheet(self): + qss_file = os.path.join(self.OUT_PATH, "style.qss") + if os.path.exists(qss_file): + with open(qss_file, 'r', encoding='utf-8') as f: + self.setStyleSheet(f.read()) + log_info(f"样式文件已加载: {qss_file}") + else: + log_error(f"样式文件不存在: {qss_file}") + + def add_test_data(self): + """添加测试数据""" + statuses = ["监控中", "错买", "已触发"] + for i in range(10): + code = f"{i + 1:06d}.SZ" + name = f"测试股票{i + 1}" + status = statuses[i % 3] + + items = [ + QStandardItem(code), + QStandardItem(name), + QStandardItem("0.00%"), + QStandardItem("10.00亿"), + QStandardItem("15.00"), + QStandardItem("16.00"), + QStandardItem("6.67%"), + QStandardItem(status), + QStandardItem(datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + ] + + # 设置排序角色 + if status == "已触发": + items[7].setData(0, Qt.ItemDataRole.UserRole) + elif status == "错买": + items[7].setData(1, Qt.ItemDataRole.UserRole) + elif status == "监控中": + items[7].setData(2, Qt.ItemDataRole.UserRole) + + self.standard_model.appendRow(items) + + def start_price_update_thread(self): + if self.price_update_worker is not None and self.price_update_worker.isRunning(): + return # 避免重复启动线程 + + self.price_update_worker = PriceUpdateWorker(self.stock_codes, self) + self.price_update_worker.data_ready.connect(self.on_data_ready) + self.price_update_worker.update_finished.connect(self.on_update_finished) + self.price_update_worker.start() + + def init_ui(self): + # 主窗口控件与布局 + main_widget = QWidget() + layout = QVBoxLayout() + + # 状态栏 + status_layout = QHBoxLayout() + + self.status_light = self.create_status_light() + self.update_light = self.create_status_light() + + # self.ping_label = QLabel("实时行情延迟: --ms") + # self.ping_light = self.create_status_light() # 新增的小灯 + + status_layout.addWidget(self.trading_status_label) + status_layout.addWidget(self.status_light) + status_layout.addStretch() + + # 在 update_light 前添加文字标签 + status_layout.addWidget(self.data_refresh_label) + status_layout.addWidget(self.update_light) + + # 新增网络延迟标签和小灯 + status_layout.addSpacing(20) # 添加间距 + # 状态栏部分 + status_layout.addWidget(self.ping_label) + status_layout.addWidget(self.ping_light) + + layout.addLayout(status_layout) + + # 表格视图 + self.table_view = QTableView() + self.standard_model.setHorizontalHeaderLabels([ + "代码", "名称", "开盘涨幅", "流通盘", "当前价", "目标价", "获利%", "状态", "预警时间" + ]) + self.table_view.setModel(self.standard_model) + + # 设置表头行为(先设 Stretch,再设固定宽度) + self.table_view.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed) + for col_index, width in enumerate([140, 120, 100, 100, 80, 80, 100, 80, 220]): + self.table_view.horizontalHeader().resizeSection(col_index, width) + + # 设置委托(确保 self.status_delegate 已在 __init__ 中初始化) + self.table_view.setItemDelegateForColumn(7, self.status_delegate) + self.table_view.setSortingEnabled(True) + + # 双击事件绑定 + self.table_view.doubleClicked.connect(self.place_order) + layout.addWidget(self.table_view) + + # 控制面板 + control_layout = QHBoxLayout() + button_font = QFont() + button_font.setPointSize(8) + + self.btn_load = QPushButton("选择文件") + self.btn_save = QPushButton("保存结果") + self.checkbox_auto = QCheckBox("自动下单") + self.checkbox_debug = QCheckBox("调试模式") + self.checkbox_topmost = QCheckBox("窗口置顶") + + # 设置对象名 + self.btn_load.setObjectName("btn_load") + self.btn_save.setObjectName("btn_save") + for btn in [self.btn_load, self.btn_save]: + btn.setObjectName(btn.text().replace(" ", "_")) + btn.setFont(button_font) + + for cb in [self.checkbox_auto, self.checkbox_debug, self.checkbox_topmost]: + cb.setFont(button_font) + cb.setObjectName(cb.text()) + + # 连接事件 + self.checkbox_topmost.stateChanged.connect(self.toggle_always_on_top) + self.btn_load.clicked.connect(self.select_file) + self.btn_save.clicked.connect(self.export_to_excel) + + control_layout.addWidget(self.btn_load) + control_layout.addWidget(self.btn_save) + control_layout.addWidget(self.checkbox_auto) + control_layout.addWidget(self.checkbox_topmost) + control_layout.addWidget(self.checkbox_debug) + + # 添加股票计数器标签到控制布局 + control_layout.addWidget(self.total_stocks_label) + control_layout.addWidget(self.triggered_stocks_label) + # self.total_stocks_label.setStyleSheet("color: green; font-size: 14pt;") + # self.triggered_stocks_label.setStyleSheet("color: blue; font-size: 14pt;") + + layout.addLayout(control_layout) + + # 日志区域 + self.log_box = QTextEdit() + self.log_box.setReadOnly(True) + self.log_box.setFixedHeight(250) # 设置固定高度为 250 像素 + layout.addWidget(self.log_box) + + # 设置主窗口布局 + main_widget.setLayout(layout) + self.setCentralWidget(main_widget) + + # 确保样式表加载 + # self.load_stylesheet() + # self.update_ping_ui(123.45) + + def start_ping_update(self): + def ping_loop(): + while True: + try: + # log_info("开始测速...") + delay = self.ping_tushare_server() + # log_info(f"获取到延迟: {delay:.2f}ms") + self.ping_updated_signal.emit(delay) # 使用信号通知主线程 + except Exception as e: + log_error(f"Ping 失败: {str(e)}") + time.sleep(60) # 每分钟更新一次 + + # log_info("启动后台 Ping 线程...") + threading.Thread(target=ping_loop, daemon=True).start() + + def ping_tushare_server(self): + """ + 使用 Tushare 实时行情接口测试 API 响应速度 + 返回三次请求的平均延迟(单位:毫秒) + """ + delays = [] + + for _ in range(3): # 进行3次尝试 + start = time.time() + try: + # 使用一个轻量级的 API 请求进行测试 + df = ts.realtime_quote(ts_code='000001.SZ') + # log_debug("API 调用成功,返回结果:") + # log_debug(str(df)) # 打印原始返回结果,便于调试 + if df is not None and not df.empty: + end = time.time() + delay = (end - start) * 1000 # 转换为毫秒 + delays.append(delay) + # log_info(f"单次延迟: {delay:.2f}ms") + else: + log_warning("API 返回空数据") + except Exception as e: + log_warning(f"API 请求失败: {str(e)}") + continue + + if delays: + avg_delay = round(sum(delays) / len(delays), 2) + # log_info(f"平均延迟: {avg_delay:.2f}ms") + return avg_delay # 返回平均延迟 + else: + log_error("无法成功调用 Tushare API,未获取到任何有效延迟数据") + raise Exception("无法成功调用 Tushare API") + + def update_ping_ui(self, delay): + if not hasattr(self, 'ping_label') or not hasattr(self, 'ping_light'): + log_warning("ping_label 或 ping_light 尚未初始化") + return + + try: + self.ping_label.setText(f"实时行情延迟: {delay:.2f}ms") + # log_debug(f"更新网络延迟标签为: {delay:.2f}ms") + self.flash_ping_light(delay) + except Exception as e: + log_error(f"更新延迟UI失败: {str(e)}") + self.ping_light.setStyleSheet("background-color: gray; border-radius: 10px;") + + def flash_ping_light(self, delay): + light = self.ping_light + if delay < 100: # 如果延迟小于100ms,显示绿色 + color = "#00FF00" + elif delay < 300: # 如果延迟在100ms到300ms之间,显示黄色 + color = "yellow" + else: # 如果延迟大于300ms,显示红色 + color = "red" + # log_debug(f"设置小灯颜色为: {color}") + light.setStyleSheet(f"background-color: {color}; border-radius: 10px;") + anim = QPropertyAnimation(light, b"geometry") + anim.setDuration(300) + anim.setKeyValueAt(0, light.geometry()) + anim.setKeyValueAt(0.5, light.geometry().adjusted(-3, -3, 3, 3)) + anim.setKeyValueAt(1, light.geometry()) + anim.setLoopCount(2) + anim.setEasingCurve(QEasingCurve.Type.OutBounce) + anim.start() + # QTimer.singleShot(600, lambda: light.setStyleSheet("background-color: gray; border-radius: 10px;")) # 0.6秒后恢复灰色 + + def create_status_light(self): + light = QLabel() + light.setFixedSize(20, 20) + light.setStyleSheet("background-color: gray; border-radius: 10px;") + return light + + def place_order(self, code, auto_flag): + """ + 下单接口,根据股票代码执行下单操作 + :param code: 股票代码 (str) + :param auto_flag: 是否自动下单 (int: 0/1) + """ + log_info(f"开始处理下单请求,股票代码: {code},自动模式: {auto_flag}") + try: + self.order_executor.place_order(code, auto_flag) + except Exception as e: + log_error(f"下单过程中发生异常: {e}") + + def update_stock_counters(self): + total = self.standard_model.rowCount() + triggered = 0 + for i in range(total): + status_item = self.standard_model.item(i, 7) # 假设状态是第8列(索引从0开始) + if status_item and status_item.text() == "已触发" or status_item.text() == "错买": + triggered += 1 + self.total_stocks_label.setText(f"总数: {triggered}/{total}") + self.triggered_stocks_label.setText(f"触发: {triggered}") + + def load_stylesheet(self): + qss_file = os.path.join(self.OUT_PATH, "style.qss") + log_info(f"加载样式文件路径: {qss_file}") + + if os.path.exists(qss_file): + with open(qss_file, 'r', encoding='utf-8') as f: + self.setStyleSheet(f.read()) + else: + log_error(f"样式文件不存在:{qss_file}") + + def dragEnterEvent(self, event): + if event.mimeData().hasUrls(): + url = event.mimeData().urls()[0] + if url.toString().lower().endswith('.txt'): + event.acceptProposedAction() + + def dropEvent(self, event): + urls = event.mimeData().urls() + if urls: + file_path = urls[0].toLocalFile() + today = datetime.datetime.now().strftime("%Y%m%d") + trade_date = os.path.basename(file_path).split('.')[0] + + if trade_date == today: + log_error("不能加载今日文件,请选择历史日期文件。") + else: + self.load_stocks(file_path, trade_date) + + def select_file(self): + file_path, _ = QFileDialog.getOpenFileName(self, "选择监控文件", self.OUT_PATH +"\history", "文本文件 (*.txt)") + if file_path: + trade_date = os.path.basename(file_path).split('.')[0] + today = datetime.datetime.now().strftime("%Y%m%d") + if trade_date == today: + log_error("不能加载今日文件,请选择历史日期文件。") + return + self.load_stocks(file_path, trade_date) + + def load_stocks(self, file_path, trade_date): + try: + with open(file_path, 'rb') as f: + raw_data = f.read(10000) + encoding = chardet.detect(raw_data)['encoding'] or 'utf-8' + + with open(file_path, 'r', encoding=encoding, errors='ignore') as f: + reader = csv.reader(f) + headers = next(reader) + required_cols = ['条件选股', '代码'] + for col in required_cols: + if col not in headers: + raise KeyError(f"缺少必要列:{col}") + + name_col = headers.index('条件选股') + code_col = headers.index('代码') + + codes = [] + self.standard_model.removeRows(0, self.standard_model.rowCount()) + + for line in reader: + if not line or len(line) <= max(name_col, code_col): + continue + name = line[name_col].strip() + code = line[code_col].strip().zfill(6) + formatted_code = self.format_code(code) + codes.append(formatted_code) + + row_items = [ + QStandardItem(formatted_code), + QStandardItem(name), + QStandardItem("-"), + QStandardItem("0"), + QStandardItem("-"), + QStandardItem("-"), + QStandardItem("-"), + QStandardItem("监控中"), + QStandardItem("") + ] + self.standard_model.appendRow(row_items) + + df = pro.daily(ts_code=','.join(codes), trade_date=trade_date) + close_prices = {row['ts_code']: row['close'] for _, row in df.iterrows()} + + for i in range(self.standard_model.rowCount()): + code_item = self.standard_model.item(i, 0) + code = code_item.text() + target_price = round(close_prices.get(code, 0) * 1.049, 2) + self.standard_model.setItem(i, 5, QStandardItem(str(target_price))) + + self.stock_codes = [self.format_code(code.zfill(6)) for code in codes] + threading.Thread(target=self.fetch_share_capital_data, daemon=True).start() + + # 更新股票计数器 + self.update_stock_counters() + + # 初始更新股票计数器 + self.update_stock_counters() + + except Exception as e: + log_error(f"加载失败: {str(e)}") + + + def format_code(self, code): + # 如果已包含市场后缀(.SH 或 .SZ),则不再处理 + if '.' in code: + return code + code = code.zfill(6) + if code.startswith(("6", "9")): + return f"{code}.SH" + elif code.startswith(("0", "2", "3")): + return f"{code}.SZ" + else: + return code + + def fetch_share_capital_data(self): + log_info("获取流通盘数据...") + retry_count = 3 # 设置最大重试次数 + for attempt in range(retry_count): + try: + dc_df = ts.realtime_list(src='dc') + # print(dc_df) + if dc_df is None or dc_df.empty: + log_warning(f"第 {attempt + 1} 次获取流通盘失败,数据为空,准备重试...") + time.sleep(2) # 等待 2 秒后重试 + continue + + float_mv_dict = { + row['TS_CODE']: row['FLOAT_MV'] / 1e8 + for _, row in dc_df.iterrows() + } + + self.float_share_cache = float_mv_dict + log_info(f"流通盘缓存已更新,共 {len(float_mv_dict)} 条记录") + self.update_share_capital_ui() + return # 成功退出循环 + + except Exception as e: + log_error(f"第 {attempt + 1} 次获取流通盘失败: {str(e)}") + time.sleep(2) + + log_error("多次尝试获取流通盘失败,请检查网络连接、Token 权限或 Tushare 接口状态。") + + def update_share_capital_ui(self): + try: + for i in range(self.standard_model.rowCount()): + code = self.standard_model.item(i, 0).text() + lt_pan = self.float_share_cache.get(code, 0) + # 判断流通盘是否大于100亿 + if lt_pan < self.capital_threshold: + item = QStandardItem(f"{lt_pan:.2f}亿") + item.setBackground(QBrush(QColor("yellow"))) + item.setForeground(QColor("red")) + self.standard_model.setItem(i, 3, item) + else: + self.standard_model.setItem(i, 3, QStandardItem(f"{lt_pan:.2f}亿")) + except Exception as e: + log_error(f"更新流通盘数据失败: {str(e)}") + + def on_data_ready(self, all_results): + self.current_all_results = all_results + + def on_update_finished(self): + now = datetime.datetime.now() + is_open = self.is_trading_time() + + for i in range(self.standard_model.rowCount()): + code_item = self.standard_model.item(i, 0) + code = code_item.text() + quote = self.current_all_results.get(code) + + if quote is None or quote.empty: + continue + + current_price = float(quote['PRICE']) + target_price = float(self.standard_model.item(i, 5).text()) + + # 计算获利百分比 + profit_pct = round((current_price - target_price) / target_price * 100, 2) # 新增这行 + + open_price = float(quote['OPEN']) + pre_close = float(quote['PRE_CLOSE']) + + # 只更新变动字段(当前价、获利%) + # 在更新价格和获利的地方添加闪烁 + self.standard_model.setItem(i, 4, QStandardItem(f"{current_price:.2f}")) + self.delegate.flash_cell(i, 4) # 价格列闪烁 + + self.standard_model.setItem(i, 6, QStandardItem(f"{profit_pct:.2f}%")) + self.delegate.flash_cell(i, 6) # 获利列闪烁 + + # 判断获利是否大于0 + if profit_pct > 0: + profit_item = self.standard_model.item(i, 6) + profit_item.setForeground(QColor("red")) + # font = profit_item.font() + # font.setBold(True) + # profit_item.setFont(font) + # profit_item.setBackground(QBrush(QColor("yellow"))) + else: + profit_item = self.standard_model.item(i, 6) + profit_item.setForeground(QColor("green")) + # font = profit_item.font() + # font.setBold(True) + # profit_item.setFont(font) + # profit_item.setBackground(QBrush(QColor("yellow"))) + # 开盘涨幅逻辑:仅当开盘后第一次计算 + if is_open and code not in self.calculated_open_pct: + try: + open_zf = round((open_price - pre_close) / pre_close * 100, 2) + self.standard_model.setItem(i, 2, QStandardItem(f"{open_zf:.2f}%")) + self.calculated_open_pct.add(code) # 标记为已计算 + # print(open_zf) + # 新增判断:开盘涨幅大于 3%,标记为“错买” + if open_zf > self.open_zf_threshold: + profit_item = self.standard_model.item(i, 2) + profit_item.setForeground(QColor("red")) + profit_item.setBackground(QBrush(QColor("yellow"))) + # log_trigger(f"涨幅 {code} 符合严格条件 (开盘涨幅 {open_zf:.2f}%)") + # log_info(f"股票 {code},开盘涨幅{open_zf:.2f}% ") + except Exception as e: + log_error(f"计算开盘涨幅失败: {str(e)}") + + # 状态判断和更新 + status_item = self.standard_model.item(i, 7) + current_status = status_item.text() if status_item else "监控中" + new_status = current_status + + high_price = float(quote['HIGH']) + target_price = float(self.standard_model.item(i, 5).text()) + + if current_price >= target_price: + new_status = "已触发" + elif high_price >= target_price: + new_status = "错买" + else: + new_status = "监控中" + + if new_status != current_status: + status_item = QStandardItem(new_status) + if new_status == "已触发": + status_item.setData(0, Qt.ItemDataRole.UserRole) + elif new_status == "错买": + status_item.setData(1, Qt.ItemDataRole.UserRole) + elif new_status == "监控中": + status_item.setData(2, Qt.ItemDataRole.UserRole) + self.standard_model.setItem(i, 7, status_item) + log_trigger(f"股票 {code} 状态更新: {current_status} -> {new_status}") + + time_item = self.standard_model.item(i, 8) + if new_status in ["已触发", "错买"] and (time_item is None or not time_item.text()): + time_item = QStandardItem(now.strftime("%Y-%m-%d %H:%M:%S")) + self.standard_model.setItem(i, 8, time_item) + log_trigger(f"{code} 状态已更新为 {new_status}") + + + # 排序触发 + self.table_view.sortByColumn(7, Qt.SortOrder.AscendingOrder) + + # 更新计数器 + self.update_stock_counters() + + # 闪烁灯效果 + self.flash_update_light() + + def update_prices(self): + if not self.is_trading_time() and not self.checkbox_debug.isChecked(): + return + + self.start_price_update_thread() + + def flash_update_light(self): + light = self.update_light + light.setStyleSheet("background-color: #00FF00; border-radius: 10px;") + anim = QPropertyAnimation(light, b"geometry") + anim.setDuration(300) + anim.setKeyValueAt(0, light.geometry()) + anim.setKeyValueAt(0.5, light.geometry().adjusted(-3, -3, 3, 3)) + anim.setKeyValueAt(1, light.geometry()) + anim.setLoopCount(2) + anim.setEasingCurve(QEasingCurve.Type.OutBounce) + anim.start() + QTimer.singleShot(600, lambda: light.setStyleSheet("background-color: gray; border-radius: 10px;")) + + def place_order(self, index=None): + if not index or not index.isValid(): + return + + code = self.standard_model.item(index.row(), 0).text() + pure_code = code.split('.')[0] + # log_info(f"选中股票代码: {pure_code}") + self.order_executor.place_order(pure_code, self.checkbox_auto.isChecked()) + + # 自动按状态排序 + self.table_view.sortByColumn(7, Qt.SortOrder.AscendingOrder) # 第7列为“状态”列 + + def toggle_always_on_top(self, state): + if state == 2: + self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowStaysOnTopHint) + log_info("启用窗口置顶") + else: + self.setWindowFlags(self.windowFlags() & ~Qt.WindowType.WindowStaysOnTopHint) + log_info("取消窗口置顶") + + self.show() # 必须调用 show() 才会生效 + + def export_to_excel(self): + try: + if not self.standard_model.rowCount(): + log_warning("没有可导出的数据!") + self.show_warning("没有数据可以导出。") + return + + today = datetime.datetime.now().strftime("%Y%m%d") + filename = os.path.join(self.OUT_PATH, f"{today}_监控结果.xlsx") + + # 准备数据 + data = [] + for i in range(self.standard_model.rowCount()): + row = [self.standard_model.item(i, j).text() for j in range(self.standard_model.columnCount())] + data.append(row) + + df = pd.DataFrame(data, columns=[ + "代码", "名称", "开盘涨幅", "流通盘", "当前价", "目标价", "获利%", "状态", "预警时间" + ]) + + # 写入 Excel + df.to_excel(filename, index=False) + log_info(f"数据已保存至: {filename}") + self.show_info("导出成功", f"数据已保存到: {filename}") + + except Exception as e: + log_error(f"导出失败: {str(e)}") + self.show_error(f"导出Excel数据时发生错误:{str(e)}") + + def is_trading_time(self): + now = datetime.datetime.now().time() + weekday = datetime.datetime.now().weekday() + return ( + weekday < 5 and( + (datetime.time(9, 30) <= now <= datetime.time(11, 30)) or + (datetime.time(13, 0) <= now <= datetime.time(15, 0)) + ) + ) + + def sort_table_by_status(self): + model = self.standard_model + rows = [] + + for i in range(model.rowCount()): + status_item = model.item(i, 7) + if not status_item: + continue + status = status_item.text() + row_data = [] + fonts = [] + bg_colors = [] + fg_colors = [] + + for j in range(model.columnCount()): + item = model.item(i, j) + if item: + row_data.append(item.text()) + fonts.append(item.font()) + bg_colors.append(item.background().color()) + fg_colors.append(item.foreground().color()) + else: + row_data.append("") + fonts.append(QFont()) + bg_colors.append(QColor('white')) + fg_colors.append(QColor('black')) + + rows.append((status, row_data, fonts, bg_colors, fg_colors)) + + # 按照状态排序:触发 > 错买 > 监控中 + sorted_rows = sorted(rows, key=lambda x: {'已触发': 0, '错买': 1, '监控中': 2}.get(x[0], 3)) + + model.removeRows(0, model.rowCount()) + + for _, row_data, fonts, bg_colors, fg_colors in sorted_rows: + items = [] + for j in range(len(row_data)): + item = QStandardItem(row_data[j]) + item.setFont(fonts[j]) + item.setBackground(QColor(bg_colors[j])) + item.setForeground(QColor(fg_colors[j])) + items.append(item) + model.appendRow(items) + + def start_status_update(self): + def check_status(): + while True: + is_open = self.is_trading_time() + status_text = "开盘中" if is_open else "收盘时间" + color = "lime" if is_open else "red" + self.trading_status_label.setText(f"当前状态:{status_text}") + self.status_light.setStyleSheet(f"background-color: {color}; border-radius: 10px;") + time.sleep(10) + + threading.Thread(target=check_status, daemon=True).start() + + + def append_log(self, full_message, log_type='default'): + """ + 接收全局日志并显示在 UI 中 + :param full_message: 带时间戳的完整日志信息 (str) + :param log_type: 日志类型 (str),如 info/warning/error/trigger 等 + """ + self.log_style_manager.insert_log(full_message, log_type) + + # 写入文件 + with open(self.log_file, "a", encoding="utf-8") as f: + f.write(full_message + "\n") + + def show_error(self, msg): + QMessageBox.critical(self, "错误", msg) + + def show_warning(self, msg): + QMessageBox.warning(self, "警告", msg) + + def show_info(self, title, msg): + QMessageBox.information(self, title, msg) + + def closeEvent(self, event): + reply = QMessageBox.question( + self, '退出', + "确定要退出吗?", + QMessageBox.Yes | QMessageBox.No, + QMessageBox.No + ) + if reply == QMessageBox.Yes: + event.accept() + else: + event.ignore() + + +if __name__ == "__main__": + app = QApplication(sys.argv) + window = MainWindow() + window.show() + sys.exit(app.exec()) \ No newline at end of file diff --git a/strategy.py b/strategy.py new file mode 100644 index 0000000..e852fc7 --- /dev/null +++ b/strategy.py @@ -0,0 +1,187 @@ +""" +行情策略管理模块 - 支持多种策略 +""" +import logging +import pandas as pd +import numpy as np +import pandas as pd +from quote_manager import QuoteManager, DataSource +from logger_utils_new import log_info, log_warning, log_error, log_trigger, log_debug, setup_logger, LOG_STYLES +from abc import ABC, abstractmethod +from enum import Enum, auto + + +class StrategyType(Enum): + TU_STRATEGY = auto() # 土策略 + BREAKOUT_STRATEGY = auto() # 突破策略 + MEAN_REVERSION_STRATEGY = auto() # 均值回归策略 + +class TradingStrategy(ABC): + def __init__(self, quote_manager): + self.quote_manager = quote_manager + self.history_data = {} + self.STOP_LOSS = 0.03 + self.COMMISSION = 0.0003 + self.MIN_TRADE_AMOUNT = 1e7 + @abstractmethod + def _analyze_signal(self, quote_data): + """分析交易信号""" + pass + +class TuStrategy(TradingStrategy): + """土策略实现""" + def analyze_signal(self, quote_data): + # 现有的土策略分析逻辑 + # ... 保留原有代码 ... + signal = { + 'code': quote_data['TS_CODE'], + 'signal': 'buy' if quote_data['PRICE'] < quote_data['OPEN'] else 'sell', + 'price': quote_data['PRICE'], + 'volume': quote_data['VOLUME'], + 'timestamp': pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S') + } + return signal + +class BreakoutStrategy(TradingStrategy): + """突破策略实现""" + def analyze_signal(self, quote_data): + # 实现突破策略逻辑 + # ... 新增代码 ... + signal = { + 'code': quote_data['TS_CODE'], + 'signal': 'buy' if quote_data['PRICE'] > quote_data['OPEN'] else'sell', + 'price': quote_data['PRICE'], + 'volume': quote_data['VOLUME'], + 'timestamp': pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S') + } + return signal + +class StrategyManager: + def __init__(self, quote_manager): + self.quote_manager = quote_manager + self.strategies = { + StrategyType.TU_STRATEGY: TuStrategy(quote_manager), + StrategyType.BREAKOUT_STRATEGY: BreakoutStrategy(quote_manager) + } + self.current_strategy = StrategyType.TU_STRATEGY # 默认策略 + + def set_strategy(self, strategy_type): + """设置当前使用的策略""" + if strategy_type in self.strategies: + self.current_strategy = strategy_type + return True + return False + + def monitor_stocks(self, stock_codes): + """使用当前策略监控股票""" + strategy = self.strategies[self.current_strategy] + return strategy.monitor_stocks(stock_codes) + + +class TradingStrategy(ABC): + def __init__(self, quote_manager): + self.quote_manager = QuoteManager() + + def monitor_stocks(self, stock_codes): + """监控股票行情""" + try: + quotes = self.quote_manager.get_realtime_quotes(stock_codes) + log_info(f"行情更新: {pd.Timestamp.now()}") + + signals = [] + for code, data in quotes.items(): + signal = self._analyze_signal(data) + log_info(f"{code}: 最新价 {data['PRICE']} 成交量 {data['VOLUME']}") + signals.append({ + 'code': code, + 'signal': signal, + 'price': data['PRICE'], + 'volume': data['VOLUME'], + 'timestamp': pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S') + }) + + + return signals + + except Exception as e: + log_error(f"获取行情失败: {str(e)}") + return [] + + def __init__(self, quote_manager): + self.quote_manager = quote_manager + self.history_data = {} + self.STOP_LOSS = 0.03 # 止损比例 + self.COMMISSION = 0.0003 # 单边佣金 + self.MIN_TRADE_AMOUNT = 1e7 # 最小成交金额 + + def _calculate_slope(self, x): + """计算斜率""" + if len(x) < 2: + return np.nan + + n = len(x) + sum_x = (n - 1) * n / 2 + sum_xx = (n - 1) * n * (2 * n - 1) / 6 + + sum_y = np.sum(x) + sum_xy = np.sum(np.arange(n) * x) + + denom = n * sum_xx - sum_x * sum_x + if denom == 0: + return np.nan + + return (n * sum_xy - sum_x * sum_y) / denom + + def _update_history_data(self, code, data): + """更新历史数据""" + if code not in self.history_data: + self.history_data[code] = [] + self.history_data[code].append(data) + # 只保留最近30天数据 + if len(self.history_data[code]) > 30: + self.history_data[code] = self.history_data[code][-30:] + return pd.DataFrame(self.history_data[code]) + + def _analyze_signal(self, quote_data): + """基于土策略分析交易信号""" + code = quote_data['TS_CODE'] + df = self._update_history_data(code, quote_data) + + if len(df) < 25: # 确保有足够数据计算指标 + return 'HOLD' + + # 计算技术指标 + df = df.sort_values('TRADE_DATE') + df['X_1'] = df['LOW'].rolling(10, min_periods=5).min() + df['X_2'] = df['HIGH'].rolling(25, min_periods=10).max() + df['X_6'] = ((df['CLOSE'] - df['X_1']) / (df['X_2'].replace(0, np.nan) - df['X_1']) * 4) + df['X_6'] = df['X_6'].ewm(span=4, adjust=False).mean().shift(1) + + # 信号过滤条件 + df['X_7'] = df['X_6'].rolling(5).apply( + lambda x: 0 if (np.diff(x > 3.5) == 1).any() else 1, raw=True + ).fillna(1) + + # 动量计算 + df['X_10'] = df['CLOSE'].pct_change(2).shift(1) * 100 + df['X_43'] = df['X_10'].rolling(2).sum() * df['X_7'] + + # 复合指标 + ema_open_12 = df['OPEN'].ewm(span=12, adjust=False).mean() + df['X_15'] = df['X_7'] * (df['OPEN'] - ema_open_12) / ema_open_12 * 200 + + # 斜率计算 + df['X_41'] = df['X_15'].rolling(2).apply(self._calculate_slope) + df['X_42'] = df['X_6'].rolling(2).apply(self._calculate_slope) + + # 最终土值计算 + df['tu_value'] = (df['X_41'] * 0.02 - df['X_42'] * df['X_7']) * df['X_7'] + + # 过滤条件 + current = df.iloc[-1] + if (current['X_43'] > 8 and + not current['CODE'].startswith(('313', '314', '315', '688')) and + current['AMOUNT'] > self.MIN_TRADE_AMOUNT and + not np.isnan(current['tu_value'])): + return 'BUY' if current['tu_value'] > 0 else 'SELL' + return 'HOLD' \ No newline at end of file diff --git a/strategy_monitor.py b/strategy_monitor.py new file mode 100644 index 0000000..77df6d0 --- /dev/null +++ b/strategy_monitor.py @@ -0,0 +1,166 @@ +""" +个股监控 - 监控个股状态 +""" +import tushare as ts +import time +import logging +from typing import Dict, List, Optional +from quote_manager import QuoteManager # 新增导入 +from logger_utils_new import setup_logger, LOG_STYLES # 新增导入 + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[logging.StreamHandler()] +) +logger = logging.getLogger(__name__) + + +# 替换原有的日志配置 +# 修改日志配置部分 +import logging +from logger_utils_new import setup_logger + +# 确保logger被正确初始化 +logger = setup_logger('strategy_monitor') # 使用logger_utils_new中的setup_logger + +# 如果setup_logger不可用,使用基本的日志配置 +if logger is None: + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' + ) + logger = logging.getLogger('strategy_monitor') + + +class StockMonitor: + def __init__(self, token: str): + """初始化监控器""" + self.quote_manager = QuoteManager() + ts.set_token(token) + self.pro = ts.pro_api() + + # 监控配置 + self.monitor_interval = 5 + self.stock_list = self._load_and_filter_stocks(r"D:\gp_data\history\20250714.txt") + + # 策略参数 + self.buy_threshold = 1.02 + self.sell_threshold = 0.98 + self.min_volume = 100000 + + def _load_and_filter_stocks(self, file_path: str) -> List[str]: + """从文件加载股票列表并过滤ST和科创板股票""" + try: + with open(file_path, 'r', encoding='utf-8') as f: + stocks = [line.strip() for line in f if line.strip()] + + # 过滤ST和科创板股票 + filtered_stocks = [] + for stock in stocks: + # 排除ST/*ST股票 + if stock.startswith(('ST', '*ST')): + continue + # 排除科创板股票(代码以688开头) + if stock.split('.')[0].startswith('688'): + continue + filtered_stocks.append(stock) + + logger.info(f"加载并过滤后股票数量: {len(filtered_stocks)}") + return filtered_stocks + + except Exception as e: + if logger: # 添加检查 + logger.error(f"加载股票列表失败: {str(e)}") + else: + print(f"加载股票列表失败: {str(e)}") # 后备方案 + return [ + "600519.SH", + "000858.SZ", + "601318.SH", + "600036.SH", + "000333.SZ" + ] + + def get_realtime_quotes(self) -> Optional[Dict]: + """使用quote_manager获取实时行情数据""" + try: + # 使用get_realtime_quotes方法替代get_quote + quotes_data = self.quote_manager.get_realtime_quotes(self.stock_list) + if not quotes_data: + return None + + # 转换数据格式以保持兼容 + quotes = {} + for code, row in quotes_data.items(): + # 确保row是字典类型 + if not isinstance(row, dict): + row = row.to_dict() if hasattr(row, 'to_dict') else {} + + quotes[code] = { + 'price': float(row.get('PRICE', 0)), + 'avg_price': (float(row.get('OPEN', 0)) + float(row.get('PRE_CLOSE', 0))) / 2, + 'volume': float(row.get('VOLUME', 0)) + } + return quotes + + except Exception as e: + logger.error(f"获取行情失败: {str(e)}") + return None + + def analyze_signals(self, quotes: Dict) -> List[Dict]: + """分析股票信号""" + signals = [] + for code, data in quotes.items(): + try: + current_price = float(data['price']) + avg_price = float(data['avg_price']) + volume = float(data['volume']) + + signal = 'HOLD' + if current_price > avg_price * self.buy_threshold and volume > self.min_volume: + signal = 'BUY' + elif current_price < avg_price * self.sell_threshold and volume > self.min_volume / 2: + signal = 'SELL' + + signals.append({ + 'code': code, + 'price': current_price, + 'avg_price': avg_price, + 'volume': volume, + 'signal': signal + }) + except Exception as e: + logger.error(f"分析股票{code}时出错: {str(e)}") + return signals + + def start_monitoring(self): + """开始监控""" + logger.info("启动股票监控系统...") + logger.info(f"监控股票: {', '.join(self.stock_list)}") + + try: + while True: + quotes = self.get_realtime_quotes() + if quotes: + signals = self.analyze_signals(quotes) + for signal in signals: + if signal['signal'] != 'HOLD': + logger.info( + f"信号: {signal['code']} | " + f"价格: {signal['price']:.2f} | " + f"均线: {signal['avg_price']:.2f} | " + f"成交量: {signal['volume']} | " + f"操作: {signal['signal']}" + ) + time.sleep(self.monitor_interval) + + except KeyboardInterrupt: + logger.info("监控已停止") + except Exception as e: + logger.error(f"监控异常终止: {str(e)}") + +if __name__ == "__main__": + monitor = StockMonitor("你的Tushare_token") + monitor.start_monitoring() \ No newline at end of file diff --git a/测试sina.py b/测试sina.py index bcf0b3f..350b436 100644 --- a/测试sina.py +++ b/测试sina.py @@ -3,12 +3,18 @@ import tushare as ts # 从环境变量读取Token(需提前设置环境变量TUSHARE_TOKEN) ts.set_token('9343e641869058684afeadfcfe7fd6684160852e52e85332a7734c8d') - +pro = ts.pro_api() try: # 定义股票代码列表,提升可维护性 # sina数据 + # 东财数据 + df1 = ts.realtime_list(src='dc') - stock_codes = ['600000.SH', '000001.SZ', '000001.SH'] + # sina数据 + df2 = ts.realtime_list(src='sina') + + stock_codes = ['300296.SZ', '300328.SZ'] + # dc_df = ts.realtime_list(src='dc') df = ts.realtime_quote(ts_code=','.join(stock_codes)) print(df) @@ -20,6 +26,8 @@ try: required_columns = ['HIGH', 'LOW', 'PRICE'] if all(col in df.columns for col in required_columns): print(df[required_columns]) + print(df['HIGH']) + else: print("列名不匹配,请检查数据源列名格式") else: diff --git a/短线速逆_历史.py b/短线速逆_历史.py new file mode 100644 index 0000000..9b94f99 --- /dev/null +++ b/短线速逆_历史.py @@ -0,0 +1,820 @@ +import tkinter as tk +from tkinter import ttk +from tkinter import messagebox # 👈 新增这一行 +import matplotlib.pyplot as plt +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import pandas as pd +import os +import logging +import tushare as ts +import datetime +import glob +from functools import lru_cache +import threading # 👈 新增这一行 +from order_executor import OrderExecutor + +plt.rcParams['font.sans-serif'] = ['SimHei'] # 设置中文字体 +plt.rcParams['axes.unicode_minus'] = False # 解决负号 '-' 显示为方块的问题 + +@lru_cache(maxsize=100) + +class StockAnalysisApp: + def __init__(self, root): + self.root = root + self.root.title("短线速逆-股票数据分析") + self.root.geometry("1000x1000") + self.days_var = tk.StringVar(value="1") # 默认显示1天 + self.count_days = 3 # 用于计算的天数 + self.days_for_limit_up = 5 # 公开参数,判断几天内是否有涨停 + self.trade_cal_cache = {} # 新增交易日历缓存字典 + # 初始化配置 + self.setup_config() + self.setup_ui() + + # 新增:初始化 OrderExecutor 实例 + self.order_executor = OrderExecutor() + + def setup_config(self): + """初始化配置""" + # 初始化 Matplotlib 字体设置 + # plt.rcParams['font.sans-serif'] = ['SimHei'] + # plt.rcParams['axes.unicode_minus'] = False + + # 设置 Tushare API Token + ts.set_token('9343e641869058684afeadfcfe7fd6684160852e52e85332a7734c8d') + self.pro = ts.pro_api() + + # 数据目录 + self.day_data_dir = r"D:\gp_data\day" + self.history_data_dir = r"D:\gp_data\history" + self.huice_data_dir = r"D:\gp_data\huice" + + # 初始化变量 + self.year_var = tk.StringVar() + self.month_var = tk.StringVar() + self.date_var = tk.StringVar() + self.offline_mode = tk.BooleanVar(value=False) + self.selected_board = tk.StringVar(value="all") + self.days_var = tk.StringVar(value="1") + self.TARGET_RATIO = 1.049 # 新增配置项 + self.MIN_LIMIT_UP_COUNT = 2 + + def setup_ui(self): + """设置用户界面""" + self.choose_frame = ttk.Frame(self.root) + self.choose_frame.grid(row=0, column=0, rowspan=1, padx=10, pady=10, sticky="ew") + self.root.grid_columnconfigure(0, weight=1) + # 年份选择框 + current_year = datetime.datetime.now().year + years = [str(current_year - i) for i in range(3)] + + # 创建蓝色大字体样式 + style = ttk.Style() + style.configure('Blue.TLabel', font=('微软雅黑', 12), foreground='blue') + style.configure('Blue.TButton', font=('微软雅黑', 12), foreground='blue') + style.configure('Red.TButton', font=('微软雅黑', 14), foreground='red') + style.configure('Blue.TCombobox', font=('微软雅黑', 14), foreground='red') + style.map('Blue.TCombobox', + fieldbackground=[('readonly', 'white')], + foreground=[('readonly', 'blue')], + selectbackground=[('readonly', 'white')], + selectforeground=[('readonly', 'blue')]) + + ttk.Label(self.choose_frame, text="选择年份:", style='Blue.TLabel').grid(row=0, column=0, padx=5, pady=10, sticky="w") + + self.year_var.set(years[0]) + year_menu = ttk.Combobox(self.choose_frame, textvariable=self.year_var, values=years, width=8, style='Blue.TCombobox') + year_menu.grid(row=0, column=1, padx=1, pady=5, sticky="w") + year_menu.bind("<>", lambda _: self.update_months()) + + # 月份选择框 + ttk.Label(self.choose_frame, text="选择月份:", style='Blue.TLabel').grid(row=0, column=2, padx=2, pady=10) + month_menu = ttk.Combobox(self.choose_frame, textvariable=self.month_var, width=10, state='readonly', style='Blue.TCombobox') + month_menu.grid(row=0, column=3, padx=2, pady=10) + month_menu.bind("<>", lambda _: (self.update_dates(), self.plot_monthly_data())) + + # 日期选择框 + ttk.Label(self.choose_frame, text="选择日期:", style='Blue.TLabel').grid(row=0, column=4, padx=10, pady=10) + date_menu = ttk.Combobox(self.choose_frame, textvariable=self.date_var, width=10, state='readonly', style='Blue.TCombobox') + date_menu.grid(row=0, column=5, padx=10, pady=10) + # date_menu.bind("<>", lambda _: (self.update_plot())) + + # 增加读取按钮 + load_btn = ttk.Button(self.choose_frame, text="读取数据", style='Blue.TButton', command=self.load_data) + load_btn.grid(row=0, column=6, padx=10, pady=10) + + # 增加导出按钮 + export_btn = ttk.Button(self.choose_frame, text="导出Excel", style='Red.TButton', command=self.export_to_excel) + export_btn.grid(row=0, column=7, padx=10, pady=10) + + self.set_frame = ttk.Frame(self.root) + self.set_frame.grid(row=1, column=0, rowspan=1, padx=10, pady=10, sticky="ew") + + # 显示触发股票数 + self.trigger_count_label = ttk.Label( + self.set_frame, + text="已触发股票数量:0", + font=('微软雅黑', 14), + foreground='red' + ) + self.trigger_count_label.grid(row=0, column=1, padx=10, pady=10) + # self.trigger_count_label.pack(padx=10, pady=5, side=tk.LEFT, anchor=tk.W) + + # 窗口置顶复选框 + self.topmost_var = tk.BooleanVar() + self.topmost_cb = ttk.Checkbutton( + self.set_frame, + text="窗口置顶", + variable=self.topmost_var, + command=self.toggle_topmost + ) + self.topmost_cb.grid(row=0, column=2, padx=10, pady=10) + + # 增加Treeview显示 + style = ttk.Style() + style.configure('Treeview', rowheight=20) # 设置行高 + + self.tree_frame = ttk.Frame(self.root) + self.tree_frame.grid(row=2, column=0, rowspan=1, padx=10, pady=10, sticky="ew") + # 增加图表显示 + self.setup_treeview() + + # 增加导出按钮 + export_btn = ttk.Button(self.set_frame, text="测试", command=self.showdate) + export_btn.grid(row=0, column=0, padx=10, pady=10) + + # 新增:创业板筛选复选框 + self.gem_only_var = tk.BooleanVar() + self.gem_only_cb = ttk.Checkbutton( + self.set_frame, + text="仅显示创业板", + variable=self.gem_only_var, + command=self.toggle_gem_filter + ) + self.gem_only_cb.grid(row=0, column=4, padx=10, pady=10) + + # 创建图表区域 + self.chart_frame = ttk.Frame(self.tree_frame) + self.chart_frame.grid(row=2, column=0, rowspan=5, padx=10, pady=10) + self.fig, self.ax = plt.subplots(figsize=(6, 4)) + self.canvas = FigureCanvasTkAgg(self.fig, master=self.chart_frame) + self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) + + # 在 setup_ui 方法中找到放置按钮的位置,并添加如下代码: + plot_btn = ttk.Button(self.set_frame, text="生成折线图", command=self.plot_monthly_data) + plot_btn.grid(row=0, column=3, padx=10, pady=10) + + def setup_treeview(self): + """设置Treeview组件""" + # 配置列属性 + self.base_columns = [ + ('code', '代码', 80), + ('name', '名称', 80), + ('open_zf', '开盘涨幅', 80), + ('day_zf', '当日涨幅', 80), # 新增当日涨幅列 + ('day_zz', '当日振幅', 80), # 新增当日振幅列 + ('lt_pan', '流通盘', 80), + ('target', '目标价', 80), + ('status', '状态', 60), + # ('limit_up', '涨停标记', 60), # 新增涨停列 + ] + # # 添加动态获利天数列 + days = int(self.count_days) + for day in range(1, days+1): + self.base_columns.append((f'profit_{day}', f'T{day}获利%', 60)) + + self.tree = ttk.Treeview( + self.tree_frame, + columns=[col[0] for col in self.base_columns], + show='headings', + height=20 # 设置默认显示行数 + ) + # 文字新增样式配置 + self.tree.tag_configure('triggered', foreground='red', font=('Verdana', 12, 'bold')) # 修改样式配置 + self.tree.tag_configure('wrong_buy', foreground='green', font=('Verdana', 11, 'bold')) # 修改样式配置 + + self.tree.grid(row=0, column=0, sticky="nsew") # 修改为 nsew 让控件向四个方向扩展 + + for col_id, col_text, col_width in self.base_columns: + self.tree.heading(col_id, text=col_text, + command=lambda c=col_id: self.treeview_sort_column(self.tree, c, False)) + self.tree.heading(col_id, text=col_text) # 设置列标题 + self.tree.column(col_id, width=col_width, anchor=tk.CENTER) + + scrollbar = ttk.Scrollbar(self.tree_frame, orient="vertical", command=self.tree.yview) + scrollbar.grid(row=0, column=1, sticky="ns") + self.tree.configure(yscrollcommand=scrollbar.set) + + # 添加以下代码让 tree_frame 的列可以扩展 + self.tree_frame.grid_columnconfigure(0, weight=1) + + # 新增:绑定双击事件 + self.tree.bind("", self.on_tree_double_click) + + def toggle_gem_filter(self): + """切换仅显示创业板状态时重新加载数据""" + self.load_data() + + def get_pure_code(self, code_with_suffix): + """去掉股票代码的 .SH 或 .SZ 后缀""" + if isinstance(code_with_suffix, str): + if '.' in code_with_suffix: + return code_with_suffix.split('.')[0] + return code_with_suffix + + # 新增:处理 TreeView 双击事件 + def on_tree_double_click(self, event): + """处理 TreeView 的双击事件""" + item = self.tree.selection() # 获取选中的行 + if item: + values = self.tree.item(item)['values'] # 获取该行的值 + if values: + code = values[0] # 提取股票代码 + print(code) + codes = self.get_pure_code(code) + self.place_order(codes,0) # 调用下单方法 + + # 新增:下单方法 + def place_order(self, code, tag=0): + """下单方法""" + try: + # 调用 OrderExecutor 的下单功能 + self.order_executor.place_order(code, auto_push=tag) # auto_push=1 表示自动下单 + print("下单成功", f"已成功下单股票代码: {code}") + except Exception as e: + print("下单失败", f"下单失败: {str(e)}") + + def treeview_sort_column(self, col, reverse): + """点击表头排序功能""" + # 获取当前所有行数据 + l = [(self.tree.set(k, col), k) for k in self.tree.get_children('')] + + # 特殊处理状态列排序 + if col == 'status': + # 修改优先级顺序,已触发排最前,错买其次,监控中最后 + priority_order = {'已触发': 0, '错买': 1, '监控中': 2} + l.sort(key=lambda t: priority_order.get(t[0], 3), reverse=reverse) + elif col == 'limit_up': + # 处理涨停列排序,'是' 排前面 + l.sort(key=lambda t: (t[0] != '是'), reverse=reverse) + else: + # 尝试转换为数字排序 + try: + l.sort(key=lambda t: float(t[0].replace('%', '')), reverse=reverse) + except (ValueError, AttributeError): + l.sort(reverse=reverse) + + # 重新排列Treeview中的行 + for index, (val, k) in enumerate(l): + self.tree.move(k, '', index) + + # 反转排序顺序 + self.tree.heading(col, command=lambda: self.treeview_sort_column(col, not reverse)) + + def export_to_excel(self): + """将Treeview数据导出到Excel文件""" + try: + # 检查Treeview是否为空 + if not self.tree.get_children(): + tk.messagebox.showwarning("导出警告", "当前没有可导出的数据!") + return + + # 检查并创建目录 + if not os.path.exists(self.huice_data_dir): + os.makedirs(self.huice_data_dir) + + # 获取当前日期作为默认文件名 + now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + filename = f"{self.huice_data_dir}\短线速逆回测数据_{now}.xlsx" + + # 获取Treeview所有数据 + data = [] + columns = self.tree["columns"] + data.append(columns) # 添加表头 + + for item in self.tree.get_children(): + values = [self.tree.set(item, col) for col in columns] + data.append(values) + + # 创建DataFrame并保存为Excel + df = pd.DataFrame(data[1:], columns=data[0]) + df.to_excel(filename, index=False) + + # 提示导出成功 + tk.messagebox.showinfo("导出成功", f"数据已成功导出到: {filename}") + except Exception as e: + tk.messagebox.showerror("导出错误", f"导出失败: {str(e)}") + + def format_code(self, code): + # 为股票代码添加后缀 + code = f"{code:0>6}" # 确保代码是6位,不足前面补零 + if code.startswith(("6", "9")): + f_code = f"{code}.SH" + elif code.startswith(("0", "2", "3")): + f_code = f"{code}.SZ" + else: + print(f"未知的股票代码格式: {code}") + return None + return f_code + + def toggle_topmost(self): + """切换窗口置顶状态""" + self.root.attributes('-topmost', self.topmost_var.get()) + if self.topmost_var.get(): + status = "窗口已置顶" + else: + status = "取消窗口置顶" + + # 获取当前时间 + current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + print(f"[{current_time}] {status}") + + + # 读取历史数据内容, 写入 treeview + def load_data(self): + """加载数据""" + self.trigger_count_label.config(text="数据加载中...") + year = self.year_var.get() + month = self.month_var.get().zfill(2) # 补零 + date = self.date_var.get().zfill(2) # 补零 + trade_date = f"{year}{month}{date}" # 格式化日期 + file_name = f"{year}{month}{date}.txt" + # 修改原路径拼接方式 + file_path = os.path.join(self.history_data_dir, f"{year}{month}{date}.txt") + + # 检查文件是否存在 + if not os.path.exists(file_path): + tk.messagebox.showwarning("文件不存在", f"未找到文件: {file_path}") + return + + # 清空Treeview + for item in self.tree.get_children(): + self.tree.delete(item) + + try: + with open(file_path, 'r') as f: + # 读取标题行并验证列 + headers = next(f).strip().split(',') + required_cols = ['条件选股', '代码'] + for col in required_cols: + if col not in headers: + raise KeyError(f"缺少必要列:{col}") + # 获取列索引 + name_col = headers.index('条件选股') + code_col = headers.index('代码') + + # 批量获取历史数据 + codes = [] + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + parts = line.split(',') + if len(parts) <= max(name_col, code_col): + continue + code = parts[code_col].strip() + # 确保代码是字符串格式 + if isinstance(code, float): # 如果code是浮点数 + code = str(int(code)) if code.is_integer() else str(code) + code = f"{code:0>6}" # 标准化为6位代码 + + # 判断是否仅显示创业板 + if self.gem_only_var.get(): + if code.startswith("30"): # 创业板股票代码以 "30" 开头 + codes.append(self.format_code(code)) + else: + codes.append(self.format_code(code)) + + days = int(self.count_days) + 1 # 增加天数 + # print(trade_date) + trade_dates = [None] * days # 初始化列表,用于存储未来的交易日 + for day in range(0, days): + #print(day) + trade_dates[day] = self.get_next_trade_date(trade_date, day) + print(trade_dates) + # 获取历史收盘价-------------------------- + import time + MAX_RETRIES = 3 + + for attempt in range(MAX_RETRIES): + try: + df_all = self.pro.daily( + ts_code=','.join(codes), + start_date=min(trade_date, *trade_dates), + end_date=max(trade_date, *trade_dates) + ) + break + except Exception as e: + if attempt == MAX_RETRIES - 1: + raise + time.sleep(2 ** attempt) # 指数退避 + # print(df_all) + + if df_all.empty: + raise ValueError("未找到历史数据,请确认:\n1.日期是否为交易日\n2.股票代码是否正确") + + # 创建价格映射表(包含T0和T1的价格数据) + price_map = {} + + for _, row in df_all.iterrows(): + ts_code = row['ts_code'] + trade_date = row['trade_date'] + + # 添加各天的数据 + for day in range(1, days + 1): + if trade_date == trade_dates[day - 1]: + price_map.setdefault(ts_code, {})[f'T{day}_open'] = row['open'] + price_map.setdefault(ts_code, {})[f'T{day}_close'] = row['close'] + price_map.setdefault(ts_code, {})[f'T{day}_high'] = row['high'] + price_map.setdefault(ts_code, {})[f'T{day}_low'] = row['low'] # 添加最低价 + price_map.setdefault(ts_code, {})[f'T{day}_pct_chg'] = row['pct_chg'] # 当日涨幅 + price_map.setdefault(ts_code, {})[f'T{day}_pre_close'] = row['pre_close'] # 前一日收盘价 + # price_map.setdefault(ts_code, {})[f'T{day}_limit'] = row['limit_status'] # 假设 limit_status 表示涨停状态 + print(price_map) + # 回到文件开头并处理数据 + f.seek(0) + next(f) # 跳过标题行 + for line in f: + line = line.strip() + # 计算涨停标记 + if not line or line.startswith('#'): + continue + parts = line.split(',') + if len(parts) <= max(name_col, code_col): + raise ValueError(f"数据行字段不足:{line}") + name = parts[name_col].strip() + code = parts[code_col].strip() + + # 确保代码保持字符串格式,保留前导零 + code = f"{code:0>6}" # 确保代码是6位,不足前面补零 + f_code = self.format_code(code) + + if not f_code: # 添加检查,如果格式不正确则跳过 + print(f"跳过 {code},股票代码格式不正确") + continue + + if f_code not in price_map or 'T1_close' not in price_map[f_code]: + print(f"跳过 {code},未找到收盘价数据") + continue + + # 计算重要数据 + T1_close = price_map[f_code]['T1_close'] + T2_open = price_map[f_code]['T2_open'] + T2_high = price_map[f_code]['T2_high'] + T2_close = price_map[f_code]['T2_close'] + # 计算目标价 + target_pic = round(float(T1_close) * 1.049, 2) + kp_zf = round(float(T2_open) / float(T1_close) - 1, 4) * 100 + status = self.check_status(target_pic, T2_high, T2_close) + + lt_pan = 0 # 初始值,实际值会在异步线程中更新 + + profit_values = [] + for day in range(2, days + 1): + if f'T{day}_close' in price_map.get(f_code, {}): + if day == 2: + day_close = price_map[f_code][f'T{day}_close'] + else: + day_close = price_map[f_code][f'T{day}_high'] + profit_pct = round((float(day_close) - float(target_pic)) / float(target_pic) * 100, 2) + profit_values.append(f"{profit_pct:.2f}%") + else: + profit_values.append("-") + limit_up_flag = False + for day in range(1, self.days_for_limit_up + 1): + limit_key = f'T{day}_limit' + if limit_key in price_map.get(f_code, {}) and price_map[f_code][limit_key] == 'U': + limit_up_flag = True + break + # 从缓存获取流通盘数据 + self.add_stock(code, name, kp_zf, target_pic, lt_pan, status, profit_values, price_map) + + # 总数 + total_stocks = len(self.tree.get_children()) + status_col = self.tree['columns'].index('status') + triggered_count = 0 + for item in self.tree.get_children(): + status = self.tree.item(item)['values'][status_col] + if status == '触发' or status == '错买': + triggered_count += 1 + + # 正确写法: + self.choose_frame.after(0, lambda: self.trigger_count_label.config(text=f"已触发股票数量:{triggered_count}/{total_stocks}")) + + except Exception as e: + print(f"加载文件失败:{str(e)}", 'error') + self.trigger_count_label.config(text="加载失败") + + def plot_monthly_data(self): + """绘制所选月份每日股票数量的折线图""" + selected_year = self.year_var.get() + selected_month = self.month_var.get().zfill(2) + + if not selected_year or not selected_month: + print("请选择年份和月份") + return + + # 构建日期前缀 (格式: YYYYMM) + date_prefix = f"{selected_year}{selected_month}" + + # 统计每天的数据量 + daily_counts = {} + for filename in os.listdir(self.history_data_dir): + if filename.startswith(date_prefix) and filename.endswith('.txt'): + try: + # 提取日期部分 (格式: YYYYMMDD.txt) + day = filename[6:8] # 获取DD部分 + date_key = f"{selected_year}-{selected_month}-{day}" + file_path = os.path.join(self.history_data_dir, filename) + + # 统计文件行数(不包括标题行) + with open(file_path, 'r') as f: + lines = f.readlines() + count = len(lines) - 1 # 减去标题行 + daily_counts[date_key] = count + except Exception as e: + print(f"处理文件 {filename} 时出错: {e}") + continue + + if not daily_counts: + print("没有数据可供绘图") + return + + # 清除之前的图表内容 + self.ax.clear() + + # 按照日期排序,并提取天数作为x轴标签 + sorted_full_dates = sorted(daily_counts.keys()) + + # 只保留“日”作为x轴标签 + x_labels = [date.split('-')[2] for date in sorted_full_dates] + counts = [daily_counts[date] for date in sorted_full_dates] + + # 使用整数索引作为x轴,之后设置自定义标签 + x_indices = list(range(len(sorted_full_dates))) + + # 绘制折线图 + self.ax.plot(x_indices, counts, marker='o', linestyle='-', color='blue') + self.ax.set_title(f'{selected_year}-{selected_month} 每日股票数量统计') + self.ax.set_xlabel('日期(日)') + self.ax.set_ylabel('股票数量') + self.ax.grid(True) + + # 设置x轴刻度和标签 + self.ax.set_xticks(x_indices) + self.ax.set_xticklabels(x_labels, rotation=45, ha='right') + + # 更新画布 + self.canvas.draw() + + def check_status(self, target_pic, high_pic, close_pic): + """检查股票状态""" + if high_pic > target_pic and close_pic >= target_pic: + return '触发' + if high_pic >= target_pic and close_pic < target_pic: + return '错买' + return '未触发' + + def add_stock(self, code, name, open_zf, target, lt_pan=0, status='计算中', profit_values=None, price_map=None): + """添加股票到监控列表""" + codes = self.format_code(str(code)) + # 构建values列表,包含基础列和多天获利数据 + # 获取T1收盘价 + if price_map is not None and codes in price_map and 'T1_close' in price_map[codes]: + # print(codes) + t1_close = price_map[codes]['T1_close'] + t1_open = price_map[codes]['T1_open'] + t1_pct_chg = price_map[codes]['T1_pct_chg'] + t1_pre_close = price_map[codes]['T1_pre_close'] + + else: + t1_close = 0 + print(f"跳过 {code},未找到T1收盘价数据") + + # 计算S点日涨幅(T2_open与T1_close的涨跌幅) + if price_map is not None and codes in price_map and 'T1_open' in price_map[codes] and t1_close > 0: + day_zf = round(float(t1_pct_chg), 2) + else: + day_zf = 0 + + # 计算当日振幅(T1_high与T1_low的差值相对于T0_close的百分比) + if (price_map is not None and codes in price_map and + 'T1_high' in price_map[codes] and 'T1_low' in price_map[codes] and t1_close > 0): + day_zz = round( + (float(price_map[codes]['T1_high']) - float(price_map[codes]['T1_low'])) / float(t1_pre_close) * 100, 2) + else: + day_zz = 0 + + values = [ + codes, + name, + f"{open_zf:.2f}%", + f"{day_zf:.2f}%", # 当日涨幅 + f"{day_zz:.2f}%", # 当日振幅 + f"{lt_pan:.2f}", + target, + status + ] + # 添加获利数据 + if profit_values: + values.extend(profit_values) + else: + # 如果没有提供profit_values,添加默认值 + values.extend(["-"] * self.count_days) + + item = self.tree.insert('', 'end', values=values) + # 根据状态设置标签样式 + if status == '触发': + self.tree.item(item, tags=('triggered',)) + elif status == '错买': + self.tree.item(item, tags=('wrong_buy',)) + + + + # 以下是原有函数改为类方法 + def treeview_sort_column(self, tv, col, reverse): + """对 Treeview 的列进行排序""" + data = [(tv.set(k, col), k) for k in tv.get_children("")] + try: + data.sort(key=lambda t: float(t[0].rstrip("%")), reverse=reverse) + except ValueError: + data.sort(reverse=reverse) + + for index, (val, k) in enumerate(data): + tv.move(k, "", index) + + tv.heading(col, command=lambda: self.treeview_sort_column(tv, col, not reverse)) + + def get_next_trade_date(self, date, n=1) -> str: + """获取n个交易日后的日期""" + try: + # 标准化输入日期格式 + clean_date = date.replace("-", "") + if len(clean_date) != 8: + raise ValueError("日期格式应为YYYYMMDD") + + # 检查缓存中是否已有该日期的交易日历 + if clean_date not in self.trade_cal_cache: + # 从API获取交易日历(获取前后30天的数据以确保覆盖) + start_date = (datetime.datetime.strptime(clean_date, "%Y%m%d") - + datetime.timedelta(days=30)).strftime("%Y%m%d") + end_date = (datetime.datetime.strptime(clean_date, "%Y%m%d") + + datetime.timedelta(days=30)).strftime("%Y%m%d") + + trade_cal = self.pro.trade_cal(exchange='', start_date=start_date, end_date=end_date) + # 只保留交易日 + trade_dates = trade_cal[trade_cal['is_open'] == 1]['cal_date'].tolist() + self.trade_cal_cache[clean_date] = sorted(trade_dates) + + trade_dates = self.trade_cal_cache[clean_date] + current_index = trade_dates.index(clean_date) + + if current_index + n >= len(trade_dates): + # 如果超出范围,返回计算的工作日日期 + future_date = (datetime.datetime.strptime(clean_date, "%Y%m%d") + + datetime.timedelta(days=n)).strftime("%Y%m%d") + return future_date + + return trade_dates[current_index + n] + + except Exception as e: + print(f"获取交易日历时出错: {e}") + # 如果API查询失败,返回计算的工作日日期 + try: + future_date = (datetime.datetime.strptime(clean_date, "%Y%m%d") + + datetime.timedelta(days=n)).strftime("%Y%m%d") + return future_date + except: + return None + + def showdate(self): + next_trade_date = self.get_next_trade_date("20250516", 1) # 获取20240101的下一个交易日 + print(f"下一个交易日是: {next_trade_date}") + print(self.trade_cal_cache) + + def update_months(self): + """更新月份""" + selected_year = self.year_var.get() + months = set() + # 遍历历史数据目录 + for filename in os.listdir(self.history_data_dir): + if filename.startswith(selected_year) and filename.endswith('.txt'): + try: + # 提取月份部分 (格式: YYYYMMDD.txt) + month = filename[4:6] # 获取MM部分 + months.add(month) + except (IndexError, ValueError): + continue + + # 将月份排序并更新下拉菜单 + sorted_months = sorted(months) + month_menu = self.choose_frame.children['!combobox2'] # 获取月份下拉框 + month_menu['values'] = sorted_months + + # 如果有月份数据,默认选择第一个 + if sorted_months: + self.month_var.set(sorted_months[0]) + self.update_dates() # 自动更新日期 + + def update_dates(self): + """更新日期""" + selected_year = self.year_var.get() + selected_month = self.month_var.get().zfill(2) + if not 1 <= int(selected_month) <= 12: + tk.messagebox.showerror("错误", "无效的月份") + return + dates = [] + # 构建日期前缀 (格式: YYYYMM) + date_prefix = f"{selected_year}{selected_month}" + # 遍历历史数据目录 + for filename in os.listdir(self.history_data_dir): + if filename.startswith(date_prefix) and filename.endswith('.txt'): + try: + # 提取日期部分 (格式: YYYYMMDD.txt) + day = filename[6:8] # 获取DD部分 + dates.append(day) + except (IndexError, ValueError): + continue + + # 将日期排序并更新下拉菜单 + sorted_dates = sorted(dates) + date_menu = self.choose_frame.children['!combobox3'] # 获取日期下拉框 + date_menu['values'] = sorted_dates + + # 如果有日期数据,默认选择第一个 + if sorted_dates: + self.date_var.set(sorted_dates[0]) + + +class DataDownloaderApp: + def __init__(self): + # self.downloader = DataDownloader() + self.root = tk.Tk() + self.running_threads = [] + self.setup_ui() + + def setup_ui(self): + # 添加UI设置代码 + self.root.title("数据下载器") + + # 创建复选框 + self.update_codes_var = tk.BooleanVar() + self.update_index_var = tk.BooleanVar() + self.update_stocks_var = tk.BooleanVar() + + tk.Checkbutton(self.root, text="更新股票代码表", variable=self.update_codes_var).pack(anchor=tk.W) + tk.Checkbutton(self.root, text="更新指数数据", variable=self.update_index_var).pack(anchor=tk.W) + tk.Checkbutton(self.root, text="更新个股数据", variable=self.update_stocks_var).pack(anchor=tk.W) + + # 创建按钮 + tk.Button(self.root, text="开始更新", command=self.on_update_button_click).pack(pady=10) + + # 创建状态标签 + self.codes_completion_label = tk.Label(self.root, text="") + self.codes_completion_label.pack() + self.index_completion_label = tk.Label(self.root, text="") + self.index_completion_label.pack() + self.stocks_completion_label = tk.Label(self.root, text="") + self.stocks_completion_label.pack() + + def on_update_button_click(self): + # 添加按钮点击事件处理代码 + if self.update_codes_var.get(): + thread = threading.Thread(target=self.update_codes) + thread.start() + self.running_threads.append(thread) + + if self.update_index_var.get(): + thread = threading.Thread(target=self.update_index) + thread.start() + self.running_threads.append(thread) + + if self.update_stocks_var.get(): + thread = threading.Thread(target=self.update_stocks) + thread.start() + self.running_threads.append(thread) + + def update_codes(self): + # 添加更新股票代码表的代码 + self.codes_completion_label.config(text="正在更新股票代码表...") + self.downloader.update_codes() + self.codes_completion_label.config(text="股票代码表更新完成!") + + def update_index(self): + # 添加更新指数数据的代码 + self.index_completion_label.config(text="正在更新指数数据...") + self.downloader.update_index() + self.index_completion_label.config(text="指数数据更新完成!") + + def update_stocks(self): + # 添加更新个股数据的代码 + self.stocks_completion_label.config(text="正在更新个股数据...") + self.downloader.update_stocks() + self.stocks_completion_label.config(text="个股数据更新完成!") + + +# 运行应用 +if __name__ == "__main__": + root = tk.Tk() + app = StockAnalysisApp(root) + root.mainloop() \ No newline at end of file diff --git a/短线速逆_监控.py b/短线速逆_监控.py new file mode 100644 index 0000000..2230550 --- /dev/null +++ b/短线速逆_监控.py @@ -0,0 +1,1050 @@ +import os +import sys +import tkinter as tk +from tkinter import ttk, messagebox, filedialog +import tushare as ts +import pandas as pd +import threading +import time +import datetime # 新增导入 +import re +import win32gui +import win32con +import pyautogui +from playsound import playsound +import pygetwindow as gw +import queue +from threading import Lock +from concurrent.futures import ThreadPoolExecutor +from order_executor import OrderExecutor +from logger_utils import setup_logger +from logger_utils import setup_logger, LOG_STYLES # 导入统一样式映射 +from logger_utils import log_info, log_warning, log_error, log_trigger, log_debug + + +class StockMonitor: + def __init__(self, master): + # 设置全局日志回调 + self.master = master + self.master.title("股票价格监控系统") + + # # 创建日志输出框 + log_frame = ttk.Frame(self.master) + log_frame.pack(side=tk.BOTTOM, fill=tk.BOTH, padx=5, pady=5) + + # self.log_label = ttk.Label(log_frame, text="日志输出:", font=('微软雅黑', 12)) + self.log_text = tk.Text(log_frame, wrap=tk.WORD, height=10, state=tk.DISABLED, font=('微软雅黑', 11), fg='blue') + scrollbar = ttk.Scrollbar(log_frame, command=self.log_text.yview) + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + self.log_text.config(yscrollcommand=scrollbar.set) + + setup_logger(self.append_log) # 注册主程序的日志回调函数 + + # 【关键点】自动加载所有日志 tag 样式 + for tag, style in LOG_STYLES.items(): + self.log_text.tag_configure(tag, **style) + + # 注册全局日志回调 + setup_logger(self.append_log) + + self.log_queue = queue.Queue() + self.start_log_processor() + ts.set_token('9343e641869058684afeadfcfe7fd6684160852e52e85332a7734c8d') + self.pro = ts.pro_api() + self.monitor_lock = Lock() + self.timer_lock = Lock() + self.thread_pool = ThreadPoolExecutor(max_workers=4) # 新增线程池 + self.executor = ThreadPoolExecutor(max_workers=2) # 添加这行 + self.float_share_cache = {} # 流通盘缓存 + self.monitor_lock = threading.Lock() # 缓存锁 + + self.error_count = 0 # 初始化错误计数器 + self.max_errors = 5 # 最大错误次数 + self.pause_on_error = False # 是否暂停刷新 + self.time_interval = 5 # 监控间隔时间(秒) + self.MIN_OPEN_CHANGE = -2.5 # 最小开盘涨幅(%) + self.MAX_OPEN_CHANGE = 3 # 最大开盘涨幅(%) + self.out_path = r"D:\gp_data" # 输出目录 + self.TARGET_RATIO = 1.049 # 目标涨幅比例 + self.limit_up_cache = {} # 新增涨停次数缓存 + self.auto_push_var = tk.IntVar(value=0) # 自动下单开关 + self.auto_check_var = tk.IntVar(value=0) # 自动检查开关 + self.auto_export_var = tk.IntVar(value=1) # 自动检查开关 + self.create_widgets() + self.monitor_active = True + self.timer_active = False # 新增计时器状态标志 + self.float_share_cache = {} # 新增流通盘数据缓存 + self.float_share_cache_max = 1000 # 添加缓存上限 + self.start_monitor() + self.trigger_count = 0 + self.previous_trading_status = None + self.start_status_update() + + # 添加触发提醒标签 + self.alert_label = tk.Label( + self.master, + text="", + font=('微软雅黑', 50, 'bold'), + fg='red', + bg='yellow', + width=20, # 宽度(字符数) + height=4 # 高度(行数) + ) + self.alert_label.place(relx=0.5, rely=0.5, anchor='center') + self.alert_label.place_forget() # 初始隐藏 + self.flash_count = 0 + self.max_flash = 0 # 闪动次数 + self.target_window_title = "东方财富终端" # 交易窗口的名称 + + # 构建界面 + def create_widgets(self): + # 创建Treeview容器框架 + tree_frame = ttk.Frame(self.master) + tree_frame.pack(padx=10, pady=10, fill=tk.BOTH, expand=True) + # 设置字体和样式 + style = ttk.Style() + style.configure('Larger.TButton', font=('微软雅黑', 12)) + + # 创建Treeview + self.tree = ttk.Treeview( + tree_frame, + columns=('code', 'name', 'open_zf', 'lt_pan', 'price', 'target', 'profit_pct', 'status', 'alert_time'), + show='headings', + height=20 # 设置默认显示行数 + ) + # 配置样式 + style = ttk.Style() + style.configure('Treeview', rowheight=20) # 设置行高 + # 文字新增样式配置 + self.tree.tag_configure('triggered', foreground='red', font=('Verdana', 12, 'bold')) # 修改样式配置 + self.tree.tag_configure('wrong_buy', foreground='green', font=('Verdana', 11, 'bold')) # 修改样式配置 + + # 添加双击事件绑定 + # 修改绑定方式为使用 lambda 忽略事件参数 + self.tree.bind('', lambda e: self.place_order()) + + # 配置列属性 + columns = [ + ('code', '代码', 80), + ('name', '名称', 80), + ('open_zf', '开盘涨幅', 80), + ('lt_pan', '流通盘', 80), + ('price', '当前价', 80), + ('target', '目标价', 80), + ('profit_pct', '获利%', 60), # 新增列 + ('status', '状态', 60), + ('alert_time', '预警时间', 200) + ] + + for col_id, col_text, col_width in columns: + self.tree.heading(col_id, text=col_text, + command=lambda c=col_id: self.treeview_sort_column(c, False)) + self.tree.column(col_id, width=col_width, anchor=tk.CENTER) + + # 创建滚动条 + vsb = ttk.Scrollbar( + tree_frame, + orient="vertical", + command=self.tree.yview + ) + self.tree.configure(yscrollcommand=vsb.set) + + # 布局组件 + self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + vsb.pack(side=tk.RIGHT, fill=tk.Y) + + # 创建控制按钮框架 + control_frame = ttk.Frame(self.master) + control_frame.pack(pady=20) + + # 文件选择按钮(原有代码保持不变) + self.btn_load = ttk.Button( + control_frame, + text="选择监控文件", + command=self.select_file, + style='Larger.TButton', # 应用样式 + width=18 # 新增宽度设置 + ) + self.btn_load.pack(side=tk.LEFT, padx=(5, 15)) # 显式指定侧边排列 + + self.btn_save = ttk.Button( + control_frame, + text="保存结果", + command=self.export_to_excel, + style='Larger.TButton', # 应用样式 + width=18 # 新增宽度设置 + ) + self.btn_save.pack(side=tk.LEFT, padx=(5, 15)) # 显式指定侧边排列 + + # 窗口置顶复选框 + self.topmost_var = tk.BooleanVar() + self.topmost_cb = ttk.Checkbutton( + control_frame, + text="窗口置顶", + variable=self.topmost_var, + command=self.toggle_topmost + ) + self.topmost_cb.pack(side=tk.LEFT, padx=5) + + # 复选框-自动保存treeview结果 + self.auto_export_cb = ttk.Checkbutton( + control_frame, + text="自动保存结果", + variable=self.auto_export_var, + # state=tk.ENABLED + ) + self.auto_export_cb.pack(side=tk.LEFT, padx=5) + + # 复选框-自动保存treeview结果 + self.auto_check_cb = ttk.Checkbutton( + control_frame, + text="调试", + variable=self.auto_check_var, + # state=tk.ENABLED + ) + self.auto_check_cb.pack(side=tk.LEFT, padx=5) + + # 复选框-自动下单 + self.auto_push_cb = ttk.Checkbutton( + control_frame, + text="半自动下单", + variable=self.auto_push_var, + state=tk.NORMAL + ) + self.auto_push_cb.pack(side=tk.LEFT, padx=5) + + # 显示触发股票数 + status_frame = ttk.Frame(self.master) + status_frame.pack(pady=5, fill=tk.X) + # 创建数量显示Label + self.trigger_count_label = ttk.Label( + status_frame, + text="已触发股票数量:0", + font=('微软雅黑', 14), + foreground='red' + ) + self.trigger_count_label.pack(padx=10, pady=5, side=tk.LEFT, anchor=tk.W) + + # 在status_frame中添加市场状态显示 + self.market_status_label = ttk.Label( + status_frame, + text="市场状态:-", + font=('微软雅黑', 14), + foreground='blue' + ) + self.market_status_label.pack(padx=20, pady=5, side=tk.LEFT, anchor=tk.W) + + # 开盘时间显示 + self.trading_status_label = ttk.Label( + status_frame, + text="当前状态:非交易时间", + font=('微软雅黑', 14), + foreground='red' + ) + self.trading_status_label.pack(padx=10, pady=5, side=tk.RIGHT, anchor=tk.E) + + # 添加状态灯(Canvas) + self.market_status_light = tk.Canvas(status_frame, width=20, height=20, bg='white', highlightthickness=0) + self.market_status_light.create_oval(2, 2, 18, 18, fill='gray', tags='light') + self.market_status_light.pack(padx=5, side=tk.RIGHT) + + # 最后更新时间标签 + self.last_update_label = ttk.Label( + status_frame, + text="最后更新: --:--:--", + font=('微软雅黑', 14), + foreground='gray' + ) + self.last_update_label.pack(padx=5, side=tk.RIGHT) + + # 在control_frame内添加定时监控组件 + timing_frame = ttk.Frame(control_frame) + timing_frame.pack(side=tk.LEFT, padx=(5, 15)) + + # 时间输入框 + timing_label = ttk.Label(timing_frame, text="定时时间:") + timing_label.pack(side=tk.LEFT) + self.timing_time = tk.StringVar() + self.timing_time.set("10:30") # 设置默认值为 9:30 + self.timing_entry = ttk.Entry(timing_frame, textvariable=self.timing_time, width=8) + self.timing_entry.pack(side=tk.LEFT) + + # 定时监控复选框 + self.timing_enabled = tk.BooleanVar() + self.timing_checkbox = ttk.Checkbutton( + control_frame, + text="定时结束监控", + variable=self.timing_enabled, + state=tk.NORMAL # state = tk.DISABLED + ) + self.timing_checkbox.pack(side=tk.LEFT, padx=5) + + def clean_cache(self): + with self.monitor_lock: + # 清理超过1000条记录的缓存 + if len(self.float_share_cache) > self.float_share_cache_max: + oldest_keys = sorted(self.float_share_cache.keys())[:100] + for key in oldest_keys: + del self.float_share_cache[key] + + def toggle_topmost(self): + """切换窗口置顶状态""" + self.master.attributes('-topmost', self.topmost_var.get()) + if self.topmost_var.get(): + log_info("窗口已置顶") + else: + log_info("取消窗口置顶") + + def get_limit_up_count(self, code, days=10): + """获取最近days天内涨停次数""" + with self.monitor_lock: # 新增锁机制 + if code in self.limit_up_cache: + return self.limit_up_cache[code] + try: + end_date = datetime.datetime.now().strftime('%Y%m%d') + start_date = (datetime.datetime.now() - datetime.timedelta(days=days)).strftime('%Y%m%d') + + df = self.pro.daily( + ts_code=code, + start_date=start_date, + end_date=end_date, + freq='D' + ) + if df.empty: + return 0 + + # 涨停条件:当日收盘价等于当日最高价,且涨幅>=9.8% + df['is_limit_up'] = (df['close'] == df['high']) & ((df['close'] / df['pre_close'] - 1) >= 0.098) + return df['is_limit_up'].sum() + + except Exception as e: + log_error(f"获取涨停次数失败: {str(e)}") + return 0 + + # 从缓存中获取 + + self.limit_up_cache[code] = count + return count + + def check_buy_conditions(self, code, current_price, open_price, pre_close, high_price): + """检查所有买入条件""" + open_zf = round((float(open_price) - float(pre_close)) / float(pre_close) * 100, 2) + # 基础条件 + is_within_open_range = (self.MIN_OPEN_CHANGE <= open_zf <= self.MAX_OPEN_CHANGE) + is_reached_target = (float(current_price) >= float(pre_close) * self.TARGET_RATIO) + + return is_within_open_range and is_reached_target + + def place_order(self): + selection = self.tree.selection() + if not selection: + log_warning("未选中任何股票") + return + + item = selection[0] + code = self.tree.item(item, 'values')[0] + pure_code = code.split('.')[0] + + log_info(f"选中股票代码: {pure_code}") + + try: + auto_flag = self.auto_push_var.get() + executor = OrderExecutor() + executor.place_order(pure_code, auto_flag) + except Exception as e: + log_error(f"下单失败: {str(e)}") + + # treeview 排序 + def treeview_sort_column(self, col, reverse): + """点击表头排序功能""" + # 获取当前所有行数据 + l = [(self.tree.set(k, col), k) for k in self.tree.get_children('')] + + # 特殊处理状态列排序 + if col == 'status': + # 修改优先级顺序,已触发排最前,错买其次,监控中最后 + priority_order = {'已触发': 0, '错买': 1, '监控中': 2} + l.sort(key=lambda t: priority_order.get(t[0], 3), reverse=reverse) + else: + # 尝试转换为数字排序 + try: + l.sort(key=lambda t: float(t[0].replace('%', '')), reverse=reverse) + except (ValueError, AttributeError): + l.sort(reverse=reverse) + + # 重新排列Treeview中的行 + for index, (val, k) in enumerate(l): + self.tree.move(k, '', index) + + # 反转排序顺序 + self.tree.heading(col, command=lambda: self.treeview_sort_column(col, not reverse)) + + def _do_append_log(self, info='message', log_type='default'): + """实际执行日志插入操作的方法""" + self.log_text.config(state=tk.NORMAL) + + # 规范化 log_type + log_type = log_type.lower() + valid_tags = ['default', 'info', 'loading', 'warning', 'error', 'trigger', 'debug'] + if log_type not in valid_tags: + log_type = 'default' + + self.log_text.insert(tk.END, f"{info}\n", (log_type,)) + self.log_text.see(tk.END) + self.log_text.config(state=tk.DISABLED) + + def start_log_processor(self): + def log_worker(): + while True: + record = self.log_queue.get() + if record is None: + break + self._do_append_log(**record) + self.log_queue.task_done() + + threading.Thread(target=log_worker, daemon=True).start() + + # 输出log + def append_log(self, info='message', log_type='default'): + self.log_queue.put({'info': info, 'log_type': log_type}) + + def flash_alert(self, code, price): + """显示闪动提醒""" + self.alert_label.config(text=f"{code}\n触发! {price}") + self.alert_label.place(relx=0.5, rely=0.5, anchor='center') + self.flash_count = 0 + self.do_flash() + + def do_flash(self): + """执行闪动动画""" + if self.flash_count < self.max_flash * 2: # 每次闪动包含显示和隐藏 + if self.flash_count % 2 == 0: + self.alert_label.place_forget() + else: + self.alert_label.place(relx=0.5, rely=0.5, anchor='center') + self.flash_count += 1 + self.master.after(500, self.do_flash) # 每500毫秒切换一次 + else: + self.alert_label.place_forget() + + def start_timer(self, message): + """启动计时器,每秒输出日志""" + self.timer_active = True + + def timer_loop(): + count = 1 + while self.timer_active: + log_warning(f"{message}...{count}..等待") + count += 1 + time.sleep(1) + + threading.Thread(target=timer_loop, daemon=True).start() + + def stop_timer(self): + """停止计时器""" + self.timer_active = False + + # 选中文件读取 + + def safe_update_tree_item(self, item, values): + """线程安全更新treeview条目""" + if self.master and hasattr(self.master, '_windowingsystem'): # 更安全的检查 + try: + self.master.after(0, lambda: self.tree.item(item, values=values) if hasattr(self, 'tree') else None) + except Exception as e: + print(f"安全更新失败: {e}") # 添加错误打印 + + def select_file(self): + filepath = filedialog.askopenfilename( + title="选择监控列表文件", + filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")] + ) + if filepath: + # 验证文件名是否为纯日期格式 + filename = os.path.basename(filepath) + if not re.match(r'^\d{8}\.txt$', filename): + messagebox.showerror("错误", "文件名必须为8位数字日期格式(如:20250401.txt)") + return + + trade_date = filename.split('.')[0] + self.load_stocks(filepath, trade_date) + + # 判断是否勾选定时监控 + if self.timing_enabled.get(): + time_str = self.timing_time.get().strip() + if not self.validate_time(time_str): + messagebox.showerror("错误", "时间格式应为 HH:MM(如:09:30)") + return + + # 将时间字符串转换为 datetime 对象 + target_time = datetime.datetime.strptime(time_str, "%H:%M").time() + # 模拟日期,假设为当前日期 + target_datetime = datetime.datetime.combine(datetime.datetime.now().date(), target_time) + + # 判断是否在开盘时间内 + if not self.is_trading_time_at(target_datetime): + log_trigger(f"定时时间 {time_str} 不在开盘时间中") + else: + log_trigger(f"定时时间 {time_str} 开始监控") + self.start_monitor_at_time(time_str) + else: + self.start_monitor() + + def is_trading_time_at(self, target_datetime): + """判断指定时间是否为交易日的开盘时间""" + current_time = target_datetime.time() + weekday = target_datetime.weekday() # 0-4 是周一到周五 + return ( + weekday < 5 and # 仅工作日 + ( + (datetime.time(9, 30) <= current_time <= datetime.time(11, 30)) or + (datetime.time(13, 0) <= current_time <= datetime.time(15, 0)) + ) + ) + + def load_stocks(self, filepath, trade_date): + # 判断是否是当日的股票数据,如果是就不操作,因为监控的是昨天或者以前的数据,不能是当天的 + today = datetime.datetime.now().strftime("%Y%m%d") + if trade_date == today: + log_error(f"尝试加载当日股票数据:{filepath}, 请加载昨天或者以前的日期") + return + + try: + self.tree.delete(*self.tree.get_children()) + + with open(filepath, 'r') as f: + # 读取标题行并验证列 + headers = next(f).strip().split(',') + required_cols = ['条件选股', '代码'] + for col in required_cols: + if col not in headers: + raise KeyError(f"缺少必要列:{col}") + + # 获取列索引 + name_col = headers.index('条件选股') + code_col = headers.index('代码') + + # 批量获取历史数据 + codes = [] + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + parts = line.split(',') + if len(parts) <= max(name_col, code_col): + continue + code = parts[code_col].strip() + code = f"{code:0>6}" # 标准化为6位代码 + codes.append(self.format_code(code)) + + # 获取历史收盘价 + df = self.pro.daily(ts_code=','.join(codes), trade_date=trade_date) + if df.empty: + raise ValueError("未找到历史数据,请确认:\n1.日期是否为交易日\n2.股票代码是否正确") + + # 创建收盘价映射表 + close_prices = {row['ts_code']: row['close'] for _, row in df.iterrows()} + + # 启动线程获取流通盘数据(只调用一次) + self.executor.submit(self.fetch_share_capital_data, codes) + + # 回到文件开头并处理数据 + f.seek(0) + next(f) # 跳过标题行 + for line in f: + line = line.strip() + if not line or line.startswith('#'): + continue + parts = line.split(',') + if len(parts) <= max(name_col, code_col): + raise ValueError(f"数据行字段不足:{line}") + name = parts[name_col].strip() + code = parts[code_col].strip() + code = f"{code:0>6}" # 确保代码是6位,不足前面补零 + f_code = self.format_code(code) + + # 获取收盘价 + if f_code not in close_prices: + print(f"跳过 {code},未找到收盘价数据") + continue + + close_price = close_prices[f_code] + target = round(float(close_price) * 1.049, 2) + + # 从缓存获取流通盘数据 + lt_pan = 0 # 初始值,实际值会在异步线程中更新 + self.add_stock(code, name, target, lt_pan) + + # 格式化显示日期 + formatted_date = f"{trade_date[:4]}-{trade_date[4:6]}-{trade_date[6:]}" + self.master.title(f"股票价格监控系统 - {formatted_date}") + log_info(f"加载文件成功:{filepath}") + + except Exception as e: + log_error(f"加载文件失败:{str(e)}") + + # def batch_get_share_capital(self, trade_date, codes=None): + # """批量获取流通盘数据""" + # self.start_timer('获取流通盘数据') + # + # def fetch_data(): + # try: + # dc_df = ts.realtime_list(src='dc') + # if dc_df.empty: + # raise ValueError("未找到股本数据") + # + # # 创建代码到流通市值的映射(单位:亿) + # float_mv_dict = { + # row['TS_CODE']: row['FLOAT_MV'] / 100000000 + # for _, row in dc_df.iterrows() + # } + # + # # 停止计时器 + # self.stop_timer() + # + # # 在主线程中更新UI + # self.master.after(0, update_ui, float_mv_dict) + # + # except Exception as e: + # log_error(f"批量获取流通盘数据失败:{str(e)},10秒后重试") + # self.stop_timer() + # self.master.after(10000, lambda: self.batch_get_share_capital(trade_date, codes)) + # + # def update_ui(float_mv_dict): + # # 更新缓存数据 + # self.float_share_cache.update(float_mv_dict) + # + # # 更新UI + # for item in self.tree.get_children(): + # code = self.tree.item(item, 'values')[0] + # lt_pan = self.float_share_cache.get(code, 0) + # self.tree.set(item, 'lt_pan', f"{lt_pan:.2f}亿") + # log_info("流通盘数据更新完成") + # + # # 启动线程获取数据 + # threading.Thread(target=fetch_data, daemon=True).start() + + def fetch_share_capital_data(self, codes): + """异步获取流通盘数据(运行在子线程)""" + try: + dc_df = ts.realtime_list(src='dc') + + if dc_df.empty: + raise ValueError("未找到股本数据") + + float_mv_dict = { + row['TS_CODE']: row['FLOAT_MV'] / 100000000 + for _, row in dc_df.iterrows() + } + + # 更新缓存 + with self.monitor_lock: + self.float_share_cache.update(float_mv_dict) + + # 触发UI更新(必须在主线程) + self.master.after(0, self.update_share_capital_ui, codes) + + except Exception as e: + log_error(f"批量获取流通盘数据失败:{str(e)}") + # 可选:10秒后重试 + self.master.after(10000, lambda: self.fetch_share_capital_data(codes)) + + def add_stock(self, code, name, target, lt_pan=0): + """添加股票到监控列表""" + codes = self.format_code(str(code)) + self.tree.insert('', 'end', values=( + codes, name, '-', f"{lt_pan:.2f}", '-', target, '-', + '监控中', '' # 初始显示为'-',等待异步加载 + )) + + # 检查是否为交易日 + def is_trading_time(self): + now = datetime.datetime.now() + current_time = now.time() + weekday = now.weekday() # 0-4是周一到周五 + return ( + weekday < 5 and # 仅工作日 + ( + (datetime.time(9, 30) <= current_time <= datetime.time(11, 30)) or + (datetime.time(13, 0) <= current_time <= datetime.time(15, 0)) + ) + ) + + def update_trading_status(self): + current_status = "开盘中" if self.is_trading_time() else "收盘时间" + color = "green" if current_status == "开盘中" else "red" + # 状态变更时才记录日志 + if current_status != self.previous_trading_status: + log_info(f"系统状态更新:{current_status}") + self.previous_trading_status = current_status + # 变更UI + self.master.after(0, lambda s=current_status, c=color: + self.trading_status_label.config(text=f"当前状态:{s}", foreground=c)) + + def update_share_capital_ui(self, codes): + """在主线程中更新 Treeview 的流通盘信息""" + try: + for item in self.tree.get_children(): + code = self.tree.item(item, 'values')[0] + lt_pan = self.float_share_cache.get(code, 0) + self.tree.set(item, 'lt_pan', f"{lt_pan:.2f}亿") + log_info("流通盘数据更新完成") + except Exception as e: + log_error(f"更新流通盘UI时发生错误:{str(e)}") + + + # 监控状态 + def start_status_update(self): + def _loop(): + while self.monitor_active: + self.update_trading_status() + time.sleep(1) # 每秒检查一次 + + threading.Thread(target=_loop, daemon=True).start() + + def validate_time(self, time_str): + try: + datetime.datetime.strptime(time_str, "%H:%M") + return True + except ValueError: + return False + + def on_timing_change(self, *args): + if self.timing_enabled.get(): + time_str = self.timing_time.get().strip() + if not self.validate_time(time_str): + messagebox.showerror("错误", "时间格式应为 HH:MM(如:09:30)") + self.timing_enabled.set(False) + return + self.start_monitor_at_time(time_str) + else: + self.monitor_active = True + self.start_monitor() + + def start_monitor_at_time(self, target_time): + now = datetime.datetime.now() + target = datetime.datetime.strptime(target_time, "%H:%M") + target = now.replace( + hour=target.hour, minute=target.minute, second=0, microsecond=0 + ) + if target < now: + target += datetime.timedelta(days=1) + delta = target - now + threading.Timer(delta.total_seconds(), self.start_monitor).start() + + # 格式化代码 + def format_code(self, code): + # 为股票代码添加后缀 + code = f"{code:0>6}" # 确保代码是6位,不足前面补零 + if code.startswith(("6", "9")): + f_code = f"{code}.SH" + elif code.startswith(("0", "2", "3")): + f_code = f"{code}.SZ" + else: + print(f"未知的股票代码格式: {code}") + return None + return f_code + + def play_audio(self, file_path): + try: + if not os.path.exists(file_path): + raise FileNotFoundError(f"文件路径不存在: {file_path}") + + # 使用playsound库静默播放 + + playsound(file_path, block=False) # block=False表示非阻塞播放 + + except FileNotFoundError as e: + log_error(f"音频文件错误: {e}") + except Exception as e: + log_error(f"播放音频失败: {e}") + + def fetch_batch_data(self, batch): + """并发获取单个批次的行情数据""" + try: + df = ts.realtime_quote(ts_code=','.join(batch)) + if df is None or df.empty: + return None + return {row['TS_CODE']: row for _, row in df.iterrows()} + except Exception as e: + log_error(f"获取行情失败(批次): {str(e)}") + return None + + def process_all_results(self, all_results): + """批量处理行情数据并准备更新项""" + updates = [] + triggered_items = [] + + # 提前获取列索引 + status_col = self.tree['columns'].index('status') + target_col = self.tree['columns'].index('target') + price_col = self.tree['columns'].index('price') + profit_pct_col = self.tree['columns'].index('profit_pct') + open_zf_col = self.tree['columns'].index('open_zf') + alert_time_col = self.tree['columns'].index('alert_time') + + now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + for code, row in all_results.items(): + current_price = float(row['PRICE']) + open_price = float(row['OPEN']) + pre_close = float(row['PRE_CLOSE']) + high_price = float(row['HIGH']) + + item = self.find_tree_item(code) + if not item: + continue + + values = list(self.tree.item(item, 'values')) + + # 计算开盘涨幅 + open_zf = round((open_price - pre_close) / pre_close * 100, 2) + values[open_zf_col] = f"{open_zf:.2f}%" + values[price_col] = f"{current_price:.2f}" + + # 计算获利比例 + target = float(values[target_col]) + profit_pct = round((current_price - target) / target * 100, 2) + values[profit_pct_col] = f"{profit_pct:.2f}%" + + # 判断买入条件 + buy_condition = self.check_buy_conditions(code, current_price, open_price, pre_close, high_price) + + # 正常触发条件 + if buy_condition and values[status_col] != '已触发': + values[alert_time_col] = now + values[status_col] = '已触发' + log_trigger(f"{code} 已触发!当前价:{current_price}") + triggered_items.append(code) + + # 自动下单 + if self.auto_push_var.get(): + self.place_order() + + # 错买判断 + was_above_target = high_price >= target + is_current_below = current_price < target + if was_above_target and is_current_below and values[status_col] != '错买': + values[status_col] = '错买' + log_warning(f"{code} 价格回落!错买,当前价:{current_price}") + + updates.append((item, values)) + + # 批量更新 UI + if updates: + self.master.after(0, self.batch_safe_update_tree_items, updates) + + # 统计并更新状态 + if updates: + triggered_count = sum(1 for _, vals in updates if vals[status_col] in ('已触发', '错买')) + self.master.after(0, lambda: self.trigger_count_label.config( + text=f"已触发股票数量:{triggered_count}" + )) + + total_stocks = len(updates) + if total_stocks > 0: + trigger_percent = (triggered_count / total_stocks) * 100 + market_status = self.get_market_status(trigger_percent) + self.master.after(0, lambda: self.market_status_label.config( + text=f"市场状态:{market_status}", + foreground=self.get_market_status_color(market_status) + )) + + # ✅ 新增:在数据更新后重新按状态排序 + self.treeview_sort_column('status', False) + + def batch_safe_update_tree_items(self, items_values_list): + """批量安全更新 Treeview 行""" + for item, values in items_values_list: + self._update_tree_item_safe(item, values) + + def update_prices(self): + if not self.is_trading_time() and not self.auto_check_var.get(): + if not getattr(self, '_logged_market_close', False): + log_info("当前为收盘时间,停止更新行情数据") + self._logged_market_close = True + return + else: + if getattr(self, '_logged_market_close', False): + del self._logged_market_close + log_info("已进入交易时间或调试模式,恢复行情更新") + + if self.pause_on_error: + log_error("暂停更新,等待恢复") + time.sleep(30) + self.pause_on_error = False + return + + if not hasattr(self, 'tree') or not self.tree.winfo_exists(): + return + + items = self.tree.get_children() + if not items: + return + + codes = [self.tree.item(item)['values'][0] for item in items] + formatted_codes = [self.format_code(str(code)) for code in codes if code] + + if not formatted_codes: + log_error("没有可监控的股票代码") + return + + batch_size = 40 + try: + with ThreadPoolExecutor(max_workers=4) as executor: + futures = [] + for i in range(0, len(formatted_codes), batch_size): + if not self.monitor_active: + return + batch = formatted_codes[i:i + batch_size] + futures.append(executor.submit(self.fetch_batch_data, batch)) + + all_results = {} + for future in futures: + result = future.result() + if result: + all_results.update(result) + + self.process_all_results(all_results) + + now = datetime.datetime.now() + self.last_update_label.config(text=f"最后更新: {now.strftime('%H:%M:%S')}") + self.market_status_light.itemconfig('light', fill='lime') + self.master.after(500, lambda: self.market_status_light.itemconfig('light', fill='gray')) + + except Exception as e: + self.error_count += 1 + if self.error_count >= self.max_errors: + log_error("连续错误过多,暂停更新30秒") + self.pause_on_error = True + self.error_count = 0 + log_error(f"更新价格严重错误: {str(e)}") + import traceback + traceback.print_exc() + + def _update_tree_item_safe(self, item, values): + """实际更新 Treeview 行的方法""" + if not item or not self.tree.exists(item): + return + + try: + status_col = self.tree['columns'].index('status') + current_status = values[status_col] + + # 设置 tag + if current_status == '已触发': + self.tree.item(item, tags=('triggered',)) + elif current_status == '错买': + self.tree.item(item, tags=('wrong_buy',)) + else: + self.tree.item(item, tags=()) + + # 更新整行数据 + self.tree.item(item, values=values) + + except Exception as e: + log_error(f"更新 Treeview 条目失败: {str(e)}") + + def find_tree_item(self, code): + for item in self.tree.get_children(): + if self.tree.item(item)['values'][0] == code: + return item + return None + + def start_monitor(self): + if self.timing_enabled.get(): + self.monitor_active = True + else: + self.monitor_active = True # 非定时模式直接启动 + + self.thread_pool.submit(self.monitor_loop) + + def monitor_loop(self): + while self.monitor_active: + self.update_prices() + time.sleep(self.time_interval) + + def get_market_status(self, percent): + """根据触发百分比返回市场状态""" + if percent == 0: + return "极弱" + elif percent <= 15: + return "弱势" + elif percent <= 20: + return "中等" + else: + return "活跃" + + @staticmethod + def get_market_status_color(status): + """根据市场状态返回颜色""" + return { + "极弱": "red", + "弱势": "orange", + "中等": "blue", + "活跃": "green" + }.get(status, "black") + + def export_to_excel(self): + """将Treeview数据导出到Excel文件""" + try: + # 检查Treeview是否为空 + if not self.tree.get_children(): + tk.messagebox.showwarning("导出警告", "当前没有可导出的数据!") + return + + # 获取当前日期作为默认文件名 + now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + filename = os.path.join(self.out_path, f"短线速逆数据_{now}.xlsx") + + # 获取Treeview所有数据 + data = [] + columns = self.tree["columns"] + data.append(columns) # 添加表头 + + for item in self.tree.get_children(): + values = [self.tree.set(item, col) for col in columns] + data.append(values) + + # 创建DataFrame并保存为Excel + df = pd.DataFrame(data[1:], columns=data[0]) + df.to_excel(filename, index=False) + + # 提示导出成功 + tk.messagebox.showinfo("导出成功", f"数据已成功导出到: {filename}") + except Exception as e: + tk.messagebox.showerror("导出错误", f"导出失败: {str(e)}") + + def on_closing(self): + try: + self.monitor_active = False # 停止所有后台循环 + self.executor.shutdown(wait=False) + self.thread_pool.shutdown(wait=False) + + # 取消所有 pending 的 after 事件 + for job in self.master.tk.eval('after info').split(): + self.master.after_cancel(job) + + time.sleep(0.2) # 给线程一点时间退出 + + if hasattr(self, 'master') and self.master.winfo_exists(): + self.master.quit() + self.master.destroy() + + except Exception as e: + log_error(f"关闭程序失败: {str(e)}") + sys.exit(0) + + +if __name__ == "__main__": + try: + root = tk.Tk() + app = StockMonitor(root) + root.protocol("WM_DELETE_WINDOW", app.on_closing) + root.mainloop() + except Exception as e: + print(f"程序发生异常: {str(e)}") + import traceback + traceback.print_exc() # 打印完整堆栈信息 + sys.exit(1) diff --git a/短线速逆_监控.spec b/短线速逆_监控.spec new file mode 100644 index 0000000..17b7541 --- /dev/null +++ b/短线速逆_监控.spec @@ -0,0 +1,38 @@ +# -*- mode: python ; coding: utf-8 -*- + + +a = Analysis( + ['短线速逆_监控.py'], + pathex=[], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + noarchive=False, + optimize=0, +) +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='短线速逆_监控', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/类-界面.py b/类-界面.py new file mode 100644 index 0000000..829865e --- /dev/null +++ b/类-界面.py @@ -0,0 +1,79 @@ +import tkinter as tk +from tkinter import ttk + + +class StockApp: + def __init__(self, root): + self.root = root + self.root.title("股票数据查询") + self.root.geometry("900x600") + + self.create_controls() + self.create_table() + self.load_sample_data() + + # 配置布局权重 + self.root.grid_rowconfigure(1, weight=1) + self.root.grid_columnconfigure(0, weight=1) + + def create_controls(self): + """创建顶部控制栏""" + t1_frame = ttk.Frame(self.root) + t2_frame = ttk.Frame(self.root) + t1_frame.grid(row=0, column=0, columnspan=6, padx=10, pady=10, sticky="nsew") + t2_frame.grid(row=1, column=0, columnspan=6, padx=10, pady=10, sticky="nsew") + + # 年份选择 + self.year_label = ttk.Label(t1_frame, text="选择年份:") + self.year_label.grid(row=0, column=0, padx=10, pady=10, sticky="w") + self.year_combobox = ttk.Combobox(t1_frame, values=["2023", "2024", "2025"], width=8) + self.year_combobox.grid(row=0, column=1, padx=10, pady=10, sticky="w") + self.year_combobox.set("2025") + + # 月份选择 + self.month_label = ttk.Label(t1_frame, text="选择月份:") + self.month_label.grid(row=0, column=2, padx=10, pady=10, sticky="w") + self.month_combobox = ttk.Combobox(t1_frame, values=[f"{i:02d}" for i in range(1, 13)], width=8) + self.month_combobox.grid(row=0, column=3, padx=10, pady=10, sticky="w") + + # 日期选择 + self.date_label = ttk.Label(t1_frame, text="选择日期:") + self.date_label.grid(row=0, column=4, padx=10, pady=10, sticky="w") + self.date_combobox = ttk.Combobox(t1_frame, values=[f"{i:02d}" for i in range(1, 32)], width=8) + self.date_combobox.grid(row=0, column=5, padx=10, pady=10, sticky="w") + + columns = ("代码", "名称", "目标价", "次日开盘价", "开盘涨幅", + "卖出最高价", "是否错买", "当日盈亏", "卖出盈亏") + self.tree = ttk.Treeview(t2_frame, columns=columns, show="headings", height=20) + self.tree.grid(row=1, column=0, columnspan=6, padx=10, pady=10, sticky="nsew") + + def create_table(self): + """创建表格组件""" + columns = ("代码", "名称", "目标价", "次日开盘价", "开盘涨幅", + "卖出最高价", "是否错买", "当日盈亏", "卖出盈亏") + + + # 设置列宽 + col_widths = [80, 100, 80, 100, 80, 100, 80, 80, 80] + for col, width in zip(columns, col_widths): + self.tree.heading(col, text=col) + self.tree.column(col, width=width, anchor="center") + + + + def load_sample_data(self): + """加载示例数据""" + sample_data = [ + ("600519", "贵州茅台", "1800.00", "1785.00", "-0.83%", + "1799.00", "否", "+0.78%", "+1.12%"), + ("000001", "平安银行", "15.30", "15.20", "-0.65%", + "15.50", "是", "-0.33%", "+1.31%") + ] + for data in sample_data: + self.tree.insert("", "end", values=data) + + +if __name__ == "__main__": + root = tk.Tk() + app = StockApp(root) + root.mainloop() \ No newline at end of file diff --git a/自动下单PYautogui测试.py b/自动下单PYautogui测试.py new file mode 100644 index 0000000..edd6507 --- /dev/null +++ b/自动下单PYautogui测试.py @@ -0,0 +1,319 @@ +import time +import pyautogui +import win32gui +import win32con +import tkinter as tk +from tkinter import filedialog +from logger_utils import setup_logger # 导入日志配置函数 +import datetime +from logger_utils import log_info, log_warning, log_error, log_trigger +from order_executor import OrderExecutor # 导入 OrderExecutor 类 下单类 +import os +from threading import Thread +import pygetwindow as gw + + +class Autotrading: + def __init__(self, master): + self.master = master + master.title("监控自动交易工具") + + + # 注册日志回调 + + self.order_executor = OrderExecutor() + + self.default_buy_price = 0.00 + self.default_sell_price = 0.00 + self.default_buy_volume = 100 + self.default_sell_volume = 100 + self.stock_code = "" + self.monitoring = False + self.monitor_thread = None + self.last_file_size = 0 + self.monitored_file = "" + self.monitoring = False # 添加监控状态标志 + self.hwnd = None # 交易窗口的句柄 + self.target_window_title = "东方财富终端" # 交易窗口的 + + # 创建界面组件 + self.label = tk.Label(master, text="选择类型:", anchor="w") + self.label.pack(anchor="w") + + # 添加单选按钮组 + self.mode_var = tk.StringVar(value="方式1") # 默认选择方式1 + self.mode1 = tk.Radiobutton(master, text="监控文件", variable=self.mode_var, font=('微软雅黑', 11), value="方式1") + self.mode2 = tk.Radiobutton(master, text="自动推送", variable=self.mode_var, font=('微软雅黑', 11), value="方式2") + self.mode1.pack(anchor="w") + self.mode2.pack(anchor="w") + + # 添加ST排除复选框 + self.exclude_st_var = tk.BooleanVar(value=True) + self.exclude_st_check = tk.Checkbutton( + master, + text="排除ST个股", + variable=self.exclude_st_var, + font=('微软雅黑', 11) + ) + self.exclude_st_check.pack(anchor="w") + + # 在 __init__ 中添加 + self.use_executor_var = tk.BooleanVar(value=True) # 默认启用 executor + self.executor_check = tk.Checkbutton( + master, + text="使用专业下单模块 (OrderExecutor)", + variable=self.use_executor_var, + font=('微软雅黑', 11) + ) + self.executor_check.pack(anchor="w") + + # 新增编辑框 + self.entry_label = tk.Label(master, text="输入策略名:", anchor="w") + self.entry_label.pack(anchor="w") + self.stock_entry = tk.Entry(master, width=25) + self.stock_entry.insert(0, "凤随☆") # 设置默认内容 + self.stock_entry.pack(anchor="w", padx=5) + + # 添加文件监控按钮 + self.monitor_button = tk.Button( + master, + text="监控文件", + command=self.choose_model, + width=15, + font=('微软雅黑', 12), # 设置字体和大小 + fg='red' # 字体颜色 + ) + self.monitor_button.pack() + + # 文件选择按钮 + self.file_button = tk.Button( + master, + text="选择文件", + command=self.select_file, + font=('微软雅黑', 12), # 设置字体和大小 + width=12 + ) + self.file_button.pack() + + # 文件显示标签 + self.file_label = tk.Label(master, text="未选择文件", font=('微软雅黑', 11)) + self.file_label.pack() + + # 新增窗口状态标签 + self.window_status = tk.Label(master, text="窗口状态: 未检测到", fg="red", font=('微软雅黑', 11)) + self.window_status.pack() + + # 添加日志窗口 + self.log_frame = tk.Frame(master) + self.log_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5) + + # 日志文本框,设置字体大小 + self.log_text = tk.Text( + self.log_frame, + height=20, + state='disabled', + font=('微软雅黑', 12) # 设置字体和大小,可按需调整 + ) + self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) + + # 滚动条 + self.log_scroll = tk.Scrollbar(self.log_frame, command=self.log_text.yview) + self.log_scroll.pack(side=tk.RIGHT, fill=tk.Y) + self.log_text.config(yscrollcommand=self.log_scroll.set) + + # === 设置日志颜色样式(放在这里)=== + self.log_text.tag_configure('error', foreground='red') + self.log_text.tag_configure('loading', foreground='orange') + self.log_text.tag_configure('trigger', foreground='green') + self.log_text.tag_configure('default', foreground='blue') + self.log_text.tag_configure('info', foreground='blue') + self.log_text.tag_configure('warning', foreground='orange') + + # 注册日志回调 + setup_logger(self.gui_log) + + def gui_log(self, message, level='default'): + """日志代理函数,用于将日志信息更新到 GUI""" + current_time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") + full_message = f" {message}\n" + self.log_text.config(state=tk.NORMAL) + self.log_text.insert(tk.END, full_message, level) + app.log_text.see(tk.END) + app.log_text.config(state=tk.DISABLED) + + def get_entry_content(self): + """获取输入框内容""" + content = self.stock_entry.get() + if not content.strip(): # 如果内容为空或只有空格 + log_error("警告:策略名不能为空!") + return None + return content.strip() + + def choose_model(self): + """根据单选按钮选择功能""" + selected_mode = self.mode_var.get() + if selected_mode == "方式1": + self.start_file_monitoring() + elif selected_mode == "方式2": + self.start_auto_push() + + # 获取窗口句柄 + def get_window_handle(self, window_title): + """获取指定标题的窗口句柄""" + hwnd = win32gui.FindWindow(None, window_title) + if not hwnd: + self.window_status.config(text=f"窗口状态: {window_title} 未打开", fg="red") + raise Exception(f"未找到标题为 '{window_title}' 的窗口") + self.window_status.config(text=f"窗口状态: {window_title}", fg="green") + return hwnd + + def get_window_content(self, hwnd): + """获取窗口可视内容(截图)""" + # 将窗口置顶 + win32gui.ShowWindow(hwnd, win32con.SW_RESTORE) + win32gui.SetForegroundWindow(hwnd) + + # 获取窗口位置和尺寸 + left, top, right, bottom = win32gui.GetWindowRect(hwnd) + width = right - left + height = bottom - top + + # 截图并保存 + screenshot = pyautogui.screenshot(region=(left, top, width, height)) + return screenshot + + def capture_window(self): + """截图功能""" + try: + hwnd = self.get_window_handle(self.target_window_title) + content = self.get_window_content(hwnd) + content.save(f"{self.target_window_title}.png") + self.file_label.config(text=f"截图已保存为 {self.target_window_title}.png") + except Exception as e: + self.file_label.config(text=f"错误: {str(e)}") + def start_auto_push(self): + """开始自动推送功能""" + self.place_order("002183", "买入", 100, 2.00, ) + + def start_file_monitoring(self): + if not self.monitored_file: + log_warning("请先选择要监控的文件") + return + + if self.monitoring: + self.monitoring = False + self.monitor_button.config(text="监控文件") + log_info(f"已停止监控文件: {self.monitored_file}") + else: + try: + self.get_window_handle(self.target_window_title) + self.monitoring = True + self.monitor_button.config(text="停止监控") + self.last_file_size = os.path.getsize(self.monitored_file) + self.monitor_thread = Thread(target=self.monitor_file_changes) + self.monitor_thread.daemon = True + self.monitor_thread.start() + log_info(f"开始监控文件: {self.monitored_file}") + except Exception as e: + log_error(f"未找到 {self.target_window_title} 窗口,请先打开该窗口再开始监控。错误信息: {str(e)}") + + def monitor_file_changes(self): + """监控文件变化的后台线程,处理ANSI格式文件""" + last_position = os.path.getsize(self.monitored_file) if os.path.exists(self.monitored_file) else 0 + last_warning_type = "" # 记录上一条预警类型 + # 获取策略名作为预警名 + warning_name = self.get_entry_content() + log_error(f"当前策略:{warning_name}") + + while self.monitoring: + + try: + current_size = os.path.getsize(self.monitored_file) + if current_size > last_position: # 比较当前文件大小和上次记录的位置 + with open(self.monitored_file, 'r', encoding='mbcs') as f: + f.seek(last_position) + new_lines = f.readlines() + last_position = f.tell() + + if new_lines: + for line in new_lines: + # 解析每行数据 + parts = line.strip().split('\t') + if len(parts) >= 6: + code = parts[0] # 代码 + name = parts[1] # 个股名称 + t_time = parts[2] # 预警时间 + price = parts[3] # 预警价格 + increase = parts[4] # 预警涨幅 + code_num = parts[5] # 编码 + warning_type = parts[6] if len(parts) > 6 else "" # 预警类型 + + output = f"预警类型: {warning_type}\n代码: {code}\n个股名称: {name}\n预警时间: {t_time}\n预警价格: {price}\n预警涨幅: {increase}\n编码: {code_num}\n" + + # 检查是否排除ST个股 + if self.exclude_st_var.get() and ('ST' in name or '*ST' in name): + log_error(f"排除ST个股: {name}({code})") + continue + + # 判断是否匹配当前策略 + if warning_type == warning_name: + log_trigger(f"策略触发----{warning_name}\n{output}") + # 调用下单函数(示例) + # self.place_order(code, name, price, self.default_buy_volume, 'buy') + else: + log_error(f"非指定策略-{warning_type}") + + log_info("--------------------------------") + except Exception as e: + log_error(f"监控出错: {str(e)}") + time.sleep(1) + time.sleep(0.5) + + def select_file(self): + """文件选择功能""" + if self.monitoring: + log_warning("请先停止监控再选择文件") + return + + file_path = filedialog.askopenfilename() + if file_path: + self.file_label.config(text=f"已选择文件: {file_path}") + self.monitored_file = file_path + log_info(f"已选择监控文件: {file_path}") + + def get_min_interval(self): + """检测系统最小可操作间隔""" + # 测试几次点击操作的最小间隔 + test_times = 3 + start_time = time.time() + for _ in range(test_times): + pyautogui.click(100, 100) # 在屏幕角落测试点击 + end_time = time.time() + # 计算平均间隔时间,并乘以安全系数(1.5) + return (end_time - start_time) / test_times * 1.5 + + def place_order(self, code, code_name, price, volume, order_type): + """ + 下单函数,优先使用 OrderExecutor 下单 + :param code: 股票代码 + :param code_name: 股票名称 + :param price: 下单价格 + :param volume: 下单数量 + :param order_type: 下单类型,如 'buy' 或 'sell' + """ + try: + pure_code = code[-6:] # 假设输入为 "SH600000" 或 "SZ000001" 等格式 + auto_push = 1 # 表示是否自动下单,这里默认开启 + + log_trigger(f"开始 {order_type} 下单,代码: {code},{code_name},价格: {price},数量: {volume}") + self.order_executor.place_order(pure_code, auto_push) + + log_trigger(f"{order_type} 下单成功,代码: {code},{code_name},价格: {price},数量: {volume}") + except Exception as e: + log_error(f"{order_type} 下单失败,代码: {code},错误信息: {str(e)}") + + +if __name__ == "__main__": + root = tk.Tk() + app = Autotrading(root) + root.mainloop()