release
This commit is contained in:
@@ -7,3 +7,5 @@ build/
|
||||
.vscode/
|
||||
example.db.bak
|
||||
venv/
|
||||
flet_desktop/
|
||||
.flet/
|
||||
|
||||
+15
-1
@@ -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)
|
||||
|
||||
+13
-6
@@ -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:
|
||||
# 检查当前时间是否已过 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):
|
||||
"""停止市场数据订阅"""
|
||||
|
||||
@@ -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
|
||||
|
||||
# 刷新网格订单(空仓只挂买单,有仓买卖对冲)
|
||||
# 只有市场活跃时才下单,收盘后不再尝试下单
|
||||
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 位置重新挂买卖单
|
||||
# 只有市场活跃时才下单,收盘后不再尝试下单
|
||||
if qmtv.isMarketActive:
|
||||
self.refreshGridOrder()
|
||||
else:
|
||||
PrintLog(LogLevel.INFO,
|
||||
f'|- 成交后市场已休市,跳过刷新网格订单')
|
||||
|
||||
finally:
|
||||
self.dataUpdateLock.release()
|
||||
|
||||
@@ -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
@@ -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
@@ -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={},
|
||||
|
||||
Reference in New Issue
Block a user