This commit is contained in:
2026-06-24 11:02:42 +08:00
parent c59c1b9fc4
commit ee8fc60eae
7 changed files with 105 additions and 16 deletions
+2
View File
@@ -7,3 +7,5 @@ build/
.vscode/
example.db.bak
venv/
flet_desktop/
.flet/
+15 -1
View File
@@ -11,13 +11,27 @@ EventPrintLog = "print_log" # 打印日志
class EventBus:
def __init__(self):
self.listeners = {} # 管理各种event的订阅情况
self.last_events = {} # 存储每个事件的最后一次值,用于"重播"给新订阅者
def subscribe(self, event_type, listener):
def subscribe(self, event_type, listener, replay=True):
"""订阅事件
Args:
event_type: 事件类型
listener: 回调函数
replay: 是否自动重播最近一次事件状态(默认True)
"""
if event_type not in self.listeners:
self.listeners[event_type] = []
self.listeners[event_type].append(listener)
# 新订阅者自动收到最近一次事件状态(如果存在)
if replay and event_type in self.last_events:
listener(self.last_events[event_type])
def publish(self, event_type, data):
# 存储最后一次事件值
self.last_events[event_type] = data
if event_type in self.listeners:
for listener in self.listeners[event_type]:
listener(data)
+15 -8
View File
@@ -545,11 +545,19 @@ class RealQmtV:
PrintLog(LogLevel.ERROR, f'- [市场数据订阅失败-{e}]')
def _on_market_data(self, datas: dict):
"""xtquant 行情回调 — 收到行情即标记市场活跃"""
"""xtquant 行情回调 — 收到行情即标记市场活跃(但需满足 09:15 后才激活)"""
self.lastMarketDataUpdateTimestamp = time.time()
if not self.isMarketActive:
self.isMarketActive = True
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, True)
# 检查当前时间是否已过 09:15(集合竞价结束后才激活市场状态)
import zoneinfo
beijing_tz = zoneinfo.ZoneInfo("Asia/Shanghai")
now = datetime.datetime.now(beijing_tz)
t = now.time()
activation_time = datetime.time(9, 15)
if t >= activation_time:
self.isMarketActive = True
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, True)
PrintLog(LogLevel.INFO, f'- [行情] 市场激活 (时间 {t.strftime("%H:%M:%S")} >= 09:15)')
eBus.event_bus.publish(eBus.MarketDataUpdate, datas)
def _is_trading_time(self) -> bool:
@@ -567,17 +575,16 @@ class RealQmtV:
return (morning_start <= t <= morning_end) or (afternoon_start <= t <= afternoon_end)
def _market_data_watchdog(self):
"""行情活跃监控 — 超过 120 秒无行情 且 非交易时间 则标记市场不活跃"""
"""行情活跃监控 — 超过 120 秒无行情 则标记市场不活跃(无论是否交易时间)"""
while True:
time.sleep(15)
if self.isMarketActive:
elapsed = time.time() - self.lastMarketDataUpdateTimestamp
# 非交易时间直接标记休市;交易时间内才用超时判断
if not self._is_trading_time() or elapsed > 120:
# 只有超过 120 秒无行情才标记不活跃,不再区分交易时间
if elapsed > 120:
self.isMarketActive = False
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, False)
if not self._is_trading_time():
PrintLog(LogLevel.INFO, '- [行情] 非交易时间,市场标记为不活跃')
PrintLog(LogLevel.INFO, f'- [行情] 超过 {elapsed:.0f} 秒无数据,市场标记为不活跃')
def stopMarketDataSubscription(self):
"""停止市场数据订阅"""
+20 -6
View File
@@ -54,6 +54,11 @@ class SFGridStrategy:
"""
self.tradeTarget: model.SFGridTradeTarget = tradeTarget
# 数据更新锁:保护 orderGrid 和 tradeTarget 的并发访问
# QMT 回调在独立线程中触发,必须在可能触发回调的操作之前创建
# 注意:这个锁必须在订阅事件之前创建,防止事件在初始化期间触发
self.dataUpdateLock = threading.Lock()
# 订阅事件总线:监听订单创建、成交、失败三种事件
event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync)
event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade)
@@ -75,10 +80,6 @@ class SFGridStrategy:
# 加载券商侧已存在的未成交订单,恢复到 orderGrid 中
self.loadExistOrders()
# 数据更新锁:保护 orderGrid 和 tradeTarget 的并发访问
# QMT 回调在独立线程中触发,必须在可能触发回调的操作之前创建
self.dataUpdateLock = threading.Lock()
# 根据数据库中的 enabled 字段决定是否启动交易
self.enabledTrading(tradeTarget.enabled) # type: ignore
@@ -122,6 +123,9 @@ class SFGridStrategy:
当市场数据从不可用变为可用时,如果策略已启用则刷新网格订单。
"""
PrintLog(LogLevel.INFO,
f'|- [市场状态切换] 标的{self.tradeTarget.targetName()} '
f'isActive={isActive}, enabled={self.tradeTarget.enabled}')
if isActive and self.tradeTarget.enabled:
self.refreshGridOrder()
@@ -300,7 +304,12 @@ class SFGridStrategy:
self.tradeTarget.enabled = False # type: ignore
# 刷新网格订单(空仓只挂买单,有仓买卖对冲)
self.refreshGridOrder()
# 只有市场活跃时才下单,收盘后不再尝试下单
if qmtv.isMarketActive:
self.refreshGridOrder()
else:
PrintLog(LogLevel.INFO,
f' |- 市场已休市,跳过刷新网格订单')
else:
# ── 停用交易: 取消所有未成交订单 ──
@@ -465,7 +474,12 @@ class SFGridStrategy:
PrintLog(LogLevel.INFO,
f' 手续费 : {trade.commission:.3f}')
# 4. 刷新网格订单:在新的 grid_index 位置重新挂买卖单
self.refreshGridOrder()
# 只有市场活跃时才下单,收盘后不再尝试下单
if qmtv.isMarketActive:
self.refreshGridOrder()
else:
PrintLog(LogLevel.INFO,
f'|- 成交后市场已休市,跳过刷新网格订单')
finally:
self.dataUpdateLock.release()
+3
View File
@@ -1279,8 +1279,11 @@ class QmtApp:
self._refresh_ui()
def _on_market_active(self, is_active: bool):
from core.logger import PrintLog, LogLevel
PrintLog(LogLevel.INFO, f'[UI] 收到市场状态切换事件 is_active={is_active}')
self._update_market_status(is_active)
self.page.update()
PrintLog(LogLevel.INFO, f'[UI] 市场状态已更新,_market_label={self._market_label.value}')
def _update_market_status(self, active: bool):
"""更新顶栏市场状态指示灯"""
+49
View File
@@ -4,6 +4,55 @@
使用 --tk 参数切换到 Tkinter UI。
"""
import sys
import os
import subprocess
import ssl
import traceback
# 修复 Windows 上 flet_desktop 子进程弹出控制台窗口的问题
# 原始 Popen 不带 CREATE_NO_WINDOW 标志,会为每个子进程创建控制台窗口
_original_popen = subprocess.Popen
class Popen(_original_popen):
def __init__(self, *args, **kwargs):
if sys.platform == "win32" and "creationflags" not in kwargs:
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
super().__init__(*args, **kwargs)
subprocess.Popen = Popen
# PyInstaller 打包后,设置 FLET_VIEW_PATH 指向打包内的 Flet 客户端
# 避免从 GitHub 下载
if getattr(sys, 'frozen', False):
# 运行在打包后的 exe 中
base_path = sys._MEIPASS # PyInstaller 解压到的临时目录
flet_client_path = os.path.join(base_path, '.flet', 'client', 'flet-desktop-full-0.85.3')
flet_exe = os.path.join(flet_client_path, 'flet', 'flet.exe')
# 写入日志便于调试
log_file = os.path.join(os.path.dirname(sys.executable), 'startup_log.txt')
with open(log_file, 'w') as f:
f.write(f'base_path: {base_path}\n')
f.write(f'flet_client_path: {flet_client_path}\n')
f.write(f'flet_exe: {flet_exe}\n')
f.write(f'exists: {os.path.exists(flet_exe)}\n')
if os.path.exists(flet_exe):
f.write('Setting FLET_VIEW_PATH\n')
os.environ['FLET_VIEW_PATH'] = flet_client_path
f.write(f'FLET_VIEW_PATH: {os.environ.get("FLET_VIEW_PATH")}\n')
# 禁用 SSL 验证,避免证书问题
if hasattr(ssl, '_create_unverified_context'):
ssl._create_default_https_context = ssl._create_unverified_context
def excepthook(type, value, tb):
"""捕获未处理的异常,写入日志"""
log_file = os.path.join(os.path.dirname(sys.executable), 'error_log.txt')
with open(log_file, 'w') as f:
f.write(''.join(traceback.format_exception(type, value, tb)))
sys.__excepthook__(type, value, tb)
sys.excepthook = excepthook
if __name__ == '__main__':
if '--tk' in sys.argv:
+1 -1
View File
@@ -4,7 +4,7 @@ a = Analysis(
['starter.py'],
pathex=[],
binaries=[],
datas=[('xtquant/xtdata.ini', 'xtquant'), ('.flet/client', '.flet/client')], # xtdata 依赖的配置文件
datas=[('xtquant/xtdata.ini', 'xtquant'), ('flet_desktop/app', 'flet_desktop/app')], # xtdata 依赖的配置文件
hiddenimports=['brotli', 'brotli.encoding'],
hookspath=[],
hooksconfig={},