新增加项目文件,

This commit is contained in:
2025-07-28 12:15:45 +08:00
parent 7e9e9cdc1d
commit f2eeecdcb2
24 changed files with 4395 additions and 2 deletions

3
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml

6
.idea/MarsCodeWorkspaceAppSettings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.codeverse.userSettings.MarscodeWorkspaceAppSettingsState">
<option name="ckgOperationStatus" value="SUCCESS" />
</component>
</project>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AhkProjectSettings">
<option name="defaultAhkSdk" value="AutoHotkey v2.0.19" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.10" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/real_view.iml" filepath="$PROJECT_DIR$/.idea/real_view.iml" />
</modules>
</component>
</project>

8
.idea/real_view.iml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="jdk" jdkName="Python 3.10" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

80
StatusStyleDelegate.py Normal file
View File

@@ -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 # 如果转换失败,保持默认颜色

99
log_style_manager.py Normal file
View File

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

82
logger_utils.py Normal file
View File

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

73
logger_utils_new.py Normal file
View File

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

58
order_executor.ahk Normal file
View File

@@ -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

172
order_executor.py Normal file
View File

@@ -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

113
pyauto操控-测试.py Normal file
View File

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

111
quote_manager.py Normal file
View File

@@ -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

894
stock_monitor_pyside.py Normal file
View File

@@ -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)) # 应该输出 <class '__main__.CustomSortModel'>
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())

187
strategy.py Normal file
View File

@@ -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'

166
strategy_monitor.py Normal file
View File

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

View File

@@ -3,12 +3,18 @@ import tushare as ts
# 从环境变量读取Token需提前设置环境变量TUSHARE_TOKEN # 从环境变量读取Token需提前设置环境变量TUSHARE_TOKEN
ts.set_token('9343e641869058684afeadfcfe7fd6684160852e52e85332a7734c8d') ts.set_token('9343e641869058684afeadfcfe7fd6684160852e52e85332a7734c8d')
pro = ts.pro_api()
try: try:
# 定义股票代码列表,提升可维护性 # 定义股票代码列表,提升可维护性
# sina数据 # 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)) df = ts.realtime_quote(ts_code=','.join(stock_codes))
print(df) print(df)
@@ -20,6 +26,8 @@ try:
required_columns = ['HIGH', 'LOW', 'PRICE'] required_columns = ['HIGH', 'LOW', 'PRICE']
if all(col in df.columns for col in required_columns): if all(col in df.columns for col in required_columns):
print(df[required_columns]) print(df[required_columns])
print(df['HIGH'])
else: else:
print("列名不匹配,请检查数据源列名格式") print("列名不匹配,请检查数据源列名格式")
else: else:

820
短线速逆_历史.py Normal file
View File

@@ -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("<<ComboboxSelected>>", 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("<<ComboboxSelected>>", 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("<<ComboboxSelected>>", 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("<Double-1>", 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()

1050
短线速逆_监控.py Normal file

File diff suppressed because it is too large Load Diff

38
短线速逆_监控.spec Normal file
View File

@@ -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,
)

79
类-界面.py Normal file
View File

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

View File

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