release
This commit is contained in:
@@ -7,3 +7,5 @@ build/
|
|||||||
.vscode/
|
.vscode/
|
||||||
example.db.bak
|
example.db.bak
|
||||||
venv/
|
venv/
|
||||||
|
flet_desktop/
|
||||||
|
.flet/
|
||||||
|
|||||||
+15
-1
@@ -11,13 +11,27 @@ EventPrintLog = "print_log" # 打印日志
|
|||||||
class EventBus:
|
class EventBus:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.listeners = {} # 管理各种event的订阅情况
|
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:
|
if event_type not in self.listeners:
|
||||||
self.listeners[event_type] = []
|
self.listeners[event_type] = []
|
||||||
self.listeners[event_type].append(listener)
|
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):
|
def publish(self, event_type, data):
|
||||||
|
# 存储最后一次事件值
|
||||||
|
self.last_events[event_type] = data
|
||||||
if event_type in self.listeners:
|
if event_type in self.listeners:
|
||||||
for listener in self.listeners[event_type]:
|
for listener in self.listeners[event_type]:
|
||||||
listener(data)
|
listener(data)
|
||||||
|
|||||||
+15
-8
@@ -545,11 +545,19 @@ class RealQmtV:
|
|||||||
PrintLog(LogLevel.ERROR, f'- [市场数据订阅失败-{e}]')
|
PrintLog(LogLevel.ERROR, f'- [市场数据订阅失败-{e}]')
|
||||||
|
|
||||||
def _on_market_data(self, datas: dict):
|
def _on_market_data(self, datas: dict):
|
||||||
"""xtquant 行情回调 — 收到行情即标记市场活跃"""
|
"""xtquant 行情回调 — 收到行情即标记市场活跃(但需满足 09:15 后才激活)"""
|
||||||
self.lastMarketDataUpdateTimestamp = time.time()
|
self.lastMarketDataUpdateTimestamp = time.time()
|
||||||
if not self.isMarketActive:
|
if not self.isMarketActive:
|
||||||
self.isMarketActive = True
|
# 检查当前时间是否已过 09:15(集合竞价结束后才激活市场状态)
|
||||||
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, True)
|
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)
|
eBus.event_bus.publish(eBus.MarketDataUpdate, datas)
|
||||||
|
|
||||||
def _is_trading_time(self) -> bool:
|
def _is_trading_time(self) -> bool:
|
||||||
@@ -567,17 +575,16 @@ class RealQmtV:
|
|||||||
return (morning_start <= t <= morning_end) or (afternoon_start <= t <= afternoon_end)
|
return (morning_start <= t <= morning_end) or (afternoon_start <= t <= afternoon_end)
|
||||||
|
|
||||||
def _market_data_watchdog(self):
|
def _market_data_watchdog(self):
|
||||||
"""行情活跃监控 — 超过 120 秒无行情 且 非交易时间 则标记市场不活跃"""
|
"""行情活跃监控 — 超过 120 秒无行情 则标记市场不活跃(无论是否交易时间)"""
|
||||||
while True:
|
while True:
|
||||||
time.sleep(15)
|
time.sleep(15)
|
||||||
if self.isMarketActive:
|
if self.isMarketActive:
|
||||||
elapsed = time.time() - self.lastMarketDataUpdateTimestamp
|
elapsed = time.time() - self.lastMarketDataUpdateTimestamp
|
||||||
# 非交易时间直接标记休市;交易时间内才用超时判断
|
# 只有超过 120 秒无行情才标记不活跃,不再区分交易时间
|
||||||
if not self._is_trading_time() or elapsed > 120:
|
if elapsed > 120:
|
||||||
self.isMarketActive = False
|
self.isMarketActive = False
|
||||||
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, False)
|
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, False)
|
||||||
if not self._is_trading_time():
|
PrintLog(LogLevel.INFO, f'- [行情] 超过 {elapsed:.0f} 秒无数据,市场标记为不活跃')
|
||||||
PrintLog(LogLevel.INFO, '- [行情] 非交易时间,市场标记为不活跃')
|
|
||||||
|
|
||||||
def stopMarketDataSubscription(self):
|
def stopMarketDataSubscription(self):
|
||||||
"""停止市场数据订阅"""
|
"""停止市场数据订阅"""
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ class SFGridStrategy:
|
|||||||
"""
|
"""
|
||||||
self.tradeTarget: model.SFGridTradeTarget = tradeTarget
|
self.tradeTarget: model.SFGridTradeTarget = tradeTarget
|
||||||
|
|
||||||
|
# 数据更新锁:保护 orderGrid 和 tradeTarget 的并发访问
|
||||||
|
# QMT 回调在独立线程中触发,必须在可能触发回调的操作之前创建
|
||||||
|
# 注意:这个锁必须在订阅事件之前创建,防止事件在初始化期间触发
|
||||||
|
self.dataUpdateLock = threading.Lock()
|
||||||
|
|
||||||
# 订阅事件总线:监听订单创建、成交、失败三种事件
|
# 订阅事件总线:监听订单创建、成交、失败三种事件
|
||||||
event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync)
|
event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync)
|
||||||
event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade)
|
event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade)
|
||||||
@@ -75,10 +80,6 @@ class SFGridStrategy:
|
|||||||
# 加载券商侧已存在的未成交订单,恢复到 orderGrid 中
|
# 加载券商侧已存在的未成交订单,恢复到 orderGrid 中
|
||||||
self.loadExistOrders()
|
self.loadExistOrders()
|
||||||
|
|
||||||
# 数据更新锁:保护 orderGrid 和 tradeTarget 的并发访问
|
|
||||||
# QMT 回调在独立线程中触发,必须在可能触发回调的操作之前创建
|
|
||||||
self.dataUpdateLock = threading.Lock()
|
|
||||||
|
|
||||||
# 根据数据库中的 enabled 字段决定是否启动交易
|
# 根据数据库中的 enabled 字段决定是否启动交易
|
||||||
self.enabledTrading(tradeTarget.enabled) # type: ignore
|
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:
|
if isActive and self.tradeTarget.enabled:
|
||||||
self.refreshGridOrder()
|
self.refreshGridOrder()
|
||||||
|
|
||||||
@@ -300,7 +304,12 @@ class SFGridStrategy:
|
|||||||
self.tradeTarget.enabled = False # type: ignore
|
self.tradeTarget.enabled = False # type: ignore
|
||||||
|
|
||||||
# 刷新网格订单(空仓只挂买单,有仓买卖对冲)
|
# 刷新网格订单(空仓只挂买单,有仓买卖对冲)
|
||||||
self.refreshGridOrder()
|
# 只有市场活跃时才下单,收盘后不再尝试下单
|
||||||
|
if qmtv.isMarketActive:
|
||||||
|
self.refreshGridOrder()
|
||||||
|
else:
|
||||||
|
PrintLog(LogLevel.INFO,
|
||||||
|
f' |- 市场已休市,跳过刷新网格订单')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# ── 停用交易: 取消所有未成交订单 ──
|
# ── 停用交易: 取消所有未成交订单 ──
|
||||||
@@ -465,7 +474,12 @@ class SFGridStrategy:
|
|||||||
PrintLog(LogLevel.INFO,
|
PrintLog(LogLevel.INFO,
|
||||||
f' 手续费 : {trade.commission:.3f}')
|
f' 手续费 : {trade.commission:.3f}')
|
||||||
# 4. 刷新网格订单:在新的 grid_index 位置重新挂买卖单
|
# 4. 刷新网格订单:在新的 grid_index 位置重新挂买卖单
|
||||||
self.refreshGridOrder()
|
# 只有市场活跃时才下单,收盘后不再尝试下单
|
||||||
|
if qmtv.isMarketActive:
|
||||||
|
self.refreshGridOrder()
|
||||||
|
else:
|
||||||
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- 成交后市场已休市,跳过刷新网格订单')
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
self.dataUpdateLock.release()
|
self.dataUpdateLock.release()
|
||||||
|
|||||||
@@ -1279,8 +1279,11 @@ class QmtApp:
|
|||||||
self._refresh_ui()
|
self._refresh_ui()
|
||||||
|
|
||||||
def _on_market_active(self, is_active: bool):
|
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._update_market_status(is_active)
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
PrintLog(LogLevel.INFO, f'[UI] 市场状态已更新,_market_label={self._market_label.value}')
|
||||||
|
|
||||||
def _update_market_status(self, active: bool):
|
def _update_market_status(self, active: bool):
|
||||||
"""更新顶栏市场状态指示灯"""
|
"""更新顶栏市场状态指示灯"""
|
||||||
|
|||||||
+49
@@ -4,6 +4,55 @@
|
|||||||
使用 --tk 参数切换到 Tkinter UI。
|
使用 --tk 参数切换到 Tkinter UI。
|
||||||
"""
|
"""
|
||||||
import sys
|
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 __name__ == '__main__':
|
||||||
if '--tk' in sys.argv:
|
if '--tk' in sys.argv:
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ a = Analysis(
|
|||||||
['starter.py'],
|
['starter.py'],
|
||||||
pathex=[],
|
pathex=[],
|
||||||
binaries=[],
|
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'],
|
hiddenimports=['brotli', 'brotli.encoding'],
|
||||||
hookspath=[],
|
hookspath=[],
|
||||||
hooksconfig={},
|
hooksconfig={},
|
||||||
|
|||||||
Reference in New Issue
Block a user