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()