新增加项目文件,
This commit is contained in:
3
.idea/.gitignore
generated
vendored
Normal file
3
.idea/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
6
.idea/MarsCodeWorkspaceAppSettings.xml
generated
Normal file
6
.idea/MarsCodeWorkspaceAppSettings.xml
generated
Normal 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>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
generated
Normal 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
7
.idea/misc.xml
generated
Normal 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
8
.idea/modules.xml
generated
Normal 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
8
.idea/real_view.iml
generated
Normal 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
6
.idea/vcs.xml
generated
Normal 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
80
StatusStyleDelegate.py
Normal 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
99
log_style_manager.py
Normal 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
82
logger_utils.py
Normal 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
73
logger_utils_new.py
Normal 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
58
order_executor.ahk
Normal 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
172
order_executor.py
Normal 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
113
pyauto操控-测试.py
Normal 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
111
quote_manager.py
Normal 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
894
stock_monitor_pyside.py
Normal 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
187
strategy.py
Normal 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
166
strategy_monitor.py
Normal 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()
|
||||||
12
测试sina.py
12
测试sina.py
@@ -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
820
短线速逆_历史.py
Normal 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
1050
短线速逆_监控.py
Normal file
File diff suppressed because it is too large
Load Diff
38
短线速逆_监控.spec
Normal file
38
短线速逆_监控.spec
Normal 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
79
类-界面.py
Normal 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()
|
||||||
319
自动下单PYautogui测试.py
Normal file
319
自动下单PYautogui测试.py
Normal 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()
|
||||||
Reference in New Issue
Block a user