new ui
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
# SFGrid 网格交易策略流程图
|
||||
|
||||
## 1. 总览:策略生命周期
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["SFGridStrategy.__init__()"] --> B["订阅事件总线<br/>onOrderCreateAsync / onOrderTrade / onOrderError"]
|
||||
B --> C["获取涨跌停价<br/>todayUpStopPrice / todayDownStopPrice"]
|
||||
C --> D["loadExistOrders()<br/>从券商侧恢复未成交订单到 orderGrid"]
|
||||
D --> E["enabledTrading(enabled)"]
|
||||
E --> F{"enabled ?"}
|
||||
F -->|True| G["启用交易流程 → 见 §3"]
|
||||
F -->|False| H["停用交易流程 → 见 §3"]
|
||||
G --> I["saveProxy() 持久化"]
|
||||
H --> I
|
||||
I --> J["构造完成,进入事件循环<br/>等待 QMT 回调 / UI 操作"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心:refreshGridOrder() 网格下单
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START["refreshGridOrder()"] --> CHECK1{"qmtv.isMarketActive<br/>AND<br/>tradeTarget.enabled ?"}
|
||||
CHECK1 -->|No| SKIP["跳过不下单"]
|
||||
CHECK1 -->|Yes| QUERY["查询未成交订单<br/>queryPendingOrder()"]
|
||||
|
||||
QUERY --> STATUS{"tradeTarget.status ?"}
|
||||
|
||||
STATUS -->|"= 0 未建仓"| CHECK_INIT{"已存在建仓单?<br/>remark = 'INIT,1,{code}'"}
|
||||
CHECK_INIT -->|"No 没有"| PLACE_INIT["下建仓单 (STOCK_BUY)<br/>价格 = getPriceGrid()[0]<br/>remark = 'INIT,1,{code}'"]
|
||||
CHECK_INIT -->|"Yes 已有"| DONE_INIT["建仓单已在途,跳过"]
|
||||
|
||||
STATUS -->|"= 1 已建仓"| GET_IDX["currentIdx = grid_index"]
|
||||
GET_IDX --> SELL_CHECK{"currentIdx > 0 ?<br/>(grid_index 不是最低点)"}
|
||||
SELL_CHECK -->|"Yes 可挂卖单"| SELL_EXIST{"已存在同 remark 卖单?<br/>remark='SELL,{idx-1},{code}'"}
|
||||
SELL_EXIST -->|"No 没有"| SELL_PLACE["下卖出单 (STOCK_SELL)<br/>价格 = grid[sellIdx]<br/>sellIdx = currentIdx - 1"]
|
||||
SELL_EXIST -->|"Yes 已有"| SELL_SKIP["跳过,避免重复"]
|
||||
SELL_CHECK -->|"No 价格已最低"| SELL_SKIP2["无卖出空间"]
|
||||
|
||||
SELL_PLACE --> BUY_CHECK
|
||||
SELL_SKIP --> BUY_CHECK
|
||||
SELL_SKIP2 --> BUY_CHECK
|
||||
|
||||
BUY_CHECK{"currentIdx < len(grid)-1 ?<br/>(grid_index 不是最高点)"}
|
||||
BUY_CHECK -->|"Yes 可挂买单"| BUY_EXIST{"已存在同价同类型买单?<br/>order_type=BUY AND price=buyPrice"}
|
||||
BUY_EXIST -->|"No 没有"| BUY_PLACE["下买入单 (STOCK_BUY)<br/>价格 = grid[buyIdx]<br/>buyIdx = currentIdx + 1"]
|
||||
BUY_EXIST -->|"Yes 已有"| BUY_SKIP["跳过,避免重复"]
|
||||
BUY_CHECK -->|"No 价格已最高"| BUY_SKIP2["无买入空间"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 交易启停:enabledTrading()
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START["enabledTrading(enabled)"] --> SET["self.tradeTarget.enabled = enabled"]
|
||||
SET --> BRANCH{"enabled ?"}
|
||||
|
||||
BRANCH -->|"True 启用"| STATUS{"tradeTarget.status ?"}
|
||||
STATUS -->|"= 0 未建仓"| INIT_IDX{"grid_index == 0 ?"}
|
||||
INIT_IDX -->|"Yes"| SET1["grid_index = 1<br/>(默认建仓位置)"]
|
||||
INIT_IDX -->|"No"| KEEP["保留现有 grid_index"]
|
||||
SET1 --> REFRESH1["refreshGridOrder()"]
|
||||
KEEP --> REFRESH1
|
||||
|
||||
STATUS -->|"= 1 已建仓"| CALC["计算最小需求仓位<br/>min = grid_volume × grid_index"]
|
||||
CALC --> CHECK{"current_position >= min ?"}
|
||||
CHECK -->|"Yes 充足"| REFRESH2["refreshGridOrder()"]
|
||||
CHECK -->|"No 不足"| DENY["拒绝启用<br/>enabled = False<br/>(风控保护)"]
|
||||
|
||||
BRANCH -->|"False 停用"| CANCEL["取消所有未成交订单<br/>cancel_order_stock_async()"]
|
||||
CANCEL --> LOG["记录取消数量"]
|
||||
|
||||
REFRESH1 --> SAVE["saveProxy() 持久化"]
|
||||
REFRESH2 --> SAVE
|
||||
DENY --> SAVE
|
||||
LOG --> SAVE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 事件回调链
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph QMT["QMT / xtquant 层"]
|
||||
OA["orderAsync()<br/>返回 seq"]
|
||||
PUSH_ERR["C扩展推送<br/>XtOrderError"]
|
||||
PUSH_RESP["C扩展推送<br/>XtOrderResponse"]
|
||||
PUSH_TRADE["C扩展推送<br/>XtTrade"]
|
||||
end
|
||||
|
||||
subgraph BUS["事件总线 event_bus"]
|
||||
EVT_ERR["MarketOrderError"]
|
||||
EVT_RESP["MarketOrderCreated"]
|
||||
EVT_TRADE["MarketOrderTraded"]
|
||||
end
|
||||
|
||||
subgraph STG["SFGridStrategy 回调"]
|
||||
OE["onOrderError()"]
|
||||
OC["onOrderCreateAsync()"]
|
||||
OT["onOrderTrade()"]
|
||||
end
|
||||
|
||||
OA --> PUSH_RESP
|
||||
OA --> PUSH_ERR
|
||||
PUSH_ERR --> EVT_ERR --> OE
|
||||
PUSH_RESP --> EVT_RESP --> OC
|
||||
PUSH_TRADE --> EVT_TRADE --> OT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. onOrderError() 委托失败处理
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START["onOrderError(order_error)"] --> CHK1{"order_remark 非空 ?"}
|
||||
CHK1 -->|"No 空"| EXIT1["无法解析,忽略"]
|
||||
CHK1 -->|"Yes"| PARSE["解析 remark<br/>'{type},{gridIdx},{stockCode}'"]
|
||||
PARSE --> CHK2{"len(parts) >= 3 ?"}
|
||||
CHK2 -->|"No"| EXIT1
|
||||
CHK2 -->|"Yes"| CHK3{"strategy_name == 'SFGRID'<br/>AND<br/>stockCode 匹配本标的 ?"}
|
||||
CHK3 -->|"No 不匹配"| EXIT1
|
||||
CHK3 -->|"Yes"| LOCK["获取 dataUpdateLock"]
|
||||
LOCK --> DEL{"gridIdx in orderGrid ?"}
|
||||
DEL -->|"Yes"| REMOVE["del orderGrid[gridIdx]<br/>清理孤立条目"]
|
||||
DEL -->|"No"| LOG_ERR["记录错误日志<br/>error_id / error_msg"]
|
||||
REMOVE --> LOG_ERR
|
||||
LOG_ERR --> UNLOCK["释放 dataUpdateLock"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. onOrderCreateAsync() 订单确认
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START["onOrderCreateAsync(response)"] --> PARSE["解析 remark<br/>'{type},{gridIdx},{stockCode}'"]
|
||||
PARSE --> FILTER{"strategy_name == 'SFGRID'<br/>AND len(parts) >= 3<br/>AND stockCode 匹配 ?"}
|
||||
FILTER -->|"No"| EXIT["忽略"]
|
||||
FILTER -->|"Yes"| LOCK["获取 dataUpdateLock"]
|
||||
LOCK --> UPDATE["orderGrid[gridIdx] = response.order_id<br/>seq → order_id 替换"]
|
||||
UPDATE --> UNLOCK["释放 dataUpdateLock"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. onOrderTrade() 成交处理
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START["onOrderTrade(trade)"] --> PARSE["解析 remark<br/>'{type},{gridIdx},{stockCode}'"]
|
||||
PARSE --> FILTER{"strategy_name == 'SFGRID'<br/>AND len(parts) >= 3<br/>AND stockCode 匹配 ?"}
|
||||
FILTER -->|"No"| EXIT["忽略"]
|
||||
FILTER -->|"Yes"| LOCK["获取 dataUpdateLock"]
|
||||
|
||||
LOCK --> TYPE{"orderType ?"}
|
||||
|
||||
TYPE -->|"INIT 建仓单"| INIT["status = 1<br/>init_price = traded_price<br/>grid_index = 1"]
|
||||
TYPE -->|"BUY / SELL 网格单"| CMP{"gridIdx vs grid_index ?"}
|
||||
|
||||
CMP -->|"gridIdx > grid_index<br/>(买入成交)"| DOWN["grid_index += 1<br/>下移一格"]
|
||||
CMP -->|"gridIdx < grid_index<br/>(卖出成交)"| UP["grid_index -= 1<br/>上移一格<br/>match_count += 1<br/>total_profit += grid_size × volume"]
|
||||
CMP -->|"gridIdx == grid_index<br/>(异常)"| SAME["日志: 理论上不应该输出"]
|
||||
|
||||
INIT --> POST
|
||||
DOWN --> POST
|
||||
UP --> POST
|
||||
SAME --> POST
|
||||
|
||||
POST["成交后处理"] --> SAVE["saveProxy() 持久化状态"]
|
||||
SAVE --> DEL["del orderGrid[gridIdx]<br/>移除已成交订单"]
|
||||
DEL --> REPORT["打印成交报告<br/>成交价/量/手续费"]
|
||||
REPORT --> REFRESH["refreshGridOrder()<br/>在新位置挂新的网格单"]
|
||||
REFRESH --> UNLOCK["释放 dataUpdateLock"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 网格交易完整状态机
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> 未建仓: 创建 SFGridStrategy
|
||||
未建仓 --> 建仓中: enabledTrading(True)<br/>下建仓单 INIT
|
||||
|
||||
建仓中 --> 已建仓: onOrderTrade(INIT)<br/>建仓单成交
|
||||
建仓中 --> 建仓失败: onOrderError(INIT)<br/>委托被拒
|
||||
|
||||
建仓失败 --> 建仓中: refreshGridOrder()<br/>重新下建仓单
|
||||
|
||||
已建仓 --> 网格运行: refreshGridOrder()<br/>上下各挂一单
|
||||
|
||||
网格运行 --> 网格运行: onOrderTrade(SELL)<br/>卖出成交 → 上移<br/>重新挂单
|
||||
网格运行 --> 网格运行: onOrderTrade(BUY)<br/>买入成交 → 下移<br/>重新挂单
|
||||
网格运行 --> 单边挂单: onOrderError<br/>某方向委托失败
|
||||
|
||||
单边挂单 --> 网格运行: refreshGridOrder()<br/>重新补挂失败方向的单
|
||||
|
||||
已建仓 --> 已停用: enabledTrading(False)<br/>取消所有挂单
|
||||
网格运行 --> 已停用: enabledTrading(False)
|
||||
单边挂单 --> 已停用: enabledTrading(False)
|
||||
|
||||
已停用 --> 已建仓: enabledTrading(True)<br/>仓位检查通过
|
||||
已停用 --> 已停用: enabledTrading(True)<br/>仓位不足,回退
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 网格价格示意
|
||||
|
||||
```
|
||||
价格
|
||||
↑
|
||||
│ grid[5] = 12.00 ← 最贵(顶部)
|
||||
│ grid[4] = 11.50
|
||||
│ grid[3] = 11.00 ← 当前位置 grid_index=3
|
||||
│ grid[2] = 10.50 上方挂卖单 @10.50 (sellIdx=2, grid_index-1)
|
||||
│ grid[1] = 10.00 下方挂买单 @10.00 (buyIdx=1, 已成交位置)
|
||||
│ grid[0] = 9.50 ← 最便宜(底部/建仓价)
|
||||
│
|
||||
└──────────────────────→
|
||||
|
||||
grid_index=3 时:
|
||||
卖单挂在 grid[2] @10.50 → 价格跌到 10.50 卖出(上移一格,赚差价)
|
||||
买单挂在 grid[4] @11.50 → 价格涨到 11.50 买入(下移一格,补仓)
|
||||
|
||||
grid_size = grid[i] - grid[i-1] = 0.50(每格利润空间)
|
||||
```
|
||||
@@ -1,5 +0,0 @@
|
||||
[config]
|
||||
miniqmtpath = C:/Programs/GJQMT/userdata_mini
|
||||
account_no = 8882874667
|
||||
log_level = INFO
|
||||
|
||||
@@ -1,65 +1,25 @@
|
||||
import configparser
|
||||
from pathlib import Path
|
||||
"""
|
||||
运行时配置 — 端口、路径、账号由自动探测设置,无需配置文件。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
miniQMTPath = r'D:\\Programs\\DTQMT\\userdata_mini' # miniQMT软件的安装路径
|
||||
# miniQMTPath = ''
|
||||
account_no:str = '99082560'
|
||||
console_log = True
|
||||
log_level = "INFO"
|
||||
# ---- 自动探测的配置项(默认值仅占位,启动时自动修正) ----
|
||||
miniQMTPath: str = ''
|
||||
account_no: str = ''
|
||||
log_level: str = 'INFO'
|
||||
console_log: bool = True
|
||||
use_simulated_qmt: bool = False
|
||||
|
||||
def get_config_path() -> Path:
|
||||
"""获取配置文件的正确路径(兼容开发环境和打包后的可执行文件)"""
|
||||
|
||||
def app_dir() -> Path:
|
||||
"""应用根目录(兼容开发环境与打包后的 exe)"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
# 打包后的可执行文件环境
|
||||
# sys._MEIPASS是PyInstaller解压临时文件的目录
|
||||
# 配置文件应该放在可执行文件同目录下
|
||||
base_path = Path(sys.executable).parent
|
||||
else:
|
||||
# 开发环境
|
||||
base_path = Path(__file__).resolve().parent
|
||||
return Path(sys.executable).parent
|
||||
return Path(__file__).resolve().parent
|
||||
|
||||
return base_path / 'config.ini'
|
||||
|
||||
def save_config(miniQmtPath:str, account_no:str, use_simulated_qmt: bool = False):
|
||||
"""创建默认配置文件"""
|
||||
config = configparser.ConfigParser()
|
||||
config['config'] = {
|
||||
'miniQMTPath': miniQmtPath,
|
||||
'account_no': account_no,
|
||||
'use_simulated_qmt': str(use_simulated_qmt),
|
||||
'log_level': 'INFO'
|
||||
}
|
||||
config_path = get_config_path()
|
||||
with open(config_path, 'w') as configfile:
|
||||
config.write(configfile)
|
||||
print(f'已创建默认配置文件: {config_path}')
|
||||
|
||||
def exist_config() -> bool:
|
||||
"""检查配置文件是否存在"""
|
||||
config_path = get_config_path()
|
||||
return config_path.exists()
|
||||
|
||||
def initConfig() -> bool:
|
||||
global miniQMTPath, account_no, log_level, use_simulated_qmt
|
||||
|
||||
# 获取配置文件路径
|
||||
config_path = get_config_path()
|
||||
|
||||
config = configparser.ConfigParser()
|
||||
config.read(config_path, encoding='utf-8')
|
||||
miniQMTPath = config.get('config','miniQMTPath')
|
||||
account_no = config.get('config','account_no')
|
||||
log_level = config.get('config','log_level')
|
||||
try:
|
||||
use_simulated_qmt = config.get('config','use_simulated_qmt').lower() in ('true', '1', 'yes')
|
||||
except (configparser.NoOptionError, configparser.NoSectionError):
|
||||
use_simulated_qmt = False
|
||||
|
||||
# 判断miniQMTPath是否为空,并且目录是否存在
|
||||
if not miniQMTPath or not Path(miniQMTPath).exists():
|
||||
print('请先配置miniQMTPath')
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
def log_file_path() -> Path:
|
||||
"""日志文件路径"""
|
||||
return app_dir() / 'sfgrid.log'
|
||||
|
||||
@@ -2,5 +2,4 @@ import xtquant.xtconstant as xtconstant
|
||||
|
||||
OrderTypeBuy = f'{xtconstant.STOCK_BUY}' # 买
|
||||
OrderTypeSell = f'{xtconstant.STOCK_SELL}' # 卖
|
||||
OrderTypeInit = "0" # 建仓
|
||||
OrderTypeNone = "None"
|
||||
+26
-3
@@ -1,4 +1,6 @@
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
import threading
|
||||
|
||||
from core.eventbus import EventPrintLog, event_bus
|
||||
import config
|
||||
@@ -14,13 +16,34 @@ class LogLevel(Enum):
|
||||
def __le__(self, other):
|
||||
return self.value <= other.value
|
||||
|
||||
|
||||
class LogData:
|
||||
def __init__(self, level:LogLevel, message:str):
|
||||
def __init__(self, level: LogLevel, message: str):
|
||||
self.level = level
|
||||
self.message = message
|
||||
|
||||
def PrintLog(level:LogLevel, message:str):
|
||||
|
||||
_log_lock = threading.Lock()
|
||||
|
||||
|
||||
def _log_file_path():
|
||||
"""日志文件路径"""
|
||||
return str(config.log_file_path())
|
||||
|
||||
|
||||
def PrintLog(level: LogLevel, message: str):
|
||||
data = LogData(level, message)
|
||||
event_bus.publish(EventPrintLog, data)
|
||||
|
||||
line = f'{datetime.now().strftime("%Y-%m-%d %H:%M:%S")} [{level.name}] {message}'
|
||||
|
||||
if config.console_log:
|
||||
print(f'{level.name} {message}')
|
||||
print(line)
|
||||
|
||||
# 写入日志文件
|
||||
try:
|
||||
with _log_lock:
|
||||
with open(_log_file_path(), 'a', encoding='utf-8') as f:
|
||||
f.write(line + '\n')
|
||||
except Exception:
|
||||
pass # 写文件失败不阻塞主流程
|
||||
|
||||
+6
-2
@@ -9,17 +9,21 @@ import config as _config
|
||||
def _get_qmt():
|
||||
"""获取 QMT 模块(配置优先于平台检测)"""
|
||||
if _config.use_simulated_qmt:
|
||||
print('[qmt] 配置指定模拟模式 → qmt_dummy')
|
||||
from core.qmt_dummy import qmtv
|
||||
return qmtv
|
||||
|
||||
if sys.platform == 'win32':
|
||||
try:
|
||||
print('[qmt] Windows 平台,尝试加载 qmt_real...')
|
||||
from core.qmt_real import qmtv as real_qmtv
|
||||
print('[qmt] qmt_real 加载成功')
|
||||
return real_qmtv
|
||||
except ImportError:
|
||||
pass
|
||||
except ImportError as e:
|
||||
print(f'[qmt] qmt_real 加载失败: {e},回退 qmt_dummy')
|
||||
|
||||
# 非 Windows 或导入失败,使用模拟器
|
||||
print('[qmt] 使用模拟模式 qmt_dummy')
|
||||
from core.qmt_dummy import qmtv
|
||||
return qmtv
|
||||
|
||||
|
||||
@@ -121,6 +121,14 @@ class DummyQmtV:
|
||||
return type('DummyPos', (), pos)()
|
||||
return None
|
||||
|
||||
def queryTodayOrders(self) -> list:
|
||||
"""查询当日所有委托 (模拟)"""
|
||||
return list(self._pending_orders)
|
||||
|
||||
def queryTodayTrades(self) -> list:
|
||||
"""查询当日所有成交 (模拟)"""
|
||||
return [] # 模拟模式无实际成交记录
|
||||
|
||||
def queryPendingOrder(self, stock_code: str, tag: str) -> list:
|
||||
"""查询挂单"""
|
||||
return [o for o in self._pending_orders
|
||||
|
||||
+260
-28
@@ -2,6 +2,8 @@
|
||||
QMT 真实交易实现 - 封装 xtquant SDK
|
||||
"""
|
||||
import datetime
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
import config
|
||||
@@ -15,6 +17,185 @@ class RealQmtV:
|
||||
封装 xtquant 的 XtQuantTrader,提供与模拟器一致的接口
|
||||
"""
|
||||
|
||||
# miniQMT 进程名关键字(GUI 壳: XtMiniQmt.exe,交易引擎: miniquote.exe)
|
||||
_QMT_PROCESS_KEYWORDS = ['Qmt', 'qmt', 'QMT', 'miniquote', 'MiniQuote']
|
||||
|
||||
@staticmethod
|
||||
def _discover_qmt_port() -> int:
|
||||
"""
|
||||
自动探测 miniQMT 监听端口。
|
||||
方法1: SDK 内部扫描 (读取配置)
|
||||
方法2: netstat 找 LISTENING 端口 → 反查所属进程名 → 匹配 QMT 关键字
|
||||
返回端口号,未找到返回 0。
|
||||
"""
|
||||
# ---- 方法1: SDK 内部扫描 ----
|
||||
try:
|
||||
from xtquant import xtconn
|
||||
addrs = xtconn.scan_available_server_addr()
|
||||
for addr in addrs:
|
||||
try:
|
||||
port = int(addr.split(':')[1])
|
||||
if port:
|
||||
PrintLog(LogLevel.INFO, f'[端口探测] SDK 扫描发现端口: {port}')
|
||||
return port
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.DEBUG, f'[端口探测] SDK 扫描异常: {e}')
|
||||
|
||||
# ---- 方法2: netstat → 反向查进程名 ----
|
||||
try:
|
||||
# 2a. netstat 找出所有 LISTENING 端口的 PID
|
||||
pid_ports = {} # pid -> [port, ...]
|
||||
netstat = subprocess.run(
|
||||
['netstat', '-ano'],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
for line in netstat.stdout.splitlines():
|
||||
if 'LISTENING' not in line and 'LISTEN' not in line:
|
||||
continue
|
||||
parts = line.split()
|
||||
if len(parts) < 5:
|
||||
continue
|
||||
try:
|
||||
local_addr = parts[1]
|
||||
port = int(local_addr.rsplit(':', 1)[-1])
|
||||
pid = int(parts[-1])
|
||||
if port > 0:
|
||||
pid_ports.setdefault(pid, []).append(port)
|
||||
except (ValueError, IndexError):
|
||||
continue
|
||||
|
||||
if not pid_ports:
|
||||
PrintLog(LogLevel.DEBUG, '[端口探测] netstat 未找到任何 LISTENING 端口')
|
||||
return 0
|
||||
|
||||
# 2b. 对每个有监听端口的 PID,查进程名是否匹配 QMT
|
||||
for pid, ports in pid_ports.items():
|
||||
name = RealQmtV._get_process_name(pid)
|
||||
if name and any(kw in name for kw in RealQmtV._QMT_PROCESS_KEYWORDS):
|
||||
port = ports[0]
|
||||
PrintLog(LogLevel.INFO, f'[端口探测] 发现 QMT 进程: {name} (PID={pid}), 端口: {port}')
|
||||
|
||||
# 同时探测 userdata_mini 路径
|
||||
exe_path = RealQmtV._get_process_exe_path(pid)
|
||||
if exe_path:
|
||||
PrintLog(LogLevel.INFO, f'[路径探测] 进程路径: {exe_path}')
|
||||
found_path = RealQmtV._find_userdata_mini(exe_path)
|
||||
if found_path:
|
||||
PrintLog(LogLevel.INFO, f'[路径探测] 发现 userdata_mini: {found_path}')
|
||||
if found_path != config.miniQMTPath:
|
||||
PrintLog(LogLevel.INFO, f'[路径探测] 自动修正 miniQMTPath: {config.miniQMTPath} -> {found_path}')
|
||||
config.miniQMTPath = found_path
|
||||
|
||||
# 同时从窗口标题提取资金账号
|
||||
account = RealQmtV._discover_account()
|
||||
if account:
|
||||
if account != config.account_no:
|
||||
PrintLog(LogLevel.INFO, f'[账号探测] 自动修正 account_no: {config.account_no[-4:]}**** -> {account[-4:]}****')
|
||||
config.account_no = account
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f'[账号探测] 确认账号: {account[-4:]}****')
|
||||
|
||||
return port
|
||||
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.INFO, f'[端口探测] 进程扫描异常: {e}')
|
||||
|
||||
PrintLog(LogLevel.WARNING, '[端口探测] 未能自动发现 miniQMT 端口')
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def _get_process_name(pid: int) -> str:
|
||||
"""通过 PID 获取进程名(单个查询,不用扫全量 tasklist)"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['tasklist', '/fi', f'PID eq {pid}', '/fo', 'csv', '/nh'],
|
||||
capture_output=True, text=True, timeout=5
|
||||
)
|
||||
for line in result.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line or line.startswith('INFO:'):
|
||||
continue
|
||||
parts = [p.strip('"').strip() for p in line.split('","')]
|
||||
if len(parts) >= 2:
|
||||
return parts[0]
|
||||
except Exception:
|
||||
pass
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def _get_process_exe_path(pid: int) -> str:
|
||||
"""通过 PID 获取进程的可执行文件完整路径"""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['powershell', '-NoProfile', '-Command',
|
||||
f'(Get-Process -Id {pid}).Path'],
|
||||
capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=5
|
||||
)
|
||||
path = result.stdout.strip()
|
||||
if path and os.path.isfile(path):
|
||||
return path
|
||||
except Exception:
|
||||
pass
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def _find_userdata_mini(exe_path: str) -> str:
|
||||
"""从 QMT 可执行文件路径向上查找 userdata_mini 目录"""
|
||||
exe_dir = os.path.dirname(exe_path)
|
||||
# 从 exe 所在目录开始,向上最多 3 层
|
||||
for _ in range(4):
|
||||
candidate = os.path.join(exe_dir, 'userdata_mini')
|
||||
if os.path.isdir(candidate):
|
||||
return candidate
|
||||
parent = os.path.dirname(exe_dir)
|
||||
if parent == exe_dir:
|
||||
break
|
||||
exe_dir = parent
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def _discover_account() -> str:
|
||||
"""
|
||||
从 XtMiniQmt.exe 的窗口标题中提取资金账号。
|
||||
标题格式: "8882874667 - 国金证券QMT交易端 2.0.8.300"
|
||||
返回账号字符串,失败返回空字符串。
|
||||
"""
|
||||
try:
|
||||
# 找到 XtMiniQmt.exe 的 PID
|
||||
tasklist = subprocess.run(
|
||||
['tasklist', '/fo', 'csv', '/nh'],
|
||||
capture_output=True, text=True, timeout=10
|
||||
)
|
||||
gui_pid = 0
|
||||
for line in tasklist.stdout.splitlines():
|
||||
line = line.strip()
|
||||
if not line:
|
||||
continue
|
||||
parts = [p.strip('"').strip() for p in line.split('","')]
|
||||
if len(parts) >= 2 and 'XtMiniQmt' in parts[0]:
|
||||
gui_pid = int(parts[1])
|
||||
break
|
||||
|
||||
if not gui_pid:
|
||||
return ''
|
||||
|
||||
# 获取窗口标题(PowerShell 输出可能含中文,用 utf-8)
|
||||
result = subprocess.run(
|
||||
['powershell', '-NoProfile', '-Command',
|
||||
f'(Get-Process -Id {gui_pid}).MainWindowTitle'],
|
||||
capture_output=True, text=True, encoding='utf-8', errors='replace', timeout=5
|
||||
)
|
||||
title = result.stdout.strip()
|
||||
if title and ' - ' in title:
|
||||
account = title.split(' - ')[0].strip()
|
||||
if account.isdigit():
|
||||
return account
|
||||
except Exception:
|
||||
pass
|
||||
return ''
|
||||
|
||||
@staticmethod
|
||||
def _to_plain_code(stock_code: str) -> str:
|
||||
"""将 xtquant 格式 '600519.SH' 转换为数据库格式 '600519'"""
|
||||
@@ -33,15 +214,6 @@ class RealQmtV:
|
||||
# fallback: try both, prefer SH
|
||||
return f'{code}.SH'
|
||||
|
||||
@staticmethod
|
||||
def _strip_code_suffixes(datas: dict) -> dict:
|
||||
"""批量去除 xtquant 数据中的代码后缀"""
|
||||
result = {}
|
||||
for code, tick in datas.items():
|
||||
result[code] = tick
|
||||
if '.' in code:
|
||||
result[code.split('.')[0]] = tick
|
||||
return result
|
||||
def __init__(self) -> None:
|
||||
self.inited = False
|
||||
self.connected = False
|
||||
@@ -66,9 +238,11 @@ class RealQmtV:
|
||||
|
||||
self.mini_qmt_path = config.miniQMTPath
|
||||
self.account = StockAccount(config.account_no, 'STOCK')
|
||||
PrintLog(LogLevel.INFO, f'[QMT] 初始化: path={self.mini_qmt_path}, account={config.account_no[-4:]}****')
|
||||
|
||||
# 创建 XtQuantTrader 实例
|
||||
session_id = int(time.time()) % 10000
|
||||
PrintLog(LogLevel.INFO, f'[QMT] 创建 XtQuantTrader, session={session_id}')
|
||||
self.xt_trader = XtQuantTrader(self.mini_qmt_path, session_id)
|
||||
|
||||
# 注册回调 — xtquant 只接受一个回调对象,会在上面调用 on_xxx 方法
|
||||
@@ -81,30 +255,62 @@ class RealQmtV:
|
||||
PrintLog(LogLevel.ERROR, f'- [失败] QMT 初始化: {e}')
|
||||
|
||||
def connect(self) -> bool:
|
||||
"""连接 MiniQMT"""
|
||||
"""连接 MiniQMT,失败自动探测端口并重试"""
|
||||
if not self.inited:
|
||||
PrintLog(LogLevel.ERROR, '- [失败] QMT 未初始化')
|
||||
PrintLog(LogLevel.ERROR, '[QMT] 连接失败: 未初始化')
|
||||
return False
|
||||
|
||||
try:
|
||||
# 启动 trader 线程
|
||||
_connect_errors = {
|
||||
0: '成功',
|
||||
-1: '一般错误(miniQMT 可能未启动)',
|
||||
-2: 'miniQMT 未运行(请先启动极简QMT)',
|
||||
-3: '连接超时',
|
||||
}
|
||||
|
||||
def _do_connect() -> int:
|
||||
self.xt_trader.start()
|
||||
# 建立连接
|
||||
connect_result = self.xt_trader.connect()
|
||||
PrintLog(LogLevel.INFO, '[QMT] xt_trader.start() 完成')
|
||||
PrintLog(LogLevel.INFO, '[QMT] 正在连接 miniQMT...')
|
||||
return self.xt_trader.connect()
|
||||
|
||||
try:
|
||||
# 尝试默认连接
|
||||
PrintLog(LogLevel.INFO, '[QMT] 尝试默认方式连接...')
|
||||
connect_result = _do_connect()
|
||||
|
||||
# 失败则自动探测端口并重试
|
||||
if connect_result != 0:
|
||||
PrintLog(LogLevel.INFO, '[QMT] 默认连接失败,启动端口自动探测...')
|
||||
discovered_port = self._discover_qmt_port()
|
||||
if discovered_port > 0:
|
||||
PrintLog(LogLevel.INFO, f'[QMT] 探测到端口 {discovered_port},尝试连接...')
|
||||
try:
|
||||
from xtquant import xtdata
|
||||
xtdata.connect(ip='127.0.0.1', port=discovered_port)
|
||||
PrintLog(LogLevel.INFO, f'[QMT] xtdata 连接成功 (端口: {discovered_port})')
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f'[QMT] xtdata 连接失败 (端口: {discovered_port}): {e}')
|
||||
return False
|
||||
connect_result = _do_connect()
|
||||
else:
|
||||
PrintLog(LogLevel.WARNING, '[QMT] 端口自动探测未找到 miniQMT 进程')
|
||||
|
||||
result_desc = _connect_errors.get(connect_result, f'未知({connect_result})')
|
||||
PrintLog(LogLevel.INFO, f'[QMT] connect() 返回: {connect_result} ({result_desc})')
|
||||
|
||||
if connect_result == 0:
|
||||
# 订阅账户 (传入 StockAccount 对象而不是 account_id 字符串)
|
||||
PrintLog(LogLevel.INFO, f'[QMT] 订阅账户...')
|
||||
self.xt_trader.subscribe(self.account)
|
||||
# 等待回调
|
||||
time.sleep(1)
|
||||
PrintLog(LogLevel.INFO, '[QMT] 订阅完成')
|
||||
self.connected = True
|
||||
self.startMarketDataSubscription()
|
||||
PrintLog(LogLevel.INFO, f'- [成功] 真实交易连接成功 (账号: {config.account_no})')
|
||||
PrintLog(LogLevel.INFO, f'[QMT] 连接成功 (账号: {config.account_no[-4:]}****)')
|
||||
return True
|
||||
else:
|
||||
PrintLog(LogLevel.ERROR, f'- [失败] 连接失败, 返回码: {connect_result}')
|
||||
PrintLog(LogLevel.ERROR, f'[QMT] 连接失败: {result_desc}')
|
||||
return False
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f'- [失败] 连接异常: {e}')
|
||||
PrintLog(LogLevel.ERROR, f'[QMT] 连接异常: {e}')
|
||||
return False
|
||||
|
||||
def getAllPositions(self) -> dict:
|
||||
@@ -144,18 +350,41 @@ class RealQmtV:
|
||||
return None
|
||||
|
||||
def queryPendingOrder(self, stock_code: str, tag: str) -> list:
|
||||
"""查询挂单"""
|
||||
"""查询挂单(过滤已撤/废单)"""
|
||||
if not self.connected:
|
||||
return []
|
||||
try:
|
||||
orders = self.xt_trader.query_stock_orders(self.account)
|
||||
# 过滤已撤(54)和废单(57),避免策略误判"已有挂单"跳过下单
|
||||
_CANCELED = {54, 57}
|
||||
return [o for o in orders
|
||||
if self._to_plain_code(getattr(o, 'stock_code', '')) == stock_code and
|
||||
(tag is None or getattr(o, 'strategy_name', None) == tag)]
|
||||
(tag is None or getattr(o, 'strategy_name', None) == tag) and
|
||||
getattr(o, 'order_status', 0) not in _CANCELED]
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f'- [查询挂单失败] {e}')
|
||||
return []
|
||||
|
||||
def queryTodayOrders(self) -> list:
|
||||
"""查询当日所有委托"""
|
||||
if not self.connected:
|
||||
return []
|
||||
try:
|
||||
return list(self.xt_trader.query_stock_orders(self.account))
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f'- [查询委托失败] {e}')
|
||||
return []
|
||||
|
||||
def queryTodayTrades(self) -> list:
|
||||
"""查询当日所有成交"""
|
||||
if not self.connected:
|
||||
return []
|
||||
try:
|
||||
return list(self.xt_trader.query_stock_trades(self.account))
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.ERROR, f'- [查询成交失败] {e}')
|
||||
return []
|
||||
|
||||
def orderAsync(self, stock_code, orderVolume, orderType, orderPrice, priceType, orderRemark, strategy_name):
|
||||
"""异步下单"""
|
||||
if not self.connected:
|
||||
@@ -328,7 +557,8 @@ class RealQmtV:
|
||||
|
||||
# xtquant 返回 "600519.SH" 格式 key,UI 使用纯代码 "600519"
|
||||
# 构建同时包含两种 key 的数据确保匹配
|
||||
eBus.event_bus.publish(eBus.MarketDataUpdate, self._strip_code_suffixes(datas))
|
||||
# 直接发布 xtquant 原始数据(代码带 .SH/.SZ 后缀)
|
||||
eBus.event_bus.publish(eBus.MarketDataUpdate, datas)
|
||||
|
||||
def _market_data_watchdog(self):
|
||||
"""行情活跃监控 — 超过 30 秒无数据则标记市场不活跃"""
|
||||
@@ -349,10 +579,10 @@ class RealQmtV:
|
||||
# ---- xtquant 回调处理 (xtquant 通过回调对象调用 on_xxx 方法) ----
|
||||
|
||||
def on_connected(self):
|
||||
print(datetime.datetime.now(), '真实 QMT 连接成功')
|
||||
PrintLog(LogLevel.INFO, f'[QMT] on_connected: 真实 QMT 连接成功 {datetime.datetime.now()}')
|
||||
|
||||
def on_disconnected(self):
|
||||
print(datetime.datetime.now(), '真实 QMT 连接断开')
|
||||
PrintLog(LogLevel.WARNING, f'[QMT] on_disconnected: 真实 QMT 连接断开 {datetime.datetime.now()}')
|
||||
|
||||
def on_stock_order(self, order):
|
||||
self._pending_orders.append(order)
|
||||
@@ -364,11 +594,13 @@ class RealQmtV:
|
||||
eBus.event_bus.publish(eBus.MarketOrderCreated, response)
|
||||
|
||||
def on_order_error(self, order_error):
|
||||
print(f"\n真实委托报错回调: order_id={order_error.order_id}, error_id={order_error.error_id}, error_msg={order_error.error_msg}, remark={order_error.order_remark}")
|
||||
PrintLog(LogLevel.ERROR,
|
||||
f'[QMT] 委托报错: order_id={order_error.order_id}, error_id={order_error.error_id}, '
|
||||
f'error_msg={order_error.error_msg}, remark={order_error.order_remark}')
|
||||
eBus.event_bus.publish(eBus.MarketOrderError, order_error)
|
||||
|
||||
def on_account_status(self, status):
|
||||
print(datetime.datetime.now(), status)
|
||||
PrintLog(LogLevel.INFO, f'[QMT] on_account_status: {datetime.datetime.now()} {status}')
|
||||
|
||||
|
||||
qmtv = RealQmtV()
|
||||
|
||||
@@ -16,7 +16,7 @@ class SFGridTradeTarget(BaseModel):
|
||||
init_price = FloatField(null=True) # 建仓成本
|
||||
grid_match_count = IntegerField(default=0)
|
||||
grid_total_profit = FloatField(default=0.0)
|
||||
status = IntegerField(default=0) # -1表示新标的,未完成交易配置,0表示新标的,已完成交易配置,1表示已建初始仓,正常交易中
|
||||
status = IntegerField(default=0) # 已废弃,改用 strategy_type + grid_index
|
||||
enabled = BooleanField(default=False) # 是否启动交易线程
|
||||
strategy_type = IntegerField(default=0) # 0=未分类, 1=网格策略
|
||||
|
||||
|
||||
+49
-108
@@ -25,7 +25,7 @@ from core.sfgrid import bus_events
|
||||
from core.sfgrid.bus_events import EventTradeTargetUpdate
|
||||
import core.sfgrid.model as model
|
||||
from core.eventbus import event_bus
|
||||
from core.constants import OrderTypeBuy, OrderTypeSell, OrderTypeInit
|
||||
from core.constants import OrderTypeBuy, OrderTypeSell
|
||||
|
||||
from xtquant import xtconstant
|
||||
from xtquant.xttype import XtOrderError, XtOrderResponse, XtTrade
|
||||
@@ -65,8 +65,7 @@ class SFGridStrategy:
|
||||
|
||||
PrintLog(LogLevel.INFO,
|
||||
f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造开始: '
|
||||
f'grid_index={tradeTarget.grid_index}, status={tradeTarget.status}, '
|
||||
f'enabled={tradeTarget.enabled}')
|
||||
f'网格={tradeTarget.grid_index}, 启用={tradeTarget.enabled}')
|
||||
|
||||
# orderGrid: 网格索引 → 订单编号(seq 或 order_id)的映射
|
||||
# seq 是 xtquant 返回的下单序号(下单瞬间),order_id 是交易所返回的正式订单号(异步回调后更新)
|
||||
@@ -149,30 +148,10 @@ class SFGridStrategy:
|
||||
# 获取当前该标的所有未成交订单
|
||||
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
||||
|
||||
# ── 分支1: status=0 未建仓 → 下建仓单 ──
|
||||
# 条件: 标的尚未建仓 且 不存在正在执行中的建仓单(防止重复建仓)
|
||||
init_remark = self._make_remark(OrderTypeInit, 1)
|
||||
if self.tradeTarget.status == 0 and not any(
|
||||
o.order_remark == init_remark for o in orders
|
||||
):
|
||||
# 建仓价取价格网格中最高价(grid_index=0 即列表第一个元素)
|
||||
price = self.tradeTarget.getPriceGrid()[0]
|
||||
tmpOrderSeq = qmtv.orderAsync(
|
||||
str(self.tradeTarget.stock_code),
|
||||
self.tradeTarget.grid_volume,
|
||||
xtconstant.STOCK_BUY, # 建仓 = 买入
|
||||
price,
|
||||
xtconstant.FIX_PRICE, # 限价单
|
||||
init_remark,
|
||||
self.getName(),
|
||||
)
|
||||
self.orderGrid[1] = tmpOrderSeq # 建仓单固定在网格索引 1
|
||||
PrintLog(LogLevel.INFO,
|
||||
f'|- 标的[{self.tradeTarget.targetName()}] 初始化: '
|
||||
f'建仓单,建仓价: {price:.3f}')
|
||||
|
||||
# ── 分支2: status=1 已建仓 → 下网格买卖单 ──
|
||||
elif self.tradeTarget.status == 1:
|
||||
# ── 统一网格逻辑 ──
|
||||
# grid_index=0 空仓: 只挂买单 @ grid[1],无持仓可卖
|
||||
# grid_index>0 有仓: 上方挂卖单 @ grid[idx-1],下方挂买单 @ grid[idx+1]
|
||||
if self.tradeTarget.grid_index >= 0:
|
||||
currentIdx = self.tradeTarget.grid_index # type: ignore
|
||||
|
||||
# --- 上方挂卖出单(空单)---
|
||||
@@ -272,19 +251,16 @@ class SFGridStrategy:
|
||||
启用或停用该标的的网格交易
|
||||
|
||||
启用时 (enabled=True):
|
||||
- status=0: 初始化网格索引后调用 refreshGridOrder 下建仓单
|
||||
- status=1: 检查持仓是否满足当前网格位置要求,满足则刷新网格订单
|
||||
不满足则回退 enabled=False(风控保护)
|
||||
- grid_index=0 空仓: 直接调用 refreshGridOrder(只挂买单)
|
||||
- grid_index>0 有仓: 检查持仓是否满足 grid_volume × grid_index
|
||||
满足则刷新网格单,不满足则回退 enabled=False(风控保护)
|
||||
|
||||
停用时 (enabled=False):
|
||||
- 取消该标的所有未成交订单,停止交易监控
|
||||
|
||||
返回:
|
||||
更新后的 tradeTarget 对象
|
||||
"""
|
||||
PrintLog(LogLevel.INFO,
|
||||
f" |- [DEBUG] enabledTrading({enabled}) 调用前: "
|
||||
f"grid_index={self.tradeTarget.grid_index}, status={self.tradeTarget.status}")
|
||||
f"grid_index={self.tradeTarget.grid_index}")
|
||||
|
||||
self.tradeTarget.enabled = enabled # type: ignore
|
||||
|
||||
@@ -294,38 +270,23 @@ class SFGridStrategy:
|
||||
f" |- 标的{self.tradeTarget.targetName()}交易启动, "
|
||||
f"持仓量:{self.tradeTarget.current_position}")
|
||||
|
||||
if self.tradeTarget.status == 0:
|
||||
# 未建仓状态: 初始化网格索引
|
||||
if self.tradeTarget.grid_index == 0:
|
||||
# grid_index=0 表示从未初始化过,设为 1(价格网格最高点建仓)
|
||||
self.tradeTarget.grid_index = 1 # pyright: ignore[reportAttributeAccessIssue]
|
||||
# 空仓: refreshGridOrder 会在 grid[1] 挂第一笔买单
|
||||
PrintLog(LogLevel.INFO,
|
||||
f" |- 标的{self.tradeTarget.targetName()}初始状态, "
|
||||
f"设置网格序号 1,")
|
||||
f" |- 标的{self.tradeTarget.targetName()}空仓, "
|
||||
f"等待首次买入建仓")
|
||||
else:
|
||||
# grid_index 非零,保留之前设置的值(可能是手动修改的)
|
||||
PrintLog(LogLevel.INFO,
|
||||
f" |- 标的{self.tradeTarget.targetName()}初始状态, "
|
||||
f"保留网格序号 {self.tradeTarget.grid_index},")
|
||||
else:
|
||||
# 已建仓状态: 检查现有持仓是否满足当前网格位置的仓位需求
|
||||
# 有仓: 检查现有持仓是否满足当前网格位置的仓位需求
|
||||
# 最小需求仓位 = 每格股数 × 当前网格索引
|
||||
# 例: grid_volume=100, grid_index=3 → 需持股 300 股
|
||||
PrintLog(LogLevel.INFO,
|
||||
f" |- 标的{self.tradeTarget.targetName()}已有仓位或非初始状态 "
|
||||
f"无需建初始仓 当前仓位: {self.tradeTarget.current_position} "
|
||||
f"状态: {self.tradeTarget.status}")
|
||||
|
||||
minRequirePosition: int = self.tradeTarget.grid_volume * int(self.tradeTarget.grid_index) # type: ignore
|
||||
|
||||
if minRequirePosition <= int(self.tradeTarget.current_position): # type: ignore
|
||||
# 持仓充足,可以继续网格交易
|
||||
PrintLog(LogLevel.INFO,
|
||||
f' |- 仓位检查: 持仓需求充足, '
|
||||
f'(gridVolume*gridIndex)={minRequirePosition}, '
|
||||
f'当前持仓:{self.tradeTarget.current_position}')
|
||||
else:
|
||||
# 持仓不足(可能是之前部分成交或手动减仓),风控:拒绝启用
|
||||
PrintLog(LogLevel.INFO,
|
||||
f' |- 仓位检查: 持仓需求不足, '
|
||||
f'(gridVolume*gridIndex)={minRequirePosition}, '
|
||||
@@ -333,7 +294,7 @@ class SFGridStrategy:
|
||||
f'交易启动失败')
|
||||
self.tradeTarget.enabled = False # type: ignore
|
||||
|
||||
# 无论 status=0 还是 status=1,最终都调用 refreshGridOrder 下对应的单
|
||||
# 刷新网格订单(空仓只挂买单,有仓买卖对冲)
|
||||
self.refreshGridOrder()
|
||||
|
||||
else:
|
||||
@@ -429,20 +390,17 @@ class SFGridStrategy:
|
||||
"""
|
||||
QMT 委托成交通知回调
|
||||
|
||||
成交后:
|
||||
1. 更新网格索引(卖出上移 / 买入下移)
|
||||
2. 如果是建仓单成交: status 0→1, 记录建仓价
|
||||
3. 如果是网格单成交: 累计网格匹配次数和总利润
|
||||
4. 从 orderGrid 删除已成交订单
|
||||
5. 持久化状态到数据库
|
||||
6. 调用 refreshGridOrder 挂新的网格单
|
||||
|
||||
trade.order_remark 格式: "{type},{gridIdx},{stockCode}"
|
||||
收到成交后:
|
||||
1. 判断成交方向(买入下移 / 卖出上移)→ 更新 grid_index
|
||||
2. 首次建仓(grid_index==0 时成交)→ 记录 init_price
|
||||
3. 卖出成交 → 累计 grid_match_count 和 grid_total_profit
|
||||
4. 清理 orderGrid → 持久化 → 刷新网格挂单
|
||||
"""
|
||||
# ── 过滤:只处理本策略本标的的成交 ──
|
||||
parsed = self._filter_event(trade.order_remark, trade.strategy_name)
|
||||
if parsed is None:
|
||||
return
|
||||
orderType, gridIdx, _ = parsed
|
||||
_, gridIdx, _ = parsed # gridIdx: 成交订单对应的网格索引(int)
|
||||
|
||||
PrintLog(LogLevel.INFO,
|
||||
f'|- 委托成交通知'
|
||||
@@ -451,61 +409,45 @@ class SFGridStrategy:
|
||||
|
||||
self.dataUpdateLock.acquire()
|
||||
try:
|
||||
desc: str = "" # 用于日志展示的成交类型描述
|
||||
|
||||
# ── 分支1: 建仓单成交 ──
|
||||
if orderType == OrderTypeInit:
|
||||
PrintLog(LogLevel.INFO,
|
||||
f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] '
|
||||
f'- 建仓单成交')
|
||||
# 状态切换: 未建仓(0) → 已建仓(1)
|
||||
self.tradeTarget.status = 1 # type: ignore
|
||||
# 记录建仓价格
|
||||
# ── 首次建仓:记录建仓价 ──
|
||||
# grid_index==0 表示成交前处于空仓状态,这笔成交就是首次建仓
|
||||
if self.tradeTarget.grid_index == 0:
|
||||
self.tradeTarget.init_price = trade.traded_price # type: ignore
|
||||
PrintLog(LogLevel.INFO,
|
||||
f'|- [DEBUG] 建仓单成交: '
|
||||
f'grid_index {self.tradeTarget.grid_index} → 1')
|
||||
# 建仓后网格索引固定为 1(价格网格最高点)
|
||||
self.tradeTarget.grid_index = 1 # type: ignore
|
||||
desc = "建仓单"
|
||||
|
||||
# ── 分支2: 网格单成交 ──
|
||||
else:
|
||||
PrintLog(LogLevel.INFO,
|
||||
f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] '
|
||||
f'- 网格单成交')
|
||||
oriIdx = self.tradeTarget.grid_index # 记录原网格位置(用于日志)
|
||||
# ── 网格方向判断 ──
|
||||
# 比较成交单的网格索引 vs 当前网格索引,判断价格移动方向
|
||||
oriIdx = self.tradeTarget.grid_index # 成交前的网格位置
|
||||
|
||||
# 判断成交方向: gridIdx > currentIdx → 买入成交(下移)
|
||||
if gridIdx > self.tradeTarget.grid_index:
|
||||
desc = "下移一格"
|
||||
self.tradeTarget.grid_index += 1
|
||||
# 成交单在下方(更大索引 = 更低价格)→ 买入成交,持仓下移
|
||||
self.tradeTarget.grid_index += 1 # type: ignore
|
||||
# 首次建仓时 oriIdx==0,加上"建仓单"前缀便于识别
|
||||
desc = "建仓单(下移)" if oriIdx == 0 else "下移一格"
|
||||
|
||||
# 判断成交方向: gridIdx < currentIdx → 卖出成交(上移)
|
||||
elif gridIdx < self.tradeTarget.grid_index:
|
||||
# 成交单在上方(更小索引 = 更高价格)→ 卖出成交,持仓上移
|
||||
self.tradeTarget.grid_index -= 1 # type: ignore
|
||||
# 卖出获利:累计匹配次数和利润
|
||||
self.tradeTarget.grid_match_count += 1 # type: ignore
|
||||
# 单格利润 = grid_size × 成交量
|
||||
self.tradeTarget.grid_total_profit += ( # type: ignore
|
||||
self.tradeTarget.grid_size * trade.traded_volume)
|
||||
desc = "上移一格"
|
||||
# 累计统计
|
||||
self.tradeTarget.grid_match_count += 1 # 网格匹配次数+1
|
||||
self.tradeTarget.grid_total_profit += (
|
||||
self.tradeTarget.grid_size * trade.traded_volume
|
||||
) # 累计利润 = 网格间距 × 成交量
|
||||
self.tradeTarget.grid_index -= 1
|
||||
|
||||
# gridIdx == currentIdx: 理论上不应出现(同一个位置不会挂单给自己)
|
||||
else:
|
||||
desc = "保持格, 理论上不应该输出"
|
||||
# gridIdx == grid_index: 同格成交,正常情况下不会出现
|
||||
desc = "同格(异常)"
|
||||
|
||||
PrintLog(LogLevel.INFO,
|
||||
f'|- 委托成交通知'
|
||||
f'[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} - '
|
||||
f'原网格位置 {oriIdx}, 现网格位置 {self.tradeTarget.grid_index}')
|
||||
f'|- [{self.tradeTarget.targetName()}] '
|
||||
f'原网格 {oriIdx} → 现网格 {self.tradeTarget.grid_index}'
|
||||
f'({desc})')
|
||||
|
||||
# ── 成交后处理 ──
|
||||
# 1. 持久化状态到数据库
|
||||
# ── 成交后统一处理 ──
|
||||
# 1. 持久化状态到数据库(grid_index、持仓量等已变更)
|
||||
self.saveProxy()
|
||||
# 2. 从 orderGrid 中删除已成交的订单(pop 防重复推送 KeyError)
|
||||
# 2. 从 orderGrid 清理已成交订单(pop 防 xtquant 重复推送 KeyError)
|
||||
self.orderGrid.pop(gridIdx, None)
|
||||
|
||||
# 3. 打印成交报告
|
||||
PrintLog(LogLevel.INFO,
|
||||
f"|- 成交报告[{self.tradeTarget.targetName()}] : "
|
||||
@@ -517,8 +459,7 @@ class SFGridStrategy:
|
||||
f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
|
||||
PrintLog(LogLevel.INFO,
|
||||
f' 手续费 : {trade.commission:.3f}')
|
||||
|
||||
# 4. 刷新网格订单:在新的 grid_index 上下重新挂买卖单
|
||||
# 4. 刷新网格订单:在新的 grid_index 位置重新挂买卖单
|
||||
self.refreshGridOrder()
|
||||
|
||||
finally:
|
||||
@@ -571,7 +512,7 @@ class SFGridStrategy:
|
||||
"""
|
||||
PrintLog(LogLevel.DEBUG,
|
||||
f'|- [DEBUG] saveProxy: {self.tradeTarget.targetName()} '
|
||||
f'grid_index={self.tradeTarget.grid_index}, status={self.tradeTarget.status}')
|
||||
f'网格={self.tradeTarget.grid_index}')
|
||||
rc = self.tradeTarget.save()
|
||||
event_bus.publish(EventTradeTargetUpdate, self.tradeTarget)
|
||||
return rc
|
||||
|
||||
@@ -0,0 +1,836 @@
|
||||
"""
|
||||
Flet UI — 完整对齐 Tkinter 版布局、数据流、刷新机制。
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
import threading
|
||||
import flet as ft
|
||||
|
||||
from core.qmt_real import RealQmtV, qmtv
|
||||
from core.logger import LogLevel, PrintLog
|
||||
from core.sfgrid.model import SFGridTradeTarget, STRATEGY_TYPE_GRID, STRATEGY_TYPE_UNCLASSIFIED
|
||||
from core.sfgrid.sfgrid_strategy import SFGridStrategy
|
||||
from core.eventbus import event_bus, MarketDataUpdate, EventMarketActiveSwitch
|
||||
from core.sfgrid.bus_events import EventTradeTargetUpdate
|
||||
|
||||
|
||||
# ── 委托状态 / 方向映射 ──
|
||||
_ORDER_STATUS = {48: '未报', 49: '待报', 50: '已报', 51: '已报待撤', 52: '部成待撤',
|
||||
53: '部撤', 54: '已撤', 55: '部成', 56: '已成', 57: '废单'}
|
||||
|
||||
|
||||
def _fmt_time(t) -> str:
|
||||
"""格式化 QMT 时间为 HH:MM:SS(北京时间,Unix timestamp → 本地时间)"""
|
||||
if not t:
|
||||
return ''
|
||||
import datetime
|
||||
try:
|
||||
ts = int(t)
|
||||
if ts > 1e12: # 毫秒级
|
||||
ts //= 1000
|
||||
return datetime.datetime.fromtimestamp(ts).strftime('%H:%M:%S')
|
||||
except (ValueError, OSError):
|
||||
return str(t)
|
||||
|
||||
|
||||
def _direction(ot: int) -> str:
|
||||
return '买' if ot == 23 else '卖' if ot == 24 else str(ot)
|
||||
|
||||
|
||||
def _plain(code: str) -> str:
|
||||
return code.split('.')[0] if '.' in code else code
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# QmtApp
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
class QmtApp:
|
||||
"""Flet 版 QMT 交易界面,布局、数据流对齐 core/ui/tkinter/sfgrid_view.py"""
|
||||
|
||||
def __init__(self, page: ft.Page):
|
||||
self.page = page
|
||||
self.page.title = "神之一手"
|
||||
self.page.window.width = 1400
|
||||
self.page.window.height = 800
|
||||
self.page.padding = 0
|
||||
|
||||
# ── 状态(对齐 Tkinter TradeTargetUI) ──
|
||||
self.tradeTargetData: dict[int, SFGridTradeTarget] = {}
|
||||
self.stockCodeIdMap: dict[str, int] = {}
|
||||
self.strategy_ctrl: dict[int, SFGridStrategy] = {}
|
||||
self.targetMarketPrice: dict[int, float] = {}
|
||||
self.targetPreClose: dict[int, float] = {} # 昨收
|
||||
self.targetAvgPrice: dict[int, float] = {}
|
||||
self.marketData: dict[str, dict] = {} # stock_code → {stock_name, last_price, time}
|
||||
self.listening_stock: list = []
|
||||
self.monitor_price: float = 10.0
|
||||
self._market_active: bool = qmtv.isMarketActive
|
||||
self._refresh_cycle: int = 0
|
||||
self._drawer_open: bool = False
|
||||
self._selected_target = None
|
||||
self._prices_loaded: bool = False
|
||||
self._orders: list = []
|
||||
self._trades: list = []
|
||||
|
||||
self._run_startup()
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 启动流程(对齐 tkinter/splash.py)
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
def _run_startup(self):
|
||||
"""启动进度 — 先渲染 splash,再异步执行启动步骤"""
|
||||
bar = ft.ProgressBar(width=340, value=0, color='#0078d4')
|
||||
self._splash_status = ft.Text("正在初始化...", size=13)
|
||||
self._splash_bar = bar
|
||||
splash = ft.Container(
|
||||
ft.Column([
|
||||
ft.Text("神之一手", size=22, weight=ft.FontWeight.BOLD, color='#0078d4'),
|
||||
ft.Text("交易系统", size=14, color='#666666'),
|
||||
ft.Container(height=20),
|
||||
self._splash_status,
|
||||
ft.Container(height=8),
|
||||
bar,
|
||||
], alignment=ft.MainAxisAlignment.CENTER, horizontal_alignment=ft.CrossAxisAlignment.CENTER),
|
||||
width=380, height=200,
|
||||
bgcolor=ft.Colors.SURFACE,
|
||||
border_radius=12,
|
||||
shadow=ft.BoxShadow(blur_radius=20, color='#20000000'),
|
||||
alignment=ft.Alignment.CENTER,
|
||||
)
|
||||
self.page.add(ft.Container(
|
||||
content=splash,
|
||||
alignment=ft.Alignment.CENTER, expand=True,
|
||||
bgcolor='#F5F5F5',
|
||||
))
|
||||
self.page.update()
|
||||
|
||||
# 异步执行启动,确保 splash 先渲染
|
||||
asyncio.ensure_future(self._do_startup())
|
||||
|
||||
async def _do_startup(self):
|
||||
"""异步启动流程 — splash 已渲染,逐步执行并更新进度"""
|
||||
# 给渲染一帧的时间
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
steps = [
|
||||
("正在检查 QMT 环境...", 0.10, lambda: RealQmtV._discover_qmt_port() or True),
|
||||
("正在初始化交易器...", 0.35, lambda: qmtv.init_qmtv()),
|
||||
("正在连接 QMT...", 0.55, lambda: qmtv.connect() or True),
|
||||
("正在加载持仓数据...", 0.75, lambda: self._init_data()),
|
||||
("正在构建界面...", 0.85, lambda: None),
|
||||
("正在初始化策略...", 0.92, lambda: self._init_strategies()),
|
||||
]
|
||||
for text, pct, action in steps:
|
||||
self._splash_status.value = text
|
||||
self._splash_bar.value = pct
|
||||
self.page.update()
|
||||
try:
|
||||
result = action()
|
||||
if result is False:
|
||||
self._show_error(f"启动失败: {text}")
|
||||
return
|
||||
except Exception as e:
|
||||
self._show_error(f"启动异常: {text}\n{e}")
|
||||
return
|
||||
|
||||
self._splash_status.value = "启动完成"
|
||||
self._splash_bar.value = 1.0
|
||||
self.page.update()
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
self.page.clean()
|
||||
self._build_main_ui()
|
||||
self.page.update()
|
||||
|
||||
# 主动拉取市价(不等行情推送)
|
||||
self._pull_prices()
|
||||
# 加载委托/成交数据
|
||||
self._refresh_orders()
|
||||
self._refresh_trades()
|
||||
self._rebuild_tables()
|
||||
self.page.update()
|
||||
|
||||
# 订阅事件 + 后台刷新
|
||||
event_bus.subscribe(MarketDataUpdate, self._on_market_data)
|
||||
event_bus.subscribe(EventMarketActiveSwitch, self._on_market_active_switch)
|
||||
event_bus.subscribe(EventTradeTargetUpdate, self._on_strategy_update)
|
||||
threading.Thread(target=self._refresh_loop, daemon=True).start()
|
||||
|
||||
def _show_error(self, msg: str):
|
||||
self.page.clean()
|
||||
self.page.add(ft.Container(
|
||||
content=ft.Column([
|
||||
ft.Icon(ft.Icons.ERROR_OUTLINE, size=48, color=ft.Colors.RED),
|
||||
ft.Text(msg, size=16),
|
||||
ft.ElevatedButton("重试", on_click=lambda e: self._retry()),
|
||||
], alignment=ft.MainAxisAlignment.CENTER, horizontal_alignment=ft.CrossAxisAlignment.CENTER),
|
||||
alignment=ft.Alignment.CENTER, expand=True,
|
||||
))
|
||||
self.page.update()
|
||||
|
||||
def _retry(self):
|
||||
self.page.clean()
|
||||
self._run_startup()
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 数据初始化(对齐 Tkinter init_trade_target_pool)
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
def _init_data(self):
|
||||
positions = qmtv.getAllPositions()
|
||||
PrintLog(LogLevel.INFO, f'[Flet] 持仓: {len(positions)} 个')
|
||||
|
||||
for code, pos in positions.items():
|
||||
existing = SFGridTradeTarget.get_or_none(SFGridTradeTarget.stock_code == code)
|
||||
if existing is None:
|
||||
name = getattr(pos, 'instrument_name', '') or qmtv.getInstrumentName(code)
|
||||
SFGridTradeTarget.create(
|
||||
stock_code=code, stock_name=name,
|
||||
current_position=int(pos.volume),
|
||||
init_price=float(getattr(pos, 'avg_price', 0) or 0),
|
||||
grid_index=0, enabled=False,
|
||||
grid_start_price=float(getattr(pos, 'avg_price', 0) or 0) or 10.0,
|
||||
grid_size=1.0, grid_volume=200, grid_upper_count=1, grid_lower_count=10,
|
||||
)
|
||||
|
||||
# 获取昨收价(需要带后缀的完整代码)
|
||||
try:
|
||||
from xtquant import xtdata
|
||||
for stock_code, pos in positions.items():
|
||||
full_code = stock_code
|
||||
if '.' not in stock_code:
|
||||
c = stock_code
|
||||
full_code = f'{c}.SH' if c.startswith(('6', '5', '9')) else f'{c}.SZ'
|
||||
detail = xtdata.get_instrument_detail(full_code)
|
||||
if detail:
|
||||
pre_close = detail.get('PreClose', 0) if isinstance(detail, dict) else getattr(detail, 'PreClose', 0)
|
||||
if pre_close > 0:
|
||||
self.targetPreClose[stock_code] = float(pre_close)
|
||||
PrintLog(LogLevel.INFO, f'[Flet] 已获取 {len(self.targetPreClose)} 个标的昨收价')
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.DEBUG, f'[Flet] 昨收价获取异常: {e}')
|
||||
|
||||
results = list(SFGridTradeTarget.select())
|
||||
for t in results:
|
||||
pos = positions.get(t.stock_code)
|
||||
t.current_position = 0 if pos is None else int(pos.volume)
|
||||
tid = t.get_id()
|
||||
self.tradeTargetData[tid] = t
|
||||
self.stockCodeIdMap[t.stock_code] = tid
|
||||
if pos is not None:
|
||||
self.targetAvgPrice[tid] = float(getattr(pos, 'avg_price', 0) or 0)
|
||||
|
||||
def _init_strategies(self):
|
||||
from core.sfgrid.model import STRATEGY_TYPE_GRID
|
||||
for tid, t in self.tradeTargetData.items():
|
||||
if t.strategy_type == STRATEGY_TYPE_GRID and t.enabled:
|
||||
self.strategy_ctrl[tid] = SFGridStrategy(t)
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 主界面构建(对齐 Tkinter create_tables_area)
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
def _build_main_ui(self):
|
||||
# ── 右侧面板内容 ──
|
||||
self._tab_orders = ft.Tab(label="当前委托")
|
||||
self._tab_trades = ft.Tab(label="当日成交")
|
||||
right_bar = ft.TabBar(tabs=[
|
||||
ft.Tab(label="实时价格监控"),
|
||||
self._tab_orders,
|
||||
self._tab_trades,
|
||||
ft.Tab(label="未分类持仓"),
|
||||
])
|
||||
self._uncl_list = ft.ListView([self._build_unclassified_table()], expand=True)
|
||||
self._right_view = ft.TabBarView(controls=[
|
||||
self._build_market_view(),
|
||||
self._build_order_view(),
|
||||
self._build_trade_view(),
|
||||
self._uncl_list,
|
||||
], expand=True)
|
||||
panel_content = ft.Container(
|
||||
content=ft.Column([
|
||||
ft.Container(ft.Text("监控面板", size=14, weight=ft.FontWeight.BOLD), padding=ft.Padding(10, 10, 10, 5)),
|
||||
ft.Tabs(ft.Column([right_bar, self._right_view], expand=True), length=4, expand=True),
|
||||
], expand=True),
|
||||
width=700, bgcolor=ft.Colors.SURFACE,
|
||||
)
|
||||
|
||||
# ── 遮罩层(点击关闭) ──
|
||||
backdrop = ft.Container(
|
||||
bgcolor='#44000000', expand=True,
|
||||
on_click=lambda e: self._hide_overlay(),
|
||||
)
|
||||
|
||||
# ── overlay 行:遮罩 + 面板 ──
|
||||
self._overlay = ft.Container(
|
||||
ft.Row([backdrop, panel_content], spacing=0),
|
||||
visible=False, expand=True,
|
||||
)
|
||||
|
||||
# ── 标题栏(始终可见,选中行后显示操作按钮) ──
|
||||
self._sidebar_icon = _PanelIcon('sidebar', active=False, on_click=lambda e: self._toggle_overlay())
|
||||
self._sel_actions = ft.Row([], spacing=4) # 动态操作按钮
|
||||
self._sel_info = ft.Text("", size=12, color='#666666')
|
||||
grid_title = ft.Container(
|
||||
ft.Row([
|
||||
ft.Row([
|
||||
ft.Text("网格策略持仓", size=13, weight=ft.FontWeight.BOLD),
|
||||
self._sel_info,
|
||||
self._sel_actions,
|
||||
]),
|
||||
ft.Row([
|
||||
ft.IconButton(ft.Icons.REFRESH, tooltip="刷新", icon_size=18,
|
||||
on_click=lambda e: self._manual_refresh()),
|
||||
self._sidebar_icon,
|
||||
], spacing=0),
|
||||
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
|
||||
padding=ft.Padding(10, 10, 10, 5),
|
||||
)
|
||||
|
||||
# ── 表格(Stack 内,可被 overlay 覆盖) ──
|
||||
self._grid_list = self._build_grid_table() # 回到 DataTable
|
||||
grid_body = ft.Container(
|
||||
content=self._grid_list, expand=True,
|
||||
padding=ft.Padding(10, 0, 10, 10),
|
||||
)
|
||||
|
||||
self.page.add(ft.Column([
|
||||
grid_title,
|
||||
ft.Stack([grid_body, self._overlay], expand=True),
|
||||
], expand=True))
|
||||
|
||||
# ── 表格工具 ──
|
||||
|
||||
def _dt(self, cols: list[str], rows: list[list[str]], col_widths: list = None) -> ft.Control:
|
||||
"""构建 DataTable"""
|
||||
data_cols = [ft.DataColumn(ft.Text(h)) for h in cols]
|
||||
data_rows = []
|
||||
for r in rows:
|
||||
cells = []
|
||||
for i, c in enumerate(r):
|
||||
w = col_widths[i] if col_widths and i < len(col_widths) else None
|
||||
cells.append(ft.DataCell(ft.Text(str(c), overflow=ft.TextOverflow.ELLIPSIS,
|
||||
max_lines=1, width=w)))
|
||||
data_rows.append(ft.DataRow(cells=cells))
|
||||
if not data_rows:
|
||||
data_rows.append(ft.DataRow(cells=[ft.DataCell(ft.Text("")) for _ in cols]))
|
||||
return ft.ListView([ft.DataTable(
|
||||
columns=data_cols, rows=data_rows,
|
||||
width=float('inf'),
|
||||
heading_row_height=36, data_row_min_height=32,
|
||||
)], expand=True)
|
||||
|
||||
# ── 各表格 ──
|
||||
|
||||
def _pending_tags(self, stock_code: str) -> list:
|
||||
"""返回该标的下挂单的方向标签列表:'多'(买单) / '空'(卖单)"""
|
||||
tags = []
|
||||
_CANCELED = {54, 57}
|
||||
for o in self._orders:
|
||||
if _plain(getattr(o, 'stock_code', '')) != stock_code:
|
||||
continue
|
||||
if getattr(o, 'order_status', 0) in _CANCELED:
|
||||
continue
|
||||
ot = getattr(o, 'order_type', 0)
|
||||
if ot == 23 and '多' not in tags:
|
||||
tags.append('多')
|
||||
elif ot == 24 and '空' not in tags:
|
||||
tags.append('空')
|
||||
return tags
|
||||
|
||||
def _tag_badge(self, text: str, color: str) -> ft.Container:
|
||||
return ft.Container(
|
||||
ft.Text(text, size=10, color='white', weight=ft.FontWeight.BOLD),
|
||||
bgcolor=color, border_radius=4, padding=ft.Padding(3, 1, 3, 1),
|
||||
)
|
||||
|
||||
def _on_grid_row_select(self, target):
|
||||
"""DataRow 选中回调 — 在标题栏显示操作按钮"""
|
||||
self._selected_target = target
|
||||
name = f'{target.stock_code} {target.stock_name}'
|
||||
self._sel_info.value = f" | 已选: {name}"
|
||||
actions = []
|
||||
if target.enabled:
|
||||
actions.append(ft.ElevatedButton("⏸ 暂停", on_click=lambda e, t=target: self._on_stop_trade(t), height=28))
|
||||
else:
|
||||
actions.append(ft.ElevatedButton("▶ 启动", on_click=lambda e, t=target: self._on_start_trade(t), height=28))
|
||||
actions.append(ft.ElevatedButton("⚙ 设置", on_click=lambda e, t=target: self._open_grid_config(t), height=28))
|
||||
self._sel_actions.controls = actions
|
||||
self.page.update()
|
||||
|
||||
def _build_grid_table(self) -> ft.Control:
|
||||
"""网格表格 — DataTable + on_select_change"""
|
||||
cols = ["ID", "股票", "市场价", "持仓", "成本", "网格基准", "状态"]
|
||||
data_cols = [ft.DataColumn(ft.Text(h)) for h in cols]
|
||||
data_rows = []
|
||||
is_sel = self._selected_target is not None
|
||||
sel_id = self._selected_target.get_id() if self._selected_target else -1
|
||||
for tid, t in self.tradeTargetData.items():
|
||||
if t.strategy_type != 1:
|
||||
continue
|
||||
pg = t.getPriceGrid()
|
||||
idx = t.grid_index
|
||||
grid_base = pg[idx] if 0 <= idx < len(pg) else 0
|
||||
mp = self.targetMarketPrice.get(tid, 0) or 0
|
||||
pre_close = self.targetPreClose.get(t.stock_code, 0) or 0
|
||||
up = mp > pre_close and pre_close > 0
|
||||
down = mp < pre_close and mp > 0 and pre_close > 0
|
||||
pcolor = '#CC0000' if up else '#009900' if down else None
|
||||
gtext = ft.Text(f'{grid_base:.2f}', weight=ft.FontWeight.BOLD)
|
||||
gparts = [gtext]
|
||||
if mp > grid_base > 0:
|
||||
gparts.append(ft.Text(' ▲', color='#CC0000', weight=ft.FontWeight.BOLD))
|
||||
elif 0 < mp < grid_base:
|
||||
gparts.append(ft.Text(' ▼', color='#009900', weight=ft.FontWeight.BOLD))
|
||||
for tag in self._pending_tags(t.stock_code):
|
||||
gparts.append(self._tag_badge(tag, '#E67E22' if tag == '多' else '#3498DB'))
|
||||
gcell = ft.Row(gparts, spacing=3) if len(gparts) > 1 else gtext
|
||||
dr = ft.DataRow(cells=[
|
||||
ft.DataCell(ft.Text(str(tid))),
|
||||
ft.DataCell(ft.Text(f'{t.stock_code} {t.stock_name}')),
|
||||
ft.DataCell(ft.Text(f'{mp:.3f}', color=pcolor, weight=ft.FontWeight.BOLD)),
|
||||
ft.DataCell(ft.Text(str(t.current_position))),
|
||||
ft.DataCell(ft.Text(f'{self.targetAvgPrice.get(tid, 0):.3f}')),
|
||||
ft.DataCell(gcell),
|
||||
ft.DataCell(ft.Text('▶运行中' if t.enabled else '⏸已暂停')),
|
||||
], selected=(is_sel and tid == sel_id))
|
||||
dr.on_select_change = lambda e, t=t: self._on_grid_row_select(t)
|
||||
data_rows.append(dr)
|
||||
if not data_rows:
|
||||
data_rows.append(ft.DataRow(cells=[ft.DataCell(ft.Text("")) for _ in cols]))
|
||||
return ft.ListView([ft.DataTable(columns=data_cols, rows=data_rows,
|
||||
width=float('inf'),
|
||||
heading_row_height=36, data_row_min_height=32)], expand=True)
|
||||
|
||||
def _build_unclassified_table(self) -> ft.Control:
|
||||
cols = ["ID", "股票", "市场价", "当前持仓", "平均成本"]
|
||||
rows = []
|
||||
for tid, t in self.tradeTargetData.items():
|
||||
if t.strategy_type == STRATEGY_TYPE_GRID:
|
||||
continue
|
||||
mp = self.targetMarketPrice.get(tid, 0) or 0
|
||||
rows.append([
|
||||
str(tid),
|
||||
f'{t.stock_code} {t.stock_name}',
|
||||
f'{mp:.3f}',
|
||||
str(t.current_position),
|
||||
f'{self.targetAvgPrice.get(tid, 0):.3f}元',
|
||||
])
|
||||
return self._dt(cols, rows)
|
||||
|
||||
def _build_market_view(self) -> ft.Control:
|
||||
"""实时价格监控 — 监控配置 + 表格"""
|
||||
price_input = ft.TextField(value=str(self.monitor_price), width=80, height=32,
|
||||
text_size=13, content_padding=ft.Padding(4, 0, 4, 0))
|
||||
confirm_btn = ft.ElevatedButton("确认", on_click=lambda e: self._set_monitor_price(price_input.value), height=32)
|
||||
|
||||
self._market_table = self._dt(["时间", "股票名称", "最新价格"], [])
|
||||
return ft.Column([
|
||||
ft.Row([
|
||||
ft.Text("监控配置", size=13), ft.Text("价格", size=13),
|
||||
price_input, confirm_btn,
|
||||
]),
|
||||
ft.Container(content=self._market_table, expand=True),
|
||||
], expand=True)
|
||||
|
||||
def _build_order_view(self) -> ft.Control:
|
||||
self._order_table = self._dt(
|
||||
["时间", "代码", "名称", "方向", "委托价", "委托量", "已成交", "均价", "状态"], [],
|
||||
col_widths=[65, 55, 70, 35, 60, 80, 55, 50])
|
||||
return self._order_table
|
||||
|
||||
def _build_trade_view(self) -> ft.Control:
|
||||
self._trade_table = self._dt(
|
||||
["时间", "代码", "名称", "方向", "成交价", "成交量", "成交金额", "手续费"], [],
|
||||
col_widths=[65, 55, 70, 35, 65, 60, 70, 55])
|
||||
return self._trade_table
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 事件回调(对齐 Tkinter onMarketDataUpdated)
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
def _on_market_data(self, data: dict):
|
||||
"""行情数据回调 — 来自 QMT 推送"""
|
||||
need_rebuild = not self._prices_loaded
|
||||
updated_count = 0
|
||||
for stock_code, tick in data.items():
|
||||
plain = _plain(stock_code)
|
||||
tid = self.stockCodeIdMap.get(plain)
|
||||
lp = tick.get('lastPrice', 0)
|
||||
|
||||
if tid is not None and tid in self.tradeTargetData:
|
||||
self.targetMarketPrice[tid] = lp
|
||||
self.tradeTargetData[tid].market_price = lp
|
||||
updated_count += 1
|
||||
else:
|
||||
# 非目标标的:监控价格触发时记录
|
||||
if lp == self.monitor_price or stock_code in self.listening_stock:
|
||||
if stock_code not in self.listening_stock:
|
||||
self.listening_stock.append(stock_code)
|
||||
t_str = time.strftime("%H:%M:%S")
|
||||
name = qmtv.getInstrumentName(stock_code)
|
||||
self.marketData[stock_code] = {'stock_name': name, 'last_price': lp, 'time': t_str}
|
||||
|
||||
if need_rebuild and not self._prices_loaded and updated_count > 0:
|
||||
self._prices_loaded = True
|
||||
self._rebuild_tables()
|
||||
self.page.update()
|
||||
|
||||
def _on_market_active_switch(self, is_active: bool):
|
||||
self._market_active = is_active
|
||||
|
||||
def _on_strategy_update(self, target):
|
||||
"""策略数据变更 — 成交后立即刷新表格"""
|
||||
self._rebuild_tables()
|
||||
self.page.update()
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 刷新循环(对齐 Tkinter refresh_loop)
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
def _pull_prices(self):
|
||||
"""主动拉取缺失的市价(对齐 Tkinter refresh_loop)"""
|
||||
for tid, t in self.tradeTargetData.items():
|
||||
if tid not in self.targetMarketPrice or self.targetMarketPrice[tid] == 0:
|
||||
price = qmtv.getLastPrice(t.stock_code)
|
||||
if price > 0:
|
||||
self.targetMarketPrice[tid] = price
|
||||
t.market_price = price
|
||||
|
||||
def _manual_refresh(self):
|
||||
self._pull_prices()
|
||||
self._refresh_positions()
|
||||
self._refresh_orders()
|
||||
self._refresh_trades()
|
||||
self._rebuild_tables()
|
||||
self.page.update()
|
||||
|
||||
def _refresh_positions(self):
|
||||
positions = qmtv.getAllPositions()
|
||||
for t in self.tradeTargetData.values():
|
||||
pos = positions.get(t.stock_code)
|
||||
t.current_position = 0 if pos is None else int(pos.volume)
|
||||
|
||||
def _refresh_orders(self):
|
||||
try:
|
||||
self._orders = list(qmtv.queryTodayOrders())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _refresh_trades(self):
|
||||
try:
|
||||
self._trades = list(qmtv.queryTodayTrades())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _rebuild_tables(self):
|
||||
"""重建所有表格数据"""
|
||||
self._grid_list.controls = [self._build_grid_table()]
|
||||
if not self._selected_target:
|
||||
self._sel_info.value = ""
|
||||
self._sel_actions.controls = []
|
||||
self._uncl_list.controls = [self._build_unclassified_table()]
|
||||
|
||||
# 委托 — 过滤已撤/废单,按 order_id 去重(保留最后一条即最新状态)
|
||||
_CANCELED = {54, 57}
|
||||
o_map = {} # order_id → latest order
|
||||
for o in self._orders:
|
||||
oid = str(getattr(o, 'order_id', ''))
|
||||
if not oid:
|
||||
continue
|
||||
o_map[oid] = o # 后面的覆盖前面的
|
||||
o_rows = []
|
||||
for o in o_map.values():
|
||||
st = getattr(o, 'order_status', 0)
|
||||
if st in _CANCELED:
|
||||
continue
|
||||
tv = getattr(o, 'traded_volume', 0) or 0
|
||||
ov = getattr(o, 'order_volume', 0) or 0
|
||||
o_rows.append([
|
||||
_fmt_time(getattr(o, 'order_time', 0)),
|
||||
_plain(getattr(o, 'stock_code', '')),
|
||||
getattr(o, 'instrument_name', '') or '',
|
||||
_direction(getattr(o, 'order_type', 0)),
|
||||
f"{getattr(o, 'price', 0):.3f}",
|
||||
f"{tv}/{ov}",
|
||||
f"{getattr(o, 'traded_price', 0):.3f}" if getattr(o, 'traded_price', 0) > 0 else '-',
|
||||
_ORDER_STATUS.get(st, '未知'),
|
||||
])
|
||||
self._tab_orders.label = f"当前委托 ({len(o_rows)})" if o_rows else "当前委托"
|
||||
self._order_table.controls = [self._dt(
|
||||
["时间", "代码", "名称", "方向", "委托价", "已成交/委托量", "均价", "状态"], o_rows,
|
||||
col_widths=[65, 55, 70, 35, 60, 80, 55, 50])]
|
||||
|
||||
# 成交 — 按 traded_id 去重(保留最后一条)
|
||||
t_map = {}
|
||||
for t in self._trades:
|
||||
tid = str(getattr(t, 'traded_id', ''))
|
||||
if not tid:
|
||||
continue
|
||||
t_map[tid] = t
|
||||
t_rows = []
|
||||
for t in t_map.values():
|
||||
t_rows.append([
|
||||
_fmt_time(getattr(t, 'traded_time', 0)),
|
||||
_plain(getattr(t, 'stock_code', '')),
|
||||
getattr(t, 'instrument_name', '') or '',
|
||||
_direction(getattr(t, 'order_type', 0)),
|
||||
f"{getattr(t, 'traded_price', 0):.3f}",
|
||||
str(getattr(t, 'traded_volume', 0)),
|
||||
f"{getattr(t, 'traded_amount', 0):.2f}",
|
||||
f"{getattr(t, 'commission', 0):.2f}",
|
||||
])
|
||||
self._tab_trades.label = f"当日成交 ({len(t_rows)})" if t_rows else "当日成交"
|
||||
self._trade_table.controls = [self._dt(
|
||||
["时间", "代码", "名称", "方向", "成交价", "成交量", "成交金额", "手续费"], t_rows,
|
||||
col_widths=[65, 55, 70, 35, 65, 60, 70, 55])]
|
||||
|
||||
# 市场监控
|
||||
m_rows = []
|
||||
for sc, d in self.marketData.items():
|
||||
m_rows.append([d['time'], f"{d['stock_name']}-{sc}", f"{d['last_price']:.3f}"])
|
||||
self._market_table.controls = [self._dt(["时间", "股票名称", "最新价格"], m_rows)]
|
||||
|
||||
def _refresh_loop(self):
|
||||
"""后台定时刷新 — 对齐 Tkinter: 5s 拉价 + 30s 委托/成交"""
|
||||
while True:
|
||||
time.sleep(5)
|
||||
self._refresh_cycle += 1
|
||||
try:
|
||||
self._pull_prices()
|
||||
self._refresh_positions()
|
||||
if self._refresh_cycle % 6 == 0:
|
||||
self._refresh_orders()
|
||||
self._refresh_trades()
|
||||
self._rebuild_tables()
|
||||
self.page.update()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 工具栏按钮
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
def _on_start_trade(self, target):
|
||||
PrintLog(LogLevel.INFO, f'[Flet-按钮] 启动按钮被点击: {target.stock_code}')
|
||||
if target.enabled:
|
||||
self._show_toast("该标的正运行中")
|
||||
return
|
||||
name = f'{target.stock_code} {target.stock_name}'
|
||||
dlg = ft.AlertDialog(
|
||||
title=ft.Text("确认启动"),
|
||||
content=ft.Text(f"确定要启动交易吗?\n\n{name}"),
|
||||
actions=[
|
||||
ft.TextButton("取消", on_click=lambda e: self._close_dialog(dlg)),
|
||||
ft.TextButton("确定", on_click=lambda e, t=target: self._do_start(t)),
|
||||
],
|
||||
)
|
||||
self.page.show_dialog(dlg)
|
||||
|
||||
def _do_start(self, target):
|
||||
self.page.pop_dialog()
|
||||
target.enabled = True
|
||||
target.save()
|
||||
from core.sfgrid.sfgrid_strategy import SFGridStrategy
|
||||
self.strategy_ctrl[target.get_id()] = SFGridStrategy(target)
|
||||
self._rebuild_tables()
|
||||
self.page.update()
|
||||
PrintLog(LogLevel.INFO, f'[Flet] 启动交易: {target.targetName()}')
|
||||
|
||||
def _on_stop_trade(self, target):
|
||||
PrintLog(LogLevel.INFO, f'[Flet-按钮] 暂停按钮被点击: {target.stock_code}')
|
||||
if not target.enabled:
|
||||
self._show_toast("该标的已暂停")
|
||||
return
|
||||
name = f'{target.stock_code} {target.stock_name}'
|
||||
dlg = ft.AlertDialog(
|
||||
title=ft.Text("确认暂停"),
|
||||
content=ft.Text(f"确定要暂停交易吗?\n\n{name}"),
|
||||
actions=[
|
||||
ft.TextButton("取消", on_click=lambda e: self._close_dialog(dlg)),
|
||||
ft.TextButton("确定", on_click=lambda e, t=target: self._do_stop(t)),
|
||||
],
|
||||
)
|
||||
self.page.show_dialog(dlg)
|
||||
|
||||
def _do_stop(self, target):
|
||||
self.page.pop_dialog()
|
||||
target.enabled = False
|
||||
target.save()
|
||||
ctrl = self.strategy_ctrl.pop(target.get_id(), None)
|
||||
if ctrl:
|
||||
ctrl.enabledTrading(False)
|
||||
self._rebuild_tables()
|
||||
self.page.update()
|
||||
PrintLog(LogLevel.INFO, f'[Flet] 暂停交易: {target.targetName()}')
|
||||
|
||||
def _open_grid_config(self, target):
|
||||
"""网格配置对话框 — 对齐 Tkinter create_grid_config_window"""
|
||||
PrintLog(LogLevel.INFO, f'[Flet-按钮] 设置按钮被点击: {target.stock_code}')
|
||||
base = ft.TextField(label="基准价格", value=str(target.grid_start_price), width=120, text_size=13)
|
||||
gsize = ft.TextField(label="网格大小", value=str(target.grid_size), width=120, text_size=13)
|
||||
gvol = ft.TextField(label="网格交易量(手)", value=str(target.grid_volume), width=120, text_size=13)
|
||||
gupper = ft.TextField(label="上方网格数", value=str(target.grid_upper_count), width=120, text_size=13)
|
||||
glower = ft.TextField(label="下方网格数", value=str(target.grid_lower_count), width=120, text_size=13)
|
||||
gidx = ft.TextField(label="当前网格层级", value=str(target.grid_index), width=120, text_size=13)
|
||||
|
||||
col1 = ft.Column([base, gsize, gvol], spacing=8)
|
||||
col2 = ft.Column([gupper, glower, gidx], spacing=8)
|
||||
grid_preview = ft.Text("", size=11, italic=True)
|
||||
|
||||
def _preview(e):
|
||||
try:
|
||||
bp = float(base.value)
|
||||
gs = float(gsize.value)
|
||||
up = int(gupper.value)
|
||||
lo = int(glower.value)
|
||||
prices = []
|
||||
for i in range(up, 0, -1):
|
||||
prices.append(f"{bp + gs * i:.2f}(卖{up - i + 1})")
|
||||
prices.append(f"→{bp:.2f}←(基准)")
|
||||
for i in range(1, lo + 1):
|
||||
p = bp - gs * i
|
||||
if p > 0:
|
||||
prices.append(f"{p:.2f}(买{i})")
|
||||
grid_preview.value = " ".join(prices)
|
||||
grid_preview.update()
|
||||
except ValueError:
|
||||
grid_preview.value = "请输入有效数字"
|
||||
grid_preview.update()
|
||||
|
||||
def _save(e):
|
||||
try:
|
||||
target.grid_start_price = float(base.value)
|
||||
target.grid_size = float(gsize.value)
|
||||
target.grid_volume = int(gvol.value)
|
||||
target.grid_upper_count = int(gupper.value)
|
||||
target.grid_lower_count = int(glower.value)
|
||||
target.grid_index = int(gidx.value)
|
||||
target.save()
|
||||
self._close_dialog()
|
||||
self._rebuild_tables()
|
||||
self.page.update()
|
||||
PrintLog(LogLevel.INFO, f'[Flet] 网格配置已保存: {target.targetName()}')
|
||||
except ValueError:
|
||||
self._show_toast("请输入有效的数值")
|
||||
|
||||
dlg = ft.AlertDialog(
|
||||
title=ft.Text(f"网格配置 - {target.stock_code} {target.stock_name}"),
|
||||
content=ft.Column([
|
||||
ft.Row([col1, col2], spacing=20),
|
||||
ft.ElevatedButton("预览网格序列", on_click=_preview),
|
||||
grid_preview,
|
||||
], spacing=10, tight=True, height=320),
|
||||
actions=[
|
||||
ft.TextButton("取消", on_click=lambda e: self._close_dialog(dlg)),
|
||||
ft.ElevatedButton("保存", on_click=_save),
|
||||
],
|
||||
)
|
||||
self.page.show_dialog(dlg)
|
||||
|
||||
def _show_toast(self, msg: str):
|
||||
dlg = ft.AlertDialog(title=ft.Text("提示"), content=ft.Text(msg),
|
||||
actions=[ft.TextButton("确定", on_click=lambda e: self._close_dialog(dlg))])
|
||||
self.page.show_dialog(dlg)
|
||||
|
||||
def _close_dialog(self, dlg=None):
|
||||
self.page.pop_dialog()
|
||||
self.page.update()
|
||||
|
||||
def _toggle_overlay(self):
|
||||
self._drawer_open = not self._drawer_open
|
||||
self._overlay.visible = self._drawer_open
|
||||
self._sidebar_icon.set_active(self._drawer_open)
|
||||
self.page.update()
|
||||
|
||||
def _hide_overlay(self):
|
||||
self._drawer_open = False
|
||||
self._overlay.visible = False
|
||||
self._sidebar_icon.set_active(False)
|
||||
self.page.update()
|
||||
|
||||
def _set_monitor_price(self, val: str):
|
||||
try:
|
||||
self.monitor_price = float(val)
|
||||
self.marketData.clear()
|
||||
self.listening_stock.clear()
|
||||
self._rebuild_tables()
|
||||
self.page.update()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# PanelIcon — 对齐 Tkinter 版 Canvas 手绘图标
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
class _PanelIcon(ft.Container):
|
||||
"""VSCode 风格面板切换图标 — 两个色块拼成的分栏图标"""
|
||||
|
||||
_SIZE = 22
|
||||
_M = 3
|
||||
_COLORS = {
|
||||
'light': {'bg': '#f0f0f0', 'hover': '#d4d4d4', 'off': '#b0b0b0', 'on': '#808080', 'active': '#0078d4'},
|
||||
'dark': {'bg': '#3c3c3c', 'hover': '#505050', 'off': '#6a6a6a', 'on': '#a0a0a0', 'active': '#ffffff'},
|
||||
}
|
||||
|
||||
def __init__(self, kind: str, active: bool = True, on_click=None):
|
||||
self._kind = kind
|
||||
self._active = active
|
||||
c = self._COLORS['light'] # 默认亮色,后续可扩展暗色检测
|
||||
self._bg = c['bg']
|
||||
self._hover_bg = c['hover']
|
||||
self._off = c['off']
|
||||
self._on = c['on']
|
||||
self._active_color = c['active']
|
||||
|
||||
rects = self._build_rects()
|
||||
super().__init__(
|
||||
content=rects,
|
||||
width=self._SIZE, height=self._SIZE,
|
||||
bgcolor=self._bg, border_radius=3,
|
||||
ink=True, on_click=on_click,
|
||||
padding=ft.Padding(self._M, self._M, self._M, self._M),
|
||||
)
|
||||
|
||||
def _build_rects(self):
|
||||
off, on, act = self._off, self._on, self._active_color
|
||||
bar_w, bar_h = 6, self._SIZE - self._M * 2 - 2
|
||||
|
||||
if self._kind == 'sidebar':
|
||||
c1 = on if self._active else off
|
||||
c2 = act if self._active else off
|
||||
return ft.Row([
|
||||
ft.Container(width=bar_w, height=bar_h, bgcolor=c1, border_radius=1),
|
||||
ft.Container(width=2), # gap
|
||||
ft.Container(width=bar_w, height=bar_h, bgcolor=c2, border_radius=1),
|
||||
], spacing=0)
|
||||
else:
|
||||
c1 = on if self._active else off
|
||||
c2 = act if self._active else off
|
||||
return ft.Column([
|
||||
ft.Container(width=bar_h, height=bar_w, bgcolor=c1, border_radius=1),
|
||||
ft.Container(height=2), # gap
|
||||
ft.Container(width=bar_h, height=bar_w, bgcolor=c2, border_radius=1),
|
||||
], spacing=0)
|
||||
|
||||
def set_active(self, active: bool):
|
||||
self._active = active
|
||||
self.content = self._build_rects()
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# 入口
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
|
||||
def main(page: ft.Page):
|
||||
QmtApp(page)
|
||||
|
||||
|
||||
def run():
|
||||
ft.app(target=main)
|
||||
|
||||
def run_web():
|
||||
ft.app(target=main, view=ft.AppView.WEB_BROWSER, port=8550)
|
||||
@@ -0,0 +1,863 @@
|
||||
"""
|
||||
Flet UI v2 — 重构版。
|
||||
分层架构:
|
||||
DataStore — 纯数据,无 UI 依赖
|
||||
GridPanel — 网格策略持仓表格 + 选中 + 操作栏
|
||||
DrawerPanel — 右侧抽屉叠加层 + 四 Tab
|
||||
TradeDialogs— 启动/暂停/网格配置弹窗
|
||||
QmtApp — 生命周期协调
|
||||
"""
|
||||
import asyncio
|
||||
import time
|
||||
import threading
|
||||
import flet as ft
|
||||
|
||||
from core.qmt_real import RealQmtV, qmtv
|
||||
from core.logger import LogLevel, PrintLog
|
||||
from core.sfgrid.model import SFGridTradeTarget, STRATEGY_TYPE_GRID
|
||||
from core.sfgrid.sfgrid_strategy import SFGridStrategy
|
||||
from core.eventbus import event_bus, MarketDataUpdate, EventMarketActiveSwitch
|
||||
from core.sfgrid.bus_events import EventTradeTargetUpdate
|
||||
|
||||
# ── 工具函数 ──
|
||||
_ORDER_STATUS = {48: '未报', 49: '待报', 50: '已报', 51: '已报待撤', 52: '部成待撤',
|
||||
53: '部撤', 54: '已撤', 55: '部成', 56: '已成', 57: '废单'}
|
||||
|
||||
def _fmt_time(t) -> str:
|
||||
if not t: return ''
|
||||
import datetime
|
||||
try:
|
||||
ts = int(t)
|
||||
if ts > 1e12: ts //= 1000
|
||||
return datetime.datetime.fromtimestamp(ts).strftime('%H:%M:%S')
|
||||
except (ValueError, OSError):
|
||||
return str(t)
|
||||
|
||||
def _direction(ot: int) -> str:
|
||||
return '买' if ot == 23 else '卖' if ot == 24 else str(ot)
|
||||
|
||||
def _plain(code: str) -> str:
|
||||
return code.split('.')[0] if '.' in code else code
|
||||
|
||||
def _text(s, color=None, bold=False, size=None) -> ft.Text:
|
||||
"""快捷构建 Text"""
|
||||
return ft.Text(str(s), color=color, weight=ft.FontWeight.BOLD if bold else None, size=size)
|
||||
|
||||
def _datatable(cols: list[str], rows: list[list[str]], col_widths: list = None) -> ft.DataTable:
|
||||
"""构建 DataTable(不包装 ListView)"""
|
||||
data_rows = []
|
||||
for r in rows:
|
||||
cells = []
|
||||
for i, c in enumerate(r):
|
||||
w = (col_widths or [None] * len(r))[i] if i < len(r) else None
|
||||
cells.append(ft.DataCell(ft.Text(str(c), overflow=ft.TextOverflow.ELLIPSIS, max_lines=1, width=w)))
|
||||
data_rows.append(ft.DataRow(cells=cells))
|
||||
if not data_rows:
|
||||
data_rows.append(ft.DataRow(cells=[ft.DataCell(ft.Text("")) for _ in cols]))
|
||||
return ft.DataTable(columns=[ft.DataColumn(ft.Text(h)) for h in cols], rows=data_rows,
|
||||
width=float('inf'), heading_row_height=36, data_row_min_height=32)
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# DataStore — 纯数据层
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
class _DataStore:
|
||||
"""持仓、行情、订单数据管理,不依赖任何 UI"""
|
||||
|
||||
def __init__(self):
|
||||
self.tradeTargets: dict[int, SFGridTradeTarget] = {}
|
||||
self.stockCodeIdMap: dict[str, int] = {}
|
||||
self.marketPrices: dict[int, float] = {} # tid → lastPrice
|
||||
self.preClose: dict[int, float] = {} # tid → 昨收
|
||||
self.avgPrices: dict[int, float] = {} # tid → avgPrice
|
||||
self.orders: list = []
|
||||
self.trades: list = []
|
||||
self.strategyCtrl: dict[int, SFGridStrategy] = {}
|
||||
# 市场监控
|
||||
self.monitorPrice: float = 10.0
|
||||
self.marketLog: dict[str, dict] = {} # stock_code → {name, price, time}
|
||||
self.listeningStocks: list = []
|
||||
|
||||
# ── 初始化 ──
|
||||
|
||||
def load_from_qmt(self):
|
||||
"""从 QMT 加载持仓并导入 DB"""
|
||||
positions = qmtv.getAllPositions()
|
||||
PrintLog(LogLevel.INFO, f'[Data] 持仓: {len(positions)} 个')
|
||||
for code, pos in positions.items():
|
||||
existing = SFGridTradeTarget.get_or_none(SFGridTradeTarget.stock_code == code)
|
||||
if existing is None:
|
||||
name = getattr(pos, 'instrument_name', '') or qmtv.getInstrumentName(code)
|
||||
SFGridTradeTarget.create(
|
||||
stock_code=code, stock_name=name,
|
||||
current_position=int(pos.volume),
|
||||
init_price=float(getattr(pos, 'avg_price', 0) or 0),
|
||||
grid_index=0, enabled=False,
|
||||
grid_start_price=float(getattr(pos, 'avg_price', 0) or 0) or 10.0,
|
||||
grid_size=1.0, grid_volume=200, grid_upper_count=1, grid_lower_count=10,
|
||||
)
|
||||
|
||||
# 映射 (先建立 stockCodeIdMap)
|
||||
results = list(SFGridTradeTarget.select())
|
||||
for t in results:
|
||||
pos = positions.get(t.stock_code)
|
||||
t.current_position = 0 if pos is None else int(pos.volume)
|
||||
tid = t.get_id()
|
||||
self.tradeTargets[tid] = t
|
||||
self.stockCodeIdMap[t.stock_code] = tid
|
||||
if pos is not None:
|
||||
self.avgPrices[tid] = float(getattr(pos, 'avg_price', 0) or 0)
|
||||
# 昨收(依赖 stockCodeIdMap)
|
||||
self._load_preclose(positions)
|
||||
|
||||
def _load_preclose(self, positions: dict):
|
||||
try:
|
||||
from xtquant import xtdata
|
||||
for stock_code in positions:
|
||||
if '.' not in stock_code:
|
||||
full_code = f'{stock_code}.SH' if stock_code.startswith(('6','5','9')) else f'{stock_code}.SZ'
|
||||
else:
|
||||
full_code = stock_code
|
||||
detail = xtdata.get_instrument_detail(full_code)
|
||||
if detail:
|
||||
pc = detail.get('PreClose', 0) if isinstance(detail, dict) else getattr(detail, 'PreClose', 0)
|
||||
if pc > 0 and stock_code in self.stockCodeIdMap:
|
||||
self.preClose[self.stockCodeIdMap[stock_code]] = float(pc)
|
||||
PrintLog(LogLevel.INFO, f'[Data] 昨收: {len(self.preClose)} 个')
|
||||
except Exception as e:
|
||||
PrintLog(LogLevel.DEBUG, f'[Data] 昨收异常: {e}')
|
||||
|
||||
def init_strategies(self):
|
||||
for tid, t in self.tradeTargets.items():
|
||||
if t.strategy_type == STRATEGY_TYPE_GRID and t.enabled:
|
||||
self.strategyCtrl[tid] = SFGridStrategy(t)
|
||||
|
||||
# ── 行情 ──
|
||||
|
||||
def apply_tick(self, data: dict) -> int:
|
||||
"""应用行情数据,返回更新的标的数"""
|
||||
count = 0
|
||||
for stock_code, tick in data.items():
|
||||
plain = _plain(stock_code)
|
||||
tid = self.stockCodeIdMap.get(plain)
|
||||
lp = tick.get('lastPrice', 0)
|
||||
if tid is not None and tid in self.tradeTargets:
|
||||
self.marketPrices[tid] = lp
|
||||
self.tradeTargets[tid].market_price = lp
|
||||
count += 1
|
||||
elif lp == self.monitorPrice or stock_code in self.listeningStocks:
|
||||
if stock_code not in self.listeningStocks:
|
||||
self.listeningStocks.append(stock_code)
|
||||
self.marketLog[stock_code] = {
|
||||
'stock_name': qmtv.getInstrumentName(stock_code),
|
||||
'last_price': lp,
|
||||
'time': time.strftime("%H:%M:%S"),
|
||||
}
|
||||
return count
|
||||
|
||||
def pull_prices(self):
|
||||
"""主动拉取缺失的市价"""
|
||||
for tid, t in self.tradeTargets.items():
|
||||
if tid not in self.marketPrices or self.marketPrices[tid] == 0:
|
||||
price = qmtv.getLastPrice(t.stock_code)
|
||||
if price > 0:
|
||||
self.marketPrices[tid] = price
|
||||
t.market_price = price
|
||||
|
||||
# ── 持仓 ──
|
||||
|
||||
def refresh_positions(self):
|
||||
positions = qmtv.getAllPositions()
|
||||
for t in self.tradeTargets.values():
|
||||
pos = positions.get(t.stock_code)
|
||||
t.current_position = 0 if pos is None else int(pos.volume)
|
||||
|
||||
# ── 委托 / 成交 ──
|
||||
|
||||
def refresh_orders(self):
|
||||
try:
|
||||
self.orders = list(qmtv.queryTodayOrders())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def refresh_trades(self):
|
||||
try:
|
||||
self.trades = list(qmtv.queryTodayTrades())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def active_orders(self) -> list:
|
||||
"""返回过滤+去重后的活跃委托行"""
|
||||
_CANCELED = {54, 57}
|
||||
o_map = {}
|
||||
for o in self.orders:
|
||||
oid = str(getattr(o, 'order_id', ''))
|
||||
if not oid: continue
|
||||
o_map[oid] = o
|
||||
rows = []
|
||||
for o in o_map.values():
|
||||
st = getattr(o, 'order_status', 0)
|
||||
if st in _CANCELED: continue
|
||||
tv = getattr(o, 'traded_volume', 0) or 0
|
||||
ov = getattr(o, 'order_volume', 0) or 0
|
||||
rows.append([
|
||||
_fmt_time(getattr(o, 'order_time', 0)),
|
||||
_plain(getattr(o, 'stock_code', '')),
|
||||
getattr(o, 'instrument_name', '') or '',
|
||||
_direction(getattr(o, 'order_type', 0)),
|
||||
f"{getattr(o, 'price', 0):.3f}",
|
||||
f"{tv}/{ov}",
|
||||
f"{getattr(o, 'traded_price', 0):.3f}" if getattr(o, 'traded_price', 0) > 0 else '-',
|
||||
_ORDER_STATUS.get(st, '未知'),
|
||||
])
|
||||
return rows
|
||||
|
||||
def active_trades(self) -> list:
|
||||
"""返回去重后的成交行"""
|
||||
t_map = {}
|
||||
for t in self.trades:
|
||||
tid = str(getattr(t, 'traded_id', ''))
|
||||
if not tid: continue
|
||||
t_map[tid] = t
|
||||
rows = []
|
||||
for t in t_map.values():
|
||||
rows.append([
|
||||
_fmt_time(getattr(t, 'traded_time', 0)),
|
||||
_plain(getattr(t, 'stock_code', '')),
|
||||
getattr(t, 'instrument_name', '') or '',
|
||||
_direction(getattr(t, 'order_type', 0)),
|
||||
f"{getattr(t, 'traded_price', 0):.3f}",
|
||||
str(getattr(t, 'traded_volume', 0)),
|
||||
f"{getattr(t, 'traded_amount', 0):.2f}",
|
||||
f"{getattr(t, 'commission', 0):.2f}",
|
||||
])
|
||||
return rows
|
||||
|
||||
def pending_tags(self, stock_code: str) -> list:
|
||||
"""某标的挂单方向标签"""
|
||||
tags = []
|
||||
for o in self.orders:
|
||||
if _plain(getattr(o, 'stock_code', '')) != stock_code: continue
|
||||
if getattr(o, 'order_status', 0) in {54, 57}: continue
|
||||
ot = getattr(o, 'order_type', 0)
|
||||
if ot == 23 and '多' not in tags: tags.append('多')
|
||||
elif ot == 24 and '空' not in tags: tags.append('空')
|
||||
return tags
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# GridPanel — 网格策略持仓表格
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
class _GridPanel:
|
||||
"""网格策略持仓面板 — 自定义 Row 表格,行内按钮直接可用"""
|
||||
|
||||
def __init__(self, data: _DataStore, dialogs=None):
|
||||
self._data = data
|
||||
self._dialogs = dialogs # _TradeDialogs
|
||||
self._col = None
|
||||
|
||||
@property
|
||||
def controls(self):
|
||||
return self._col.controls if self._col else []
|
||||
|
||||
def build(self) -> ft.Control:
|
||||
self._col = ft.Column(spacing=0, scroll=ft.ScrollMode.AUTO, expand=True)
|
||||
self._rebuild()
|
||||
return self._col
|
||||
|
||||
def _rebuild(self):
|
||||
# 列: (宽度/expand, ...) expand为比重,无则固定宽
|
||||
_C = [(40, 0), (0, 2), (80, 2), (0, 2), (55, 1), (65, 1), (75, 1), (100, 0)]
|
||||
H = ["ID", "股票", "市场价", "网格基准", "持仓", "成本", "状态", "操作"]
|
||||
header = []
|
||||
for h, (w, e) in zip(H, _C):
|
||||
if e > 0:
|
||||
header.append(ft.Container(_text(h, bold=True), padding=4, expand=e))
|
||||
else:
|
||||
header.append(ft.Container(_text(h, bold=True), width=w, padding=4))
|
||||
rows = [ft.Row(header, spacing=0), ft.Divider(height=1, color='#e0e0e0')]
|
||||
|
||||
for tid, t in self._data.tradeTargets.items():
|
||||
if t.strategy_type != STRATEGY_TYPE_GRID: continue
|
||||
pg = t.getPriceGrid()
|
||||
idx = t.grid_index
|
||||
base = pg[idx] if 0 <= idx < len(pg) else 0
|
||||
mp = self._data.marketPrices.get(tid, 0) or 0
|
||||
pc = self._data.preClose.get(tid, 0) or 0
|
||||
pcolor = '#CC0000' if mp > pc > 0 else '#009900' if mp < pc and mp > 0 else None
|
||||
|
||||
# 网格基准列
|
||||
gcells = [_text(f'{base:.2f}', bold=True, )]
|
||||
if mp > base > 0:
|
||||
gcells.append(_text(' ▲', color='#CC0000', bold=True, ))
|
||||
elif 0 < mp < base:
|
||||
gcells.append(_text(' ▼', color='#009900', bold=True, ))
|
||||
for tag in self._data.pending_tags(t.stock_code):
|
||||
gcells.append(ft.Container(
|
||||
_text(tag, color='white', bold=True, size=10),
|
||||
bgcolor='#E67E22' if tag == '多' else '#3498DB',
|
||||
border_radius=4, padding=ft.Padding(3, 1, 3, 1),
|
||||
))
|
||||
gcell = ft.Row(gcells, spacing=3) if len(gcells) > 1 else gcells[0]
|
||||
|
||||
# 操作按钮 — IconButton 图标居中,无点击偏移
|
||||
if t.enabled:
|
||||
btns = [
|
||||
ft.IconButton(ft.Icons.PAUSE, icon_size=18, tooltip="暂停",
|
||||
on_click=lambda e, tt=t: self._dialogs.confirm_stop(tt)),
|
||||
]
|
||||
else:
|
||||
btns = [
|
||||
ft.IconButton(ft.Icons.PLAY_ARROW, icon_size=18, tooltip="启动",
|
||||
on_click=lambda e, tt=t: self._dialogs.confirm_start(tt)),
|
||||
]
|
||||
btns.append(ft.IconButton(ft.Icons.SETTINGS, icon_size=18, tooltip="设置",
|
||||
icon_color='#BDBDBD' if t.enabled else None,
|
||||
on_click=lambda e, tt=t: self._dialogs.open_config(tt)))
|
||||
|
||||
cells_text = [str(tid), f'{t.stock_code} {t.stock_name}',
|
||||
f'{mp:.3f}', gcell,
|
||||
str(t.current_position),
|
||||
f'{self._data.avgPrices.get(tid, 0):.3f}',
|
||||
'▶运行中' if t.enabled else '⏸已暂停',
|
||||
btns]
|
||||
row_cells = []
|
||||
for i, (w, e) in enumerate(_C):
|
||||
content = cells_text[i]
|
||||
if i == 2: # 市场价用颜色
|
||||
content = _text(cells_text[i], color=pcolor, bold=True)
|
||||
elif isinstance(content, str):
|
||||
content = _text(content)
|
||||
elif isinstance(content, list):
|
||||
content = ft.Row(content, spacing=2)
|
||||
if e > 0:
|
||||
row_cells.append(ft.Container(content, padding=4, expand=e))
|
||||
else:
|
||||
row_cells.append(ft.Container(content, width=w, padding=4 if i != 7 else 2))
|
||||
row = ft.Row(row_cells, spacing=0)
|
||||
rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2)))
|
||||
rows.append(ft.Divider(height=1, color='#f0f0f0'))
|
||||
|
||||
self._col.controls = rows
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# DrawerPanel — 右侧抽屉面板
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
class _DrawerPanel:
|
||||
"""右侧叠加抽屉:市场监控 + 委托 + 成交 + 未分类"""
|
||||
|
||||
def __init__(self, data: _DataStore, dialogs=None):
|
||||
self._data = data
|
||||
self._dialogs = dialogs
|
||||
self._tab_orders = ft.Tab(label="当前委托")
|
||||
self._tab_trades = ft.Tab(label="当日成交")
|
||||
self._order_table = None
|
||||
self._trade_table = None
|
||||
self._uncl_col = None
|
||||
self._market_table = None
|
||||
|
||||
def build(self) -> ft.Control:
|
||||
"""返回 overlay Container"""
|
||||
self._uncl_col = ft.Column(spacing=0, scroll=ft.ScrollMode.AUTO, expand=True)
|
||||
self._order_table = ft.ListView(expand=True)
|
||||
self._trade_table = ft.ListView(expand=True)
|
||||
self._market_table = ft.ListView(expand=True)
|
||||
|
||||
bar = ft.TabBar(tabs=[
|
||||
ft.Tab(label="实时价格监控"),
|
||||
self._tab_orders,
|
||||
self._tab_trades,
|
||||
ft.Tab(label="未分类持仓"),
|
||||
])
|
||||
view = ft.TabBarView(controls=[
|
||||
self._build_market_tab(),
|
||||
self._order_table,
|
||||
self._trade_table,
|
||||
self._uncl_col,
|
||||
], expand=True)
|
||||
|
||||
panel = ft.Container(
|
||||
ft.Column([
|
||||
ft.Container(_text("监控面板", bold=True, size=14), padding=ft.Padding(10, 10, 10, 5)),
|
||||
ft.Tabs(ft.Column([bar, view], expand=True), length=4, expand=True),
|
||||
], expand=True),
|
||||
width=700, bgcolor=ft.Colors.SURFACE,
|
||||
)
|
||||
|
||||
backdrop = ft.Container(bgcolor='#44000000', expand=True)
|
||||
return ft.Container(ft.Row([backdrop, panel], spacing=0), visible=False, expand=True)
|
||||
|
||||
def _build_market_tab(self) -> ft.Control:
|
||||
price_input = ft.TextField(value=str(self._data.monitorPrice), width=80, height=32, text_size=13)
|
||||
return ft.Column([
|
||||
ft.Row([_text("监控配置", size=13), _text("价格", size=13), price_input,
|
||||
ft.ElevatedButton("确认", on_click=lambda e: self._set_monitor(price_input.value), height=32)]),
|
||||
ft.Container(content=self._market_table, expand=True),
|
||||
], expand=True)
|
||||
|
||||
def _set_monitor(self, val: str):
|
||||
try:
|
||||
self._data.monitorPrice = float(val)
|
||||
self._data.marketLog.clear()
|
||||
self._data.listeningStocks.clear()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# ── 刷新 ──
|
||||
|
||||
def refresh_all(self):
|
||||
self._refresh_grid()
|
||||
self._refresh_orders()
|
||||
self._refresh_trades()
|
||||
self._refresh_market()
|
||||
|
||||
def _refresh_grid(self):
|
||||
W = [35, 180, 80, 60, 70, 65] # ID, 股票, 市场价, 持仓, 成本, 操作
|
||||
H = ["ID", "股票", "市场价", "持仓", "成本", "操作"]
|
||||
rows = [
|
||||
ft.Row([ft.Container(_text(h, bold=True), width=w, padding=4)
|
||||
for h, w in zip(H, W)], spacing=0),
|
||||
ft.Divider(height=1, color='#e0e0e0'),
|
||||
]
|
||||
for tid, t in self._data.tradeTargets.items():
|
||||
if t.strategy_type == STRATEGY_TYPE_GRID: continue
|
||||
mp = self._data.marketPrices.get(tid, 0) or 0
|
||||
row = ft.Row([
|
||||
ft.Container(_text(str(tid)), width=W[0], padding=4),
|
||||
ft.Container(_text(f'{t.stock_code} {t.stock_name}'), width=W[1], padding=4),
|
||||
ft.Container(_text(f'{mp:.3f}'), width=W[2], padding=4),
|
||||
ft.Container(_text(str(t.current_position)), width=W[3], padding=4),
|
||||
ft.Container(_text(f'{self._data.avgPrices.get(tid, 0):.3f}'), width=W[4], padding=4),
|
||||
ft.Container(ft.IconButton(ft.Icons.SETTINGS, icon_size=18, tooltip="网格配置",
|
||||
on_click=lambda e, tt=t: self._dialogs.open_config(tt)),
|
||||
width=W[5], padding=0),
|
||||
], spacing=0)
|
||||
rows.append(ft.Container(row, padding=ft.Padding(0, 2, 0, 2)))
|
||||
rows.append(ft.Divider(height=1, color='#f0f0f0'))
|
||||
self._uncl_col.controls = rows
|
||||
|
||||
def _refresh_orders(self):
|
||||
rows = self._data.active_orders()
|
||||
self._tab_orders.label = f"当前委托 ({len(rows)})" if rows else "当前委托"
|
||||
cols = ["时间", "代码", "名称", "方向", "委托价", "已成交/委托量", "均价", "状态"]
|
||||
self._order_table.controls = [ft.ListView(
|
||||
[_datatable(cols, rows, [65,55,70,35,60,80,55,50])], expand=True)]
|
||||
|
||||
def _refresh_trades(self):
|
||||
rows = self._data.active_trades()
|
||||
self._tab_trades.label = f"当日成交 ({len(rows)})" if rows else "当日成交"
|
||||
cols = ["时间", "代码", "名称", "方向", "成交价", "成交量", "成交金额", "手续费"]
|
||||
self._trade_table.controls = [ft.ListView(
|
||||
[_datatable(cols, rows, [65,55,70,35,65,60,70,55])], expand=True)]
|
||||
|
||||
def _refresh_market(self):
|
||||
rows = [[d['time'], f"{d['stock_name']}-{sc}", f"{d['last_price']:.3f}"]
|
||||
for sc, d in self._data.marketLog.items()]
|
||||
self._market_table.controls = [ft.ListView(
|
||||
[_datatable(["时间", "股票名称", "最新价格"], rows)], expand=True)]
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# ActionBar — 选中行后的操作按钮栏
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# SplashScreen
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
class _SplashScreen:
|
||||
def __init__(self, page: ft.Page):
|
||||
self._bar = ft.ProgressBar(width=340, value=0, color='#0078d4')
|
||||
self._status = ft.Text("正在初始化...", size=13)
|
||||
self._page = page
|
||||
page.add(ft.Container(
|
||||
ft.Container(
|
||||
ft.Column([
|
||||
_text("神之一手", color='#0078d4', bold=True, size=22),
|
||||
_text("交易系统", color='#666666', size=14),
|
||||
ft.Container(height=20),
|
||||
self._status,
|
||||
ft.Container(height=8),
|
||||
self._bar,
|
||||
], alignment=ft.MainAxisAlignment.CENTER, horizontal_alignment=ft.CrossAxisAlignment.CENTER),
|
||||
width=380, height=200,
|
||||
bgcolor=ft.Colors.SURFACE, border_radius=12,
|
||||
shadow=ft.BoxShadow(blur_radius=20, color='#20000000'),
|
||||
alignment=ft.Alignment.CENTER,
|
||||
),
|
||||
alignment=ft.Alignment.CENTER, expand=True, bgcolor='#F5F5F5',
|
||||
))
|
||||
page.update()
|
||||
|
||||
def progress(self, text: str, pct: float):
|
||||
self._status.value = text
|
||||
self._bar.value = pct
|
||||
self._page.update()
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# TradeDialogs
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
class _TradeDialogs:
|
||||
"""启动/暂停/网格配置弹窗"""
|
||||
|
||||
def __init__(self, page: ft.Page, data: _DataStore, on_done):
|
||||
self._page = page
|
||||
self._data = data
|
||||
self._on_done = on_done
|
||||
|
||||
def confirm_start(self, target):
|
||||
dlg = ft.AlertDialog(
|
||||
title=_text("确认启动"),
|
||||
content=_text(f"确定要启动交易吗?\n\n{target.stock_code} {target.stock_name}"),
|
||||
actions=[
|
||||
ft.TextButton("取消", on_click=lambda e: self._close()),
|
||||
ft.TextButton("确定", on_click=lambda e, t=target: self._do_start(t)),
|
||||
],
|
||||
)
|
||||
self._page.show_dialog(dlg)
|
||||
|
||||
def confirm_stop(self, target):
|
||||
dlg = ft.AlertDialog(
|
||||
title=_text("确认暂停"),
|
||||
content=_text(f"确定要暂停交易吗?\n\n{target.stock_code} {target.stock_name}"),
|
||||
actions=[
|
||||
ft.TextButton("取消", on_click=lambda e: self._close()),
|
||||
ft.TextButton("确定", on_click=lambda e, t=target: self._do_stop(t)),
|
||||
],
|
||||
)
|
||||
self._page.show_dialog(dlg)
|
||||
|
||||
def open_config(self, target):
|
||||
if target.enabled:
|
||||
dlg = ft.AlertDialog(title=_text("提示"), content=_text("运行中的策略无法修改参数,请先暂停"))
|
||||
self._page.show_dialog(dlg)
|
||||
# 1.5 秒后自动关闭
|
||||
import asyncio
|
||||
async def _auto_close():
|
||||
await asyncio.sleep(1.5)
|
||||
self._page.pop_dialog()
|
||||
self._page.update()
|
||||
asyncio.ensure_future(_auto_close())
|
||||
return
|
||||
PrintLog(LogLevel.INFO, f'[Flet-v2] 打开网格配置: {target.stock_code}')
|
||||
|
||||
def _row(label, value, unit=''):
|
||||
inp = ft.TextField(value=str(value), width=120, height=30, text_size=13,
|
||||
content_padding=ft.Padding(6, 0, 6, 0))
|
||||
return ft.Row([_text(label, size=12),
|
||||
inp,
|
||||
_text(unit, color='#888888', size=12) if unit else ft.Container(width=0)],
|
||||
spacing=6, alignment=ft.MainAxisAlignment.START)
|
||||
|
||||
r1 = _row("基准价格:", target.grid_start_price, "元")
|
||||
r2 = _row("网格大小:", target.grid_size, "元")
|
||||
r3 = _row("网格交易量:", target.grid_volume, "手")
|
||||
r4 = _row("上方网格数量:", target.grid_upper_count, "格")
|
||||
r5 = _row("下方网格数量:", target.grid_lower_count, "格")
|
||||
r6 = _row("当前网格层级:", target.grid_index, "")
|
||||
|
||||
all_rows = [r1, r2, r3, r4, r5, r6]
|
||||
inputs = [r.controls[1] for r in all_rows] # index 1 is the TextField
|
||||
|
||||
preview = ft.Text("", size=11, italic=True)
|
||||
|
||||
def _refresh_preview(e=None):
|
||||
try:
|
||||
bp, gs = float(inputs[0].value), float(inputs[1].value)
|
||||
up, lo = int(inputs[3].value), int(inputs[4].value)
|
||||
parts = []
|
||||
for i in range(up, 0, -1): parts.append(f"{bp + gs * i:.2f}(卖{up - i + 1})")
|
||||
parts.append(f"→{bp:.2f}←(基准)")
|
||||
for i in range(1, lo + 1):
|
||||
p = bp - gs * i
|
||||
if p > 0: parts.append(f"{p:.2f}(买{i})")
|
||||
preview.value = " ".join(parts) or "—"
|
||||
except ValueError:
|
||||
preview.value = ""
|
||||
preview.update()
|
||||
|
||||
# 实时计算:四个关键字段绑定 on_change
|
||||
for idx in (0, 1, 3, 4):
|
||||
inputs[idx].on_change = _refresh_preview
|
||||
|
||||
def _save(e):
|
||||
try:
|
||||
target.grid_start_price = float(inputs[0].value)
|
||||
target.grid_size = float(inputs[1].value)
|
||||
target.grid_volume = int(inputs[2].value)
|
||||
target.grid_upper_count = int(inputs[3].value)
|
||||
target.grid_lower_count = int(inputs[4].value)
|
||||
target.grid_index = int(inputs[5].value)
|
||||
target.save()
|
||||
self._close()
|
||||
self._on_done()
|
||||
PrintLog(LogLevel.INFO, f'[Flet] 网格配置已保存: {target.targetName()}')
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
info = ft.Column([
|
||||
ft.Row([_text("股票代码:", bold=True, size=12), _text(target.stock_code, size=12)]),
|
||||
ft.Row([_text("股票名称:", bold=True, size=12), _text(target.stock_name, size=12)]),
|
||||
], spacing=2)
|
||||
|
||||
self._page.show_dialog(ft.AlertDialog(
|
||||
title=_text(f"网格配置 - {target.stock_code} ({target.stock_name})"),
|
||||
content=ft.Column([
|
||||
ft.Container(info, bgcolor='#F5F5F5', padding=10, border_radius=6),
|
||||
ft.Divider(height=1, color='#e0e0e0'),
|
||||
*all_rows,
|
||||
preview,
|
||||
], spacing=8, tight=True, height=420, scroll=ft.ScrollMode.AUTO),
|
||||
actions=[ft.TextButton("取消", on_click=lambda e: self._close()),
|
||||
ft.ElevatedButton("保存", on_click=_save)],
|
||||
))
|
||||
|
||||
def toast(self, msg: str):
|
||||
self._page.show_dialog(ft.AlertDialog(
|
||||
title=_text("提示"), content=_text(msg),
|
||||
actions=[ft.TextButton("确定", on_click=lambda e: self._close())],
|
||||
))
|
||||
|
||||
def _do_start(self, target):
|
||||
self._close()
|
||||
target.enabled = True
|
||||
target.save()
|
||||
self._data.strategyCtrl[target.get_id()] = SFGridStrategy(target)
|
||||
self._on_done()
|
||||
PrintLog(LogLevel.INFO, f'[Flet] 启动交易: {target.targetName()}')
|
||||
|
||||
def _do_stop(self, target):
|
||||
self._close()
|
||||
target.enabled = False
|
||||
target.save()
|
||||
ctrl = self._data.strategyCtrl.pop(target.get_id(), None)
|
||||
if ctrl: ctrl.enabledTrading(False)
|
||||
self._on_done()
|
||||
PrintLog(LogLevel.INFO, f'[Flet] 暂停交易: {target.targetName()}')
|
||||
|
||||
def _close(self):
|
||||
self._page.pop_dialog()
|
||||
self._page.update()
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# QmtApp v2
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
class QmtApp:
|
||||
"""Flet 版 QMT 交易界面 v2"""
|
||||
|
||||
def __init__(self, page: ft.Page):
|
||||
self.page = page
|
||||
self.page.title = "神之一手"
|
||||
self.page.window.width = 1400
|
||||
self.page.window.height = 800
|
||||
self.page.padding = 0
|
||||
|
||||
self._data = _DataStore()
|
||||
self._dialogs = _TradeDialogs(page, self._data, self._refresh_ui)
|
||||
self._grid = _GridPanel(self._data, dialogs=self._dialogs)
|
||||
self._drawer = _DrawerPanel(self._data, dialogs=self._dialogs)
|
||||
self._prices_loaded = False
|
||||
|
||||
self._run_startup()
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 启动
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
def _run_startup(self):
|
||||
splash = _SplashScreen(self.page)
|
||||
asyncio.ensure_future(self._do_startup(splash))
|
||||
|
||||
async def _do_startup(self, splash: _SplashScreen):
|
||||
await asyncio.sleep(0.05)
|
||||
|
||||
steps = [
|
||||
("正在检查 QMT 环境...", 0.10, lambda: RealQmtV._discover_qmt_port() or True),
|
||||
("正在初始化交易器...", 0.35, lambda: qmtv.init_qmtv()),
|
||||
("正在连接 QMT...", 0.55, lambda: qmtv.connect() or True),
|
||||
("正在加载持仓数据...", 0.75, lambda: self._data.load_from_qmt()),
|
||||
("正在构建界面...", 0.85, lambda: None),
|
||||
("正在初始化策略...", 0.92, lambda: self._data.init_strategies()),
|
||||
]
|
||||
for text, pct, action in steps:
|
||||
splash.progress(text, pct)
|
||||
try:
|
||||
if action() is False:
|
||||
self._show_error(f"启动失败: {text}"); return
|
||||
except Exception as e:
|
||||
self._show_error(f"启动异常: {text}\n{e}"); return
|
||||
|
||||
splash.progress("启动完成", 1.0)
|
||||
await asyncio.sleep(0.3)
|
||||
|
||||
self.page.clean()
|
||||
self._build_main_ui()
|
||||
self.page.update()
|
||||
|
||||
self._data.pull_prices()
|
||||
self._data.refresh_orders()
|
||||
self._data.refresh_trades()
|
||||
self._refresh_ui()
|
||||
|
||||
event_bus.subscribe(MarketDataUpdate, self._on_market_data)
|
||||
event_bus.subscribe(EventMarketActiveSwitch, self._on_market_active)
|
||||
event_bus.subscribe(EventTradeTargetUpdate, lambda t: self._refresh_ui())
|
||||
threading.Thread(target=self._refresh_loop, daemon=True).start()
|
||||
|
||||
def _show_error(self, msg: str):
|
||||
self.page.clean()
|
||||
self.page.add(ft.Container(
|
||||
ft.Column([
|
||||
ft.Icon(ft.Icons.ERROR_OUTLINE, size=48, color=ft.Colors.RED),
|
||||
_text(msg, size=16),
|
||||
ft.ElevatedButton("重试", on_click=lambda e: self._retry()),
|
||||
], alignment=ft.MainAxisAlignment.CENTER, horizontal_alignment=ft.CrossAxisAlignment.CENTER),
|
||||
alignment=ft.Alignment.CENTER, expand=True,
|
||||
))
|
||||
self.page.update()
|
||||
|
||||
def _retry(self):
|
||||
self.page.clean()
|
||||
self._run_startup()
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# UI 构建
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
def _build_main_ui(self):
|
||||
self._sidebar_icon = _PanelIcon('sidebar', active=False,
|
||||
on_click=lambda e: self._toggle_drawer())
|
||||
self._overlay = self._drawer.build()
|
||||
|
||||
title = ft.Container(
|
||||
ft.Row([
|
||||
_text("网格策略持仓", bold=True, size=13),
|
||||
ft.Row([
|
||||
ft.IconButton(ft.Icons.REFRESH, tooltip="刷新", icon_size=18,
|
||||
on_click=lambda e: self._manual_refresh()),
|
||||
self._sidebar_icon,
|
||||
], spacing=0),
|
||||
], alignment=ft.MainAxisAlignment.SPACE_BETWEEN),
|
||||
padding=ft.Padding(10, 10, 10, 5),
|
||||
)
|
||||
|
||||
grid_body = ft.Container(content=self._grid.build(), expand=True,
|
||||
padding=ft.Padding(10, 0, 10, 10))
|
||||
|
||||
self.page.add(ft.Column([
|
||||
title, ft.Stack([grid_body, self._overlay], expand=True),
|
||||
], expand=True))
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# UI 刷新
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
def _refresh_ui(self):
|
||||
self._grid._rebuild()
|
||||
self._drawer.refresh_all()
|
||||
self.page.update()
|
||||
|
||||
def _manual_refresh(self):
|
||||
self._data.pull_prices()
|
||||
self._data.refresh_positions()
|
||||
self._data.refresh_orders()
|
||||
self._data.refresh_trades()
|
||||
self._refresh_ui()
|
||||
|
||||
def _toggle_drawer(self):
|
||||
self._drawer_open = not getattr(self, '_drawer_open', False)
|
||||
self._overlay.visible = self._drawer_open
|
||||
self._sidebar_icon.set_active(self._drawer_open)
|
||||
self.page.update()
|
||||
|
||||
def _hide_overlay(self):
|
||||
self._drawer_open = False
|
||||
self._overlay.visible = False
|
||||
self._sidebar_icon.set_active(False)
|
||||
self.page.update()
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 事件
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
def _on_market_data(self, data: dict):
|
||||
count = self._data.apply_tick(data)
|
||||
if not self._prices_loaded and count > 0:
|
||||
self._prices_loaded = True
|
||||
self._refresh_ui()
|
||||
|
||||
def _on_market_active(self, is_active: bool):
|
||||
pass
|
||||
|
||||
# ══════════════════════════════════════════════════════════
|
||||
# 后台刷新
|
||||
# ══════════════════════════════════════════════════════════
|
||||
|
||||
def _refresh_loop(self):
|
||||
cycle = 0
|
||||
while True:
|
||||
time.sleep(5)
|
||||
cycle += 1
|
||||
try:
|
||||
self._data.pull_prices()
|
||||
self._data.refresh_positions()
|
||||
if cycle % 6 == 0:
|
||||
self._data.refresh_orders()
|
||||
self._data.refresh_trades()
|
||||
self._refresh_ui()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# PanelIcon
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
class _PanelIcon(ft.Container):
|
||||
_SIZE = 22; _M = 3
|
||||
_C = {'bg': '#f0f0f0', 'off': '#b0b0b0', 'on': '#808080', 'active': '#0078d4'}
|
||||
|
||||
def __init__(self, kind: str, active: bool = True, on_click=None):
|
||||
self._kind = kind
|
||||
self._active = active
|
||||
super().__init__(content=self._rects(), width=self._SIZE, height=self._SIZE,
|
||||
bgcolor=self._C['bg'], border_radius=3, ink=True,
|
||||
on_click=on_click, padding=ft.Padding(self._M, self._M, self._M, self._M))
|
||||
|
||||
def _rects(self):
|
||||
o, n, a = self._C['off'], self._C['on'], self._C['active']
|
||||
bw, bh = 6, self._SIZE - self._M * 2 - 2
|
||||
if self._kind == 'sidebar':
|
||||
return ft.Row([
|
||||
ft.Container(width=bw, height=bh, bgcolor=n if self._active else o, border_radius=1),
|
||||
ft.Container(width=2),
|
||||
ft.Container(width=bw, height=bh, bgcolor=a if self._active else o, border_radius=1),
|
||||
], spacing=0)
|
||||
return ft.Column([
|
||||
ft.Container(width=bh, height=bw, bgcolor=n if self._active else o, border_radius=1),
|
||||
ft.Container(height=2),
|
||||
ft.Container(width=bh, height=bw, bgcolor=a if self._active else o, border_radius=1),
|
||||
], spacing=0)
|
||||
|
||||
def set_active(self, active: bool):
|
||||
self._active = active
|
||||
self.content = self._rects()
|
||||
|
||||
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
# 入口
|
||||
# ══════════════════════════════════════════════════════════════
|
||||
|
||||
def main(page: ft.Page):
|
||||
QmtApp(page)
|
||||
|
||||
def run():
|
||||
ft.app(target=main)
|
||||
@@ -1,7 +1,7 @@
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
from core.logger import LogLevel, LogData, PrintLog
|
||||
from core.sfgrid.sfgrid_ui import TradeTargetUI
|
||||
from core.ui.tkinter.sfgrid_view import TradeTargetUI
|
||||
|
||||
# 检测运行环境,决定使用真实或模拟 QMT
|
||||
def get_qmt_module():
|
||||
@@ -20,7 +20,7 @@ from core.eventbus import event_bus as eBus
|
||||
|
||||
|
||||
class MainWindow:
|
||||
def __init__(self, configLogLevel:str):
|
||||
def __init__(self, configLogLevel:str, progress=None):
|
||||
self.root = tk.Tk()
|
||||
self.root.title("神之一手 - 交易系统")
|
||||
self.root.geometry("1400x700")
|
||||
@@ -31,12 +31,12 @@ class MainWindow:
|
||||
self.strategy_frames = {}
|
||||
# 日志面板可见性标志
|
||||
self.log_visible = False
|
||||
self.create_ui()
|
||||
self.create_ui(progress)
|
||||
|
||||
eBus.subscribe(EventPrintLog, self.on_log_event)
|
||||
|
||||
|
||||
def create_ui(self):
|
||||
def create_ui(self, progress=None):
|
||||
"""创建UI界面"""
|
||||
# 主容器
|
||||
main_container = ttk.Frame(self.root)
|
||||
@@ -52,7 +52,7 @@ class MainWindow:
|
||||
|
||||
# 创建策略Frame
|
||||
strategy_names = ["网格"]
|
||||
self.create_strategy_frames(strategy_names)
|
||||
self.create_strategy_frames(strategy_names, progress)
|
||||
|
||||
# 创建全局日志面板(默认隐藏)
|
||||
self.create_global_log_panel(main_container)
|
||||
@@ -111,9 +111,9 @@ class MainWindow:
|
||||
for item in self.log_table.get_children():
|
||||
self.log_table.delete(item)
|
||||
|
||||
def create_strategy_frames(self, strategy_names):
|
||||
def create_strategy_frames(self, strategy_names, progress=None):
|
||||
"""创建各个策略的Frame"""
|
||||
frame = TradeTargetUI(self.content_container)
|
||||
frame = TradeTargetUI(self.content_container, progress=progress)
|
||||
self.strategy_frames[0] = frame
|
||||
|
||||
def show_strategy_frame(self, index):
|
||||
@@ -14,7 +14,9 @@ from core.sfgrid.sfgrid_strategy import SFGridStrategy
|
||||
|
||||
|
||||
class TradeTargetUI(ttk.Frame):
|
||||
def __init__(self, parent):
|
||||
def __init__(self, parent, progress=None):
|
||||
import time as _time
|
||||
_t0 = _time.time()
|
||||
super().__init__(parent)
|
||||
self.tradeTargetData:dict[int, SFGridTradeTarget] = {} # id->trade_target
|
||||
self.stockCodeIdMap:dict[str, int] = {}
|
||||
@@ -27,41 +29,48 @@ class TradeTargetUI(ttk.Frame):
|
||||
# 追踪最后点击的表格 (0=网格, 1=未分类)
|
||||
self._active_table = 0
|
||||
|
||||
if progress:
|
||||
progress("正在加载持仓数据...", 0.3)
|
||||
self.init_trade_target_pool()
|
||||
|
||||
# 市场监控数据
|
||||
self.marketData: dict[str, Any] = {} # 存储市场数据 {stock_code: {stock_name, last_price, time}}
|
||||
|
||||
# 市场监控窗口显示状态
|
||||
# 面板显示状态
|
||||
self.market_monitor_visible = True
|
||||
self.bottom_panel_visible = True
|
||||
# 市场活跃状态 + 刷新控制
|
||||
self._market_active = qmtv.isMarketActive # type: ignore
|
||||
self._refresh_event = threading.Event()
|
||||
self._refresh_cycle = 0 # 后台刷新周期计数
|
||||
self._prices_pulled_after_close = False # 收盘后是否已拉取过
|
||||
|
||||
if progress:
|
||||
progress("正在构建界面...", 0.6)
|
||||
# 创建界面
|
||||
self.create_ui()
|
||||
|
||||
if progress:
|
||||
progress("正在初始化策略...", 0.85)
|
||||
eBus.event_bus.subscribe(eBus.MarketDataUpdate, self.onMarketDataUpdated)
|
||||
eBus.event_bus.subscribe(eBus.EventMarketActiveSwitch, self._on_market_active_switch)
|
||||
|
||||
eBus.event_bus.subscribe(bus_events.EventTradeTargetUpdate, self.onStrategyUpdate)
|
||||
eBus.event_bus.subscribe(bus_events.EventTradeTargetDeleted, self.onTradeTargetDeleted)
|
||||
print(f'[计时] TradeTargetUI.__init__ 总计: {_time.time() - _t0:.2f}s')
|
||||
|
||||
|
||||
def init_trade_target_pool(self):
|
||||
# 一次性迁移: 已配置过的标的 (status >= 0) → 网格策略
|
||||
from core.sfgrid.model import STRATEGY_TYPE_GRID
|
||||
migrated = SFGridTradeTarget.update(strategy_type=STRATEGY_TYPE_GRID).where(
|
||||
SFGridTradeTarget.status >= 0
|
||||
).execute()
|
||||
if migrated:
|
||||
PrintLog(LogLevel.INFO, f'- [迁移] {migrated} 个已配置标的标记为网格策略')
|
||||
import time as _time
|
||||
_t = _time.time()
|
||||
|
||||
# 一次性从 QMT 获取全部持仓
|
||||
all_positions = qmtv.getAllPositions()
|
||||
PrintLog(LogLevel.INFO, f'- [持仓] 从 QMT 获取到 {len(all_positions)} 个持仓')
|
||||
print(f'[计时] └─ getAllPositions: {_time.time() - _t:.2f}s')
|
||||
|
||||
# 自动将 QMT 持仓导入到数据库(持仓但未在交易池中的标的)
|
||||
_t2 = _time.time()
|
||||
imported_count = 0
|
||||
for code, pos in all_positions.items():
|
||||
existing = SFGridTradeTarget.get_or_none(SFGridTradeTarget.stock_code == code)
|
||||
@@ -77,7 +86,6 @@ class TradeTargetUI(ttk.Frame):
|
||||
init_price=avg_price,
|
||||
grid_match_count=0,
|
||||
grid_total_profit=0.0,
|
||||
status=-1,
|
||||
enabled=False,
|
||||
grid_start_price=avg_price if avg_price > 0 else 10.0,
|
||||
grid_size=1.0,
|
||||
@@ -89,7 +97,9 @@ class TradeTargetUI(ttk.Frame):
|
||||
PrintLog(LogLevel.INFO, f'- [导入] QMT持仓 → 交易池: {code} {name} 持仓:{volume} 成本:{avg_price:.4f}')
|
||||
if imported_count:
|
||||
PrintLog(LogLevel.INFO, f'- [导入] 共新增 {imported_count} 个标的到交易池')
|
||||
print(f'[计时] └─ 持仓导入DB: {_time.time() - _t2:.2f}s ({imported_count} 个新增)')
|
||||
|
||||
_t3 = _time.time()
|
||||
results = SFGridTradeTarget.select()
|
||||
for temp in results:
|
||||
tradeTarget:SFGridTradeTarget = temp
|
||||
@@ -104,6 +114,8 @@ class TradeTargetUI(ttk.Frame):
|
||||
PrintLog(LogLevel.DEBUG, f'- [DEBUG] updateTradeTarget 前: {tradeTarget.stock_code} grid_index={tradeTarget.grid_index}, status={tradeTarget.status}, enabled={tradeTarget.enabled}')
|
||||
self.updateTradeTarget(tradeTarget, True) # 初始化的时候
|
||||
PrintLog(LogLevel.DEBUG, f'- [DEBUG] updateTradeTarget 后: {tradeTarget.stock_code} grid_index={tradeTarget.grid_index}')
|
||||
print(f'[计时] └─ 策略初始化: {_time.time() - _t3:.2f}s ({len(results)} 个标的)')
|
||||
print(f'[计时] └─ init_trade_target_pool 总计: {_time.time() - _t:.2f}s')
|
||||
|
||||
PrintLog(LogLevel.INFO, f'- [成功]交易标的信息初始化, 共 {len(self.tradeTargetData)} 个标的')
|
||||
|
||||
@@ -111,14 +123,14 @@ class TradeTargetUI(ttk.Frame):
|
||||
# 收集所有市场数据用于市场监控
|
||||
def onMarketDataUpdated(self, data):
|
||||
for stock_code, tickData in data.items():
|
||||
if stock_code in self.stockCodeIdMap:
|
||||
id:int = self.stockCodeIdMap[stock_code]
|
||||
# 统一去掉后缀用于内部查找
|
||||
plain_code = stock_code.split('.')[0] if '.' in stock_code else stock_code
|
||||
if plain_code in self.stockCodeIdMap:
|
||||
id:int = self.stockCodeIdMap[plain_code]
|
||||
self.targetMarketPrice[id] = tickData['lastPrice']
|
||||
tradeTarget = self.tradeTargetData[id]
|
||||
# timeStr = datetime.fromtimestamp(tickData['time']/1000)
|
||||
lastPrice = float("{:.3f}".format(tickData['lastPrice']))
|
||||
tradeTarget.market_price = lastPrice # type: ignore
|
||||
# PrintLog(LogLevel.INFO, f'|- 市价更新[{tradeTarget.targetName()}] - {timeStr.strftime("%H:%M:%S")} 市价更新: {lastPrice}======================{id}')
|
||||
self.updateTradeTarget(tradeTarget, False) # 市价更新
|
||||
else:
|
||||
# 非目标交易,发布市场数据更新事件用于市场监控
|
||||
@@ -141,6 +153,8 @@ class TradeTargetUI(ttk.Frame):
|
||||
def onStrategyUpdate(self, target: SFGridTradeTarget):
|
||||
id = target.get_id()
|
||||
self.tradeTargetData[id] = target
|
||||
# 唤醒 refresh_loop 立即刷新表格,统一由 refresh_table 处理
|
||||
self._refresh_event.set()
|
||||
|
||||
|
||||
# priceChange 用于控制是否对更新价格数据,进行交易判断
|
||||
@@ -170,16 +184,21 @@ class TradeTargetUI(ttk.Frame):
|
||||
# UI CREATE
|
||||
def create_ui(self):
|
||||
"""创建UI界面"""
|
||||
import time as _time
|
||||
_t = _time.time()
|
||||
|
||||
# 主框架(使用self作为父容器)
|
||||
main_frame = ttk.Frame(self)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
|
||||
|
||||
# 表格区域(左右布局:左侧=工具栏+持仓表格,右侧=Notebook标签页)
|
||||
self.create_tables_area(main_frame)
|
||||
print(f'[计时] └─ create_tables_area: {_time.time() - _t:.2f}s')
|
||||
|
||||
# 启动刷新线程
|
||||
self.refresh_thread = threading.Thread(target=self.refresh_loop, daemon=True)
|
||||
self.refresh_thread.start()
|
||||
print(f'[计时] └─ create_ui 总计: {_time.time() - _t:.2f}s')
|
||||
|
||||
|
||||
def _on_market_active_switch(self, is_active: bool):
|
||||
@@ -206,6 +225,13 @@ class TradeTargetUI(ttk.Frame):
|
||||
|
||||
self.after(0, self.refresh_table)
|
||||
self.after(0, self.populate_market_table)
|
||||
|
||||
# 每 6 个周期 (30s) 自动刷新委托/成交
|
||||
self._refresh_cycle += 1
|
||||
if self._refresh_cycle % 6 == 0:
|
||||
self.after(0, self._refresh_orders)
|
||||
self.after(0, self._refresh_trades)
|
||||
|
||||
self._refresh_event.wait(timeout=5)
|
||||
self._refresh_event.clear()
|
||||
|
||||
@@ -263,8 +289,16 @@ class TradeTargetUI(ttk.Frame):
|
||||
command=self.btnHandlerStopSelectedTrade, width=12).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(toolbar_frame, text="🛠 交易设置",
|
||||
command=self.btnHandlerTradeSettings, width=12).pack(side=tk.LEFT, padx=2)
|
||||
ttk.Button(toolbar_frame, text="▣ 边栏",
|
||||
command=self.btnHandlerToggleMarketMonitor, width=8).pack(side=tk.RIGHT, padx=2)
|
||||
|
||||
# 右上角 VSCode 风格图标按钮
|
||||
sidebar_btn = PanelIcon(toolbar_frame, 'sidebar', self.btnHandlerToggleMarketMonitor,
|
||||
active=self.market_monitor_visible)
|
||||
sidebar_btn.pack(side=tk.RIGHT, padx=1, pady=2)
|
||||
bottom_btn = PanelIcon(toolbar_frame, 'bottom', self._toggle_bottom_panel,
|
||||
active=self.bottom_panel_visible)
|
||||
bottom_btn.pack(side=tk.RIGHT, padx=1, pady=2)
|
||||
self._sidebar_icon = sidebar_btn
|
||||
self._bottom_icon = bottom_btn
|
||||
|
||||
# 上半部分: 网格策略持仓
|
||||
grid_frame = ttk.LabelFrame(left_frame, text="网格策略持仓", padding=5)
|
||||
@@ -272,9 +306,9 @@ class TradeTargetUI(ttk.Frame):
|
||||
self.create_grid_table(grid_frame)
|
||||
|
||||
# 下半部分: 未分类持仓
|
||||
unclassified_frame = ttk.LabelFrame(left_frame, text="未分类持仓", padding=5)
|
||||
unclassified_frame.pack(fill=tk.BOTH, expand=True, pady=(3, 0))
|
||||
self.create_unclassified_table(unclassified_frame)
|
||||
self.unclassified_frame = ttk.LabelFrame(left_frame, text="未分类持仓", padding=5)
|
||||
self.unclassified_frame.pack(fill=tk.BOTH, expand=True, pady=(3, 0))
|
||||
self.create_unclassified_table(self.unclassified_frame)
|
||||
|
||||
# 右侧: Notebook 标签页容器
|
||||
self.right_notebook = ttk.Notebook(tables_frame)
|
||||
@@ -297,17 +331,18 @@ class TradeTargetUI(ttk.Frame):
|
||||
|
||||
self.create_market_monitor_table(self.market_frame)
|
||||
|
||||
# Tab 2: 订单记录 (占位)
|
||||
model_tab = ttk.Frame(self.right_notebook)
|
||||
self.right_notebook.add(model_tab, text="订单记录")
|
||||
ttk.Label(model_tab, text="订单记录 - 待实现", font=('Arial', 12),
|
||||
foreground='gray').pack(expand=True)
|
||||
# Tab 2: 当日委托
|
||||
self.order_tab = ttk.Frame(self.right_notebook)
|
||||
self.right_notebook.add(self.order_tab, text="当日委托")
|
||||
self._create_order_table(self.order_tab)
|
||||
|
||||
# Tab 3: 成交记录 (占位)
|
||||
dataset_tab = ttk.Frame(self.right_notebook)
|
||||
self.right_notebook.add(dataset_tab, text="成交记录")
|
||||
ttk.Label(dataset_tab, text="成交记录 - 待实现", font=('Arial', 12),
|
||||
foreground='gray').pack(expand=True)
|
||||
# Tab 3: 当日成交
|
||||
self.trade_tab = ttk.Frame(self.right_notebook)
|
||||
self.right_notebook.add(self.trade_tab, text="当日成交")
|
||||
self._create_trade_table(self.trade_tab)
|
||||
|
||||
# Tab 切换时自动刷新
|
||||
self.right_notebook.bind("<<NotebookTabChanged>>", self._on_tab_changed)
|
||||
|
||||
|
||||
def create_grid_table(self, parent):
|
||||
@@ -411,6 +446,125 @@ class TradeTargetUI(ttk.Frame):
|
||||
# 填充初始数据
|
||||
self.populate_market_table()
|
||||
|
||||
# ---- 委托/成交 Tab 页 ----
|
||||
|
||||
_ORDER_STATUS_MAP = {
|
||||
48: '未报', 49: '待报', 50: '已报', 51: '已报待撤',
|
||||
52: '部成待撤', 53: '部撤', 54: '已撤', 55: '部成',
|
||||
56: '已成', 57: '废单',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _order_direction(ot: int) -> str:
|
||||
return '买' if ot in (23,) else '卖' if ot in (24,) else str(ot)
|
||||
|
||||
def _create_order_table(self, parent):
|
||||
"""创建当日委托表格"""
|
||||
toolbar = ttk.Frame(parent)
|
||||
toolbar.pack(fill=tk.X, pady=(0, 3))
|
||||
ttk.Button(toolbar, text="刷新", command=self._refresh_orders, width=6).pack(side=tk.RIGHT)
|
||||
ttk.Label(toolbar, text="当日委托", font=('', 10, 'bold')).pack(side=tk.LEFT)
|
||||
|
||||
cols = ("时间", "代码", "名称", "方向", "委托价", "委托量", "已成交", "均价", "状态")
|
||||
self.order_tree = ttk.Treeview(parent, columns=cols, show='headings', height=14)
|
||||
widths = {"时间": 70, "代码": 60, "名称": 70, "方向": 30, "委托价": 55, "委托量": 55, "已成交": 55, "均价": 55, "状态": 55}
|
||||
for c in cols:
|
||||
self.order_tree.heading(c, text=c)
|
||||
self.order_tree.column(c, width=widths.get(c, 60), anchor=tk.CENTER)
|
||||
|
||||
sb = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.order_tree.yview)
|
||||
self.order_tree.configure(yscrollcommand=sb.set)
|
||||
self.order_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
sb.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
def _create_trade_table(self, parent):
|
||||
"""创建当日成交表格"""
|
||||
toolbar = ttk.Frame(parent)
|
||||
toolbar.pack(fill=tk.X, pady=(0, 3))
|
||||
ttk.Button(toolbar, text="刷新", command=self._refresh_trades, width=6).pack(side=tk.RIGHT)
|
||||
ttk.Label(toolbar, text="当日成交", font=('', 10, 'bold')).pack(side=tk.LEFT)
|
||||
|
||||
cols = ("时间", "代码", "名称", "方向", "成交价", "成交量", "成交金额", "手续费")
|
||||
self.trade_tree = ttk.Treeview(parent, columns=cols, show='headings', height=14)
|
||||
widths = {"时间": 70, "代码": 60, "名称": 70, "方向": 30, "成交价": 55, "成交量": 55, "成交金额": 65, "手续费": 50}
|
||||
for c in cols:
|
||||
self.trade_tree.heading(c, text=c)
|
||||
self.trade_tree.column(c, width=widths.get(c, 60), anchor=tk.CENTER)
|
||||
|
||||
sb = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.trade_tree.yview)
|
||||
self.trade_tree.configure(yscrollcommand=sb.set)
|
||||
self.trade_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
sb.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
def _on_tab_changed(self, event):
|
||||
"""Tab 切换时自动刷新"""
|
||||
nb = self.right_notebook
|
||||
try:
|
||||
tab_id = nb.select()
|
||||
tab_text = nb.tab(tab_id, "text")
|
||||
if tab_text == "当日委托":
|
||||
self._refresh_orders()
|
||||
elif tab_text == "当日成交":
|
||||
self._refresh_trades()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _refresh_orders(self):
|
||||
"""从 QMT 读取当日委托并刷新表格"""
|
||||
from core.qmt import qmtv
|
||||
try:
|
||||
orders = qmtv.queryTodayOrders()
|
||||
except Exception:
|
||||
orders = []
|
||||
|
||||
for item in self.order_tree.get_children():
|
||||
self.order_tree.delete(item)
|
||||
|
||||
_CANCELED = {54, 57}
|
||||
for o in orders:
|
||||
if getattr(o, 'order_status', 0) in _CANCELED:
|
||||
continue
|
||||
code = getattr(o, 'stock_code', '')
|
||||
if '.' in code:
|
||||
code = code.split('.')[0]
|
||||
self.order_tree.insert('', tk.END, values=(
|
||||
str(getattr(o, 'order_time', '')),
|
||||
code,
|
||||
getattr(o, 'instrument_name', '') or code,
|
||||
self._order_direction(getattr(o, 'order_type', 0)),
|
||||
f"{getattr(o, 'price', 0):.3f}",
|
||||
getattr(o, 'order_volume', 0),
|
||||
getattr(o, 'traded_volume', 0),
|
||||
f"{getattr(o, 'traded_price', 0):.3f}" if getattr(o, 'traded_price', 0) > 0 else '-',
|
||||
self._ORDER_STATUS_MAP.get(getattr(o, 'order_status', 255), '未知'),
|
||||
))
|
||||
|
||||
def _refresh_trades(self):
|
||||
"""从 QMT 读取当日成交并刷新表格"""
|
||||
from core.qmt import qmtv
|
||||
try:
|
||||
trades = qmtv.queryTodayTrades()
|
||||
except Exception:
|
||||
trades = []
|
||||
|
||||
for item in self.trade_tree.get_children():
|
||||
self.trade_tree.delete(item)
|
||||
|
||||
for t in trades:
|
||||
code = getattr(t, 'stock_code', '')
|
||||
if '.' in code:
|
||||
code = code.split('.')[0]
|
||||
self.trade_tree.insert('', tk.END, values=(
|
||||
str(getattr(t, 'traded_time', '')),
|
||||
code,
|
||||
getattr(t, 'instrument_name', '') or code,
|
||||
self._order_direction(getattr(t, 'order_type', 0)),
|
||||
f"{getattr(t, 'traded_price', 0):.3f}",
|
||||
getattr(t, 'traded_volume', 0),
|
||||
f"{getattr(t, 'traded_amount', 0):.2f}",
|
||||
f"{getattr(t, 'commission', 0):.2f}",
|
||||
))
|
||||
|
||||
def populate_market_table(self):
|
||||
"""填充市场监控表格数据"""
|
||||
# 保存当前选中的项
|
||||
@@ -490,9 +644,9 @@ class TradeTargetUI(ttk.Frame):
|
||||
|
||||
def get_trade_enabled_indicator(self, target: SFGridTradeTarget) -> str:
|
||||
"""获取交易状态指示器"""
|
||||
if target.status == -1:
|
||||
if target.strategy_type == 0: # 未分类 = 未配置
|
||||
return "请做交易设置"
|
||||
elif target.status >= 0:
|
||||
else:
|
||||
if target.enabled:
|
||||
return "▶ 运行中"
|
||||
else:
|
||||
@@ -550,15 +704,16 @@ class TradeTargetUI(ttk.Frame):
|
||||
|
||||
def populate_grid_table(self):
|
||||
"""填充网格策略表格数据"""
|
||||
# 先清空所有行再重建,防止重复
|
||||
self.grid_table.delete(*self.grid_table.get_children())
|
||||
for id, target in self.tradeTargetData.items():
|
||||
from core.sfgrid.model import STRATEGY_TYPE_GRID
|
||||
if target.strategy_type != STRATEGY_TYPE_GRID: # type: ignore
|
||||
continue
|
||||
|
||||
grid_info = '-'
|
||||
if target.status >= 0:
|
||||
grid_idx = target.grid_index
|
||||
price_grid = target.getPriceGrid()
|
||||
grid_info = '-'
|
||||
if 0 <= grid_idx < len(price_grid):
|
||||
grid_info = f'{grid_idx}({price_grid[grid_idx]:.2f}元)'
|
||||
else:
|
||||
@@ -578,6 +733,8 @@ class TradeTargetUI(ttk.Frame):
|
||||
|
||||
def populate_unclassified_table(self):
|
||||
"""填充未分类持仓表格数据"""
|
||||
# 先清空所有行再重建,防止重复
|
||||
self.unclassified_table.delete(*self.unclassified_table.get_children())
|
||||
for id, target in self.tradeTargetData.items():
|
||||
from core.sfgrid.model import STRATEGY_TYPE_UNCLASSIFIED
|
||||
if target.strategy_type != STRATEGY_TYPE_UNCLASSIFIED: # type: ignore
|
||||
@@ -629,6 +786,18 @@ class TradeTargetUI(ttk.Frame):
|
||||
messagebox.showwarning("未选中", "请先选择一个交易标的")
|
||||
return None
|
||||
|
||||
def get_selected_targets(self):
|
||||
"""获取所有选中的交易标的(支持多选)"""
|
||||
targets = []
|
||||
for table in (self.grid_table, self.unclassified_table):
|
||||
for item in table.selection():
|
||||
values = table.item(item)['values']
|
||||
if values:
|
||||
target = self.tradeTargetData.get(int(values[0]))
|
||||
if target:
|
||||
targets.append(target)
|
||||
return targets
|
||||
|
||||
def refresh_table(self):
|
||||
"""刷新表格数据(纯UI操作,在主线程执行)"""
|
||||
# 刷新网格策略表格
|
||||
@@ -703,25 +872,25 @@ class TradeTargetUI(ttk.Frame):
|
||||
ttk.Label(info_frame, text=f"股票名称: {target.stock_name}").grid(row=0, column=1, sticky=tk.W, padx=(20, 0), pady=2)
|
||||
|
||||
# 建仓状态(可变更)
|
||||
status_text = "已建仓" if target.status >= 1 else "未建仓"
|
||||
status_color = "green" if target.status >= 1 else "orange"
|
||||
status_text = "已建仓" if target.grid_index > 0 else "未建仓"
|
||||
status_color = "green" if target.grid_index > 0 else "orange"
|
||||
status_label = ttk.Label(info_frame, text=f"建仓状态: {status_text}", foreground=status_color)
|
||||
status_label.grid(row=1, column=0, sticky=tk.W, pady=2)
|
||||
|
||||
def toggle_position_status():
|
||||
new_status = 0 if target.status >= 1 else 1
|
||||
setattr(target, 'status', new_status)
|
||||
new_idx = 0 if target.grid_index > 0 else 1
|
||||
setattr(target, 'grid_index', new_idx)
|
||||
target.save()
|
||||
new_text = "已建仓" if new_status >= 1 else "未建仓"
|
||||
new_color = "green" if new_status >= 1 else "orange"
|
||||
new_text = "已建仓" if new_idx > 0 else "未建仓"
|
||||
new_color = "green" if new_idx > 0 else "orange"
|
||||
status_label.config(text=f"建仓状态: {new_text}", foreground=new_color)
|
||||
toggle_btn.config(text="标记为未建仓" if new_status >= 1 else "标记为已建仓")
|
||||
toggle_btn.config(text="标记为未建仓" if new_idx > 0 else "标记为已建仓")
|
||||
self.updateTradeTarget(target, False)
|
||||
PrintLog(LogLevel.INFO, f"建仓状态变更: {target.stock_code} → {new_text}")
|
||||
|
||||
toggle_btn = ttk.Button(
|
||||
info_frame,
|
||||
text="标记为未建仓" if target.status >= 1 else "标记为已建仓",
|
||||
text="标记为未建仓" if target.grid_index > 0 else "标记为已建仓",
|
||||
command=toggle_position_status
|
||||
)
|
||||
toggle_btn.grid(row=1, column=1, sticky=tk.W, padx=(20, 0), pady=2)
|
||||
@@ -826,15 +995,6 @@ class TradeTargetUI(ttk.Frame):
|
||||
ttk.Label(info_frame, text=f"股票代码: {target.stock_code}").grid(row=0, column=0, sticky=tk.W, pady=2)
|
||||
ttk.Label(info_frame, text=f"股票名称: {target.stock_name}").grid(row=0, column=1, sticky=tk.W, padx=(20, 0), pady=2)
|
||||
|
||||
# 建仓状态选择
|
||||
ttk.Label(info_frame, text="建仓状态:", width=12).grid(row=1, column=0, sticky=tk.W, pady=2)
|
||||
position_status_var = tk.StringVar(value="未建仓" if target.status < 1 else "已建仓")
|
||||
position_status_combo = ttk.Combobox(
|
||||
info_frame, textvariable=position_status_var,
|
||||
values=["未建仓", "已建仓"], state="readonly", width=10
|
||||
)
|
||||
position_status_combo.grid(row=1, column=1, sticky=tk.W, padx=(20, 0), pady=2)
|
||||
|
||||
ttk.Label(info_frame, text=f"状态: 新标的(可配置模式)").grid(row=2, column=0, columnspan=2, sticky=tk.W, pady=2)
|
||||
|
||||
# 创建网格配置框架
|
||||
@@ -977,8 +1137,6 @@ class TradeTargetUI(ttk.Frame):
|
||||
setattr(target, 'grid_volume', grid_volume)
|
||||
setattr(target, 'grid_upper_count', grid_upper_count)
|
||||
setattr(target, 'grid_lower_count', grid_lower_count)
|
||||
# 建仓状态: "已建仓" → 1, "未建仓" → 0
|
||||
setattr(target, 'status', 1 if position_status_var.get() == "已建仓" else 0)
|
||||
# grid_index 设为基准价在网格中的位置 (grid_upper_count)
|
||||
setattr(target, 'grid_index', grid_upper_count)
|
||||
# 自动标记为网格策略
|
||||
@@ -1068,7 +1226,6 @@ class TradeTargetUI(ttk.Frame):
|
||||
current_position=0 if pos is None else int(pos.volume),
|
||||
grid_index=gridIndex,
|
||||
init_price=0.0,
|
||||
status=-1,
|
||||
strategy_type=0 # 默认为未分类
|
||||
)
|
||||
# 更新标的池
|
||||
@@ -1094,6 +1251,19 @@ class TradeTargetUI(ttk.Frame):
|
||||
else:
|
||||
self.right_notebook.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=(5, 0))
|
||||
self.market_monitor_visible = True
|
||||
if hasattr(self, '_sidebar_icon'):
|
||||
self._sidebar_icon.set_active(self.market_monitor_visible)
|
||||
|
||||
def _toggle_bottom_panel(self):
|
||||
"""切换底部未分类持仓面板显示/隐藏"""
|
||||
if self.bottom_panel_visible:
|
||||
self.unclassified_frame.pack_forget()
|
||||
self.bottom_panel_visible = False
|
||||
else:
|
||||
self.unclassified_frame.pack(fill=tk.BOTH, expand=True, pady=(3, 0))
|
||||
self.bottom_panel_visible = True
|
||||
if hasattr(self, '_bottom_icon'):
|
||||
self._bottom_icon.set_active(self.bottom_panel_visible)
|
||||
|
||||
def btnHandlerTradeSettings(self):
|
||||
"""网格配置功能"""
|
||||
@@ -1109,71 +1279,61 @@ class TradeTargetUI(ttk.Frame):
|
||||
self.create_grid_view_window(target)
|
||||
|
||||
def btnHandlerStartSelectedTrade(self):
|
||||
"""启动选中的交易"""
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
|
||||
"""启动选中的交易(支持多选)"""
|
||||
from core.sfgrid.model import STRATEGY_TYPE_GRID
|
||||
if target.strategy_type != STRATEGY_TYPE_GRID: # type: ignore
|
||||
messagebox.showinfo("提示", "该标的不属于网格策略,请先转为网格策略后再启动交易。")
|
||||
targets = self.get_selected_targets()
|
||||
if not targets:
|
||||
messagebox.showwarning("未选中", "请先选择交易标的")
|
||||
return
|
||||
|
||||
if target.status < 0:
|
||||
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 未配置交易参数, 请做交易设置。")
|
||||
# 过滤:只处理已暂停的网格策略标的
|
||||
to_start = [t for t in targets
|
||||
if t.strategy_type == STRATEGY_TYPE_GRID and not t.enabled] # type: ignore
|
||||
if not to_start:
|
||||
messagebox.showinfo("提示", "选中的标的中没有可启动的(已在运行中或非网格策略)")
|
||||
return
|
||||
|
||||
if target.enabled: # type: ignore
|
||||
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经在运行中")
|
||||
# 确认对话框
|
||||
names = "\n".join(f"{t.stock_code} {t.stock_name}" for t in to_start)
|
||||
result = messagebox.askyesno("确认启动", f"确定要启动以下 {len(to_start)} 个交易标的吗?\n\n{names}")
|
||||
if not result:
|
||||
return
|
||||
|
||||
result = messagebox.askyesno(
|
||||
"确认启动",
|
||||
f"确定要启动以下交易标的吗?\n\n"
|
||||
f"股票代码: {target.stock_code}\n"
|
||||
f"股票名称: {target.stock_name}"
|
||||
)
|
||||
|
||||
if result:
|
||||
for target in to_start:
|
||||
PrintLog(LogLevel.INFO, f'启动标的交易 {target.targetName()}')
|
||||
target.enabled = True # type: ignore
|
||||
|
||||
id = target.get_id()
|
||||
if id in self.strategy_ctrl:
|
||||
tradeController: SFGridStrategy = self.strategy_ctrl[target.get_id()]
|
||||
tradeTarget = tradeController.enabledTrading(True)
|
||||
self.tradeTargetData[id] = tradeTarget
|
||||
ctrl = self.strategy_ctrl.get(target.get_id())
|
||||
if ctrl:
|
||||
ctrl.enabledTrading(True)
|
||||
else:
|
||||
PrintLog(LogLevel.INFO, f"\t创建标的交易控制器 {target.targetName()}")
|
||||
|
||||
def btnHandlerStopSelectedTrade(self):
|
||||
"""暂停选中的交易"""
|
||||
target = self.get_selected_target()
|
||||
if not target:
|
||||
return
|
||||
|
||||
"""暂停选中的交易(支持多选)"""
|
||||
from core.sfgrid.model import STRATEGY_TYPE_GRID
|
||||
if target.strategy_type != STRATEGY_TYPE_GRID: # type: ignore
|
||||
messagebox.showinfo("提示", "该标的不属于网格策略。")
|
||||
targets = self.get_selected_targets()
|
||||
if not targets:
|
||||
messagebox.showwarning("未选中", "请先选择交易标的")
|
||||
return
|
||||
|
||||
if not target.enabled: # type: ignore
|
||||
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经是暂停状态")
|
||||
# 过滤:只处理已启用的网格策略标的
|
||||
to_stop = [t for t in targets
|
||||
if t.strategy_type == STRATEGY_TYPE_GRID and t.enabled] # type: ignore
|
||||
if not to_stop:
|
||||
messagebox.showinfo("提示", "选中的标的中没有可暂停的(已是暂停状态或非网格策略)")
|
||||
return
|
||||
|
||||
result = messagebox.askyesno(
|
||||
"确认暂停",
|
||||
f"确定要暂停以下交易标的吗?\n\n"
|
||||
f"股票代码: {target.stock_code}\n"
|
||||
f"股票名称: {target.stock_name}"
|
||||
)
|
||||
# 确认对话框
|
||||
names = "\n".join(f"{t.stock_code} {t.stock_name}" for t in to_stop)
|
||||
result = messagebox.askyesno("确认暂停", f"确定要暂停以下 {len(to_stop)} 个交易标的吗?\n\n{names}")
|
||||
if not result:
|
||||
return
|
||||
|
||||
if result:
|
||||
for target in to_stop:
|
||||
PrintLog(LogLevel.INFO, f'暂停标的交易 {target.targetName()}')
|
||||
id = target.get_id()
|
||||
if id in self.strategy_ctrl:
|
||||
tradeController: SFGridStrategy = self.strategy_ctrl[target.get_id()]
|
||||
tradeController.enabledTrading(False)
|
||||
ctrl = self.strategy_ctrl.get(target.get_id())
|
||||
if ctrl:
|
||||
ctrl.enabledTrading(False)
|
||||
else:
|
||||
print(f"标的交易控制器不存在 {target.stock_code} {target.stock_name}\n")
|
||||
|
||||
@@ -1288,3 +1448,86 @@ class TradeTargetUI(ttk.Frame):
|
||||
PrintLog(LogLevel.INFO, f"监控价格已更新为: {new_price}")
|
||||
except ValueError:
|
||||
messagebox.showerror("错误", "请输入有效的数字")
|
||||
|
||||
|
||||
class PanelIcon(tk.Canvas):
|
||||
"""VSCode 风格面板切换图标,颜色自适应系统主题"""
|
||||
|
||||
_SIZE = 22
|
||||
|
||||
def __init__(self, parent, kind: str, command, active: bool = True):
|
||||
super().__init__(parent, width=self._SIZE, height=self._SIZE,
|
||||
bd=0, highlightthickness=0, cursor='hand2')
|
||||
self._command = command
|
||||
self._kind = kind
|
||||
self._active = active
|
||||
self.bind('<Button-1>', self._on_click)
|
||||
self.bind('<Enter>', self._on_enter)
|
||||
self.bind('<Leave>', self._on_leave)
|
||||
self.bind('<Map>', lambda e: self._update_colors())
|
||||
self._update_colors()
|
||||
|
||||
def _update_colors(self):
|
||||
"""从父容器读取实际背景色,推导图标配色"""
|
||||
try:
|
||||
bg = self.master.cget('background') or 'SystemButtonFace'
|
||||
# 转 RGB 判断明暗
|
||||
rgb = self.winfo_rgb(bg) if bg.startswith('#') else None
|
||||
if rgb:
|
||||
r, g, b = rgb[0] >> 8, rgb[1] >> 8, rgb[2] >> 8
|
||||
bright = (r * 299 + g * 587 + b * 114) / 1000
|
||||
else:
|
||||
bright = 240 # 系统颜色默认当作亮色
|
||||
except Exception:
|
||||
bright = 240
|
||||
|
||||
if bright > 128:
|
||||
# 亮色主题
|
||||
self._BG = '#e8e8e8' if bright < 220 else '#f0f0f0'
|
||||
self._HOVER = '#d4d4d4'
|
||||
self._OFF = '#b0b0b0'
|
||||
self._ON = '#808080'
|
||||
self._ACTIVE = '#0078d4' # Windows 蓝色
|
||||
else:
|
||||
# 暗色主题
|
||||
self._BG = '#3c3c3c'
|
||||
self._HOVER = '#505050'
|
||||
self._OFF = '#6a6a6a'
|
||||
self._ON = '#a0a0a0'
|
||||
self._ACTIVE = '#ffffff'
|
||||
|
||||
self.configure(bg=self._BG)
|
||||
self._draw()
|
||||
|
||||
def _draw(self):
|
||||
self.delete('all')
|
||||
m = 3
|
||||
s = self._SIZE - m * 2
|
||||
off = self._OFF
|
||||
on = self._ON
|
||||
|
||||
if self._kind == 'sidebar':
|
||||
# 左右分栏图标,右边面板高亮
|
||||
l = self._ON if self._active else off
|
||||
r = self._ACTIVE if self._active else off
|
||||
self.create_rectangle(m + 1, m + 1, m + 6, m + s - 2, fill=l, outline='', width=0)
|
||||
self.create_rectangle(m + 8, m + 1, m + 13, m + s - 2, fill=r, outline='', width=0)
|
||||
else:
|
||||
# 上下分栏图标,下面板高亮
|
||||
t = self._ON if self._active else off
|
||||
b = self._ACTIVE if self._active else off
|
||||
self.create_rectangle(m + 1, m + 1, m + s - 2, m + 6, fill=t, outline='', width=0)
|
||||
self.create_rectangle(m + 1, m + 8, m + s - 2, m + 13, fill=b, outline='', width=0)
|
||||
|
||||
def set_active(self, active: bool):
|
||||
self._active = active
|
||||
self._draw()
|
||||
|
||||
def _on_click(self, event):
|
||||
self._command()
|
||||
|
||||
def _on_enter(self, event):
|
||||
self.configure(bg=self._HOVER)
|
||||
|
||||
def _on_leave(self, event):
|
||||
self.configure(bg=self._BG)
|
||||
@@ -0,0 +1,112 @@
|
||||
"""
|
||||
启动进度窗口 — 无边框小窗口,负责整个初始化流程。
|
||||
"""
|
||||
import time
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
|
||||
|
||||
class SplashWindow:
|
||||
"""初始化进度窗口,所有者启动逻辑"""
|
||||
|
||||
def __init__(self):
|
||||
self.root = tk.Tk()
|
||||
self.root.title("神之一手")
|
||||
self.root.geometry("380x120")
|
||||
self.root.resizable(False, False)
|
||||
self.root.overrideredirect(True)
|
||||
|
||||
self.root.update_idletasks()
|
||||
sw = self.root.winfo_screenwidth()
|
||||
sh = self.root.winfo_screenheight()
|
||||
w, h = 380, 120
|
||||
self.root.geometry(f"{w}x{h}+{(sw - w) // 2}+{(sh - h) // 2}")
|
||||
|
||||
frame = ttk.Frame(self.root, padding=20)
|
||||
frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
ttk.Label(frame, text="神之一手", font=('Microsoft YaHei', 14, 'bold')).pack(pady=(0, 5))
|
||||
self._status = ttk.Label(frame, text="正在初始化...", font=('Microsoft YaHei', 9))
|
||||
self._status.pack(pady=(0, 10))
|
||||
|
||||
self._bar = ttk.Progressbar(frame, mode='determinate', length=340)
|
||||
self._bar.pack()
|
||||
self.root.update()
|
||||
|
||||
def progress(self, text: str, pct: float):
|
||||
self._status.configure(text=text)
|
||||
self._bar.configure(value=pct)
|
||||
self.root.update()
|
||||
|
||||
def _destroy(self):
|
||||
self.root.destroy()
|
||||
|
||||
def run(self):
|
||||
"""执行完整启动流程,成功返回主窗口,失败返回 None"""
|
||||
from core.qmt_real import RealQmtV, qmtv as selected_qmtv
|
||||
|
||||
while True:
|
||||
_t_total = time.time()
|
||||
|
||||
# 步骤1: 探测 QMT 环境
|
||||
self.progress("正在检查 QMT 环境...", 10)
|
||||
_t = time.time()
|
||||
try:
|
||||
discovered = RealQmtV._discover_qmt_port()
|
||||
except Exception:
|
||||
discovered = 0
|
||||
print(f'[计时] 步骤1-探测QMT环境: {time.time() - _t:.2f}s')
|
||||
|
||||
if not discovered:
|
||||
self._destroy()
|
||||
messagebox.showerror(
|
||||
"启动失败",
|
||||
"未能自动探测到 QMT 环境。\n\n"
|
||||
"请确认:\n"
|
||||
"1. 极简QMT(GJQMT)已启动并登录\n"
|
||||
"2. XtMiniQmt.exe 和 miniquote.exe 进程在运行"
|
||||
)
|
||||
return None
|
||||
|
||||
# 步骤2: 初始化交易器
|
||||
self.progress("正在初始化交易器...", 35)
|
||||
_t = time.time()
|
||||
selected_qmtv.init_qmtv()
|
||||
print(f'[计时] 步骤2-初始化交易器: {time.time() - _t:.2f}s')
|
||||
|
||||
# 步骤3: 连接 QMT
|
||||
self.progress("正在连接 QMT...", 55)
|
||||
_t = time.time()
|
||||
connected = selected_qmtv.connect()
|
||||
print(f'[计时] 步骤3-连接QMT: {time.time() - _t:.2f}s')
|
||||
|
||||
if not connected:
|
||||
self._destroy()
|
||||
option = messagebox.askokcancel(
|
||||
"连接失败",
|
||||
"QMT 连接失败。\n\n"
|
||||
"请确认极简QMT 已启动并登录交易账号。\n"
|
||||
"点击「确定」重试,或「取消」退出。"
|
||||
)
|
||||
if not option:
|
||||
return None
|
||||
# 重试:重新创建进度窗口
|
||||
self.__init__()
|
||||
continue
|
||||
|
||||
# 步骤4: 加载主界面
|
||||
self.progress("正在加载持仓与策略...", 75)
|
||||
_t = time.time()
|
||||
from core.ui.tkinter.main_window import MainWindow
|
||||
window = MainWindow('INFO', progress=lambda t, p: self.progress(t, 75 + p * 0.2))
|
||||
print(f'[计时] 步骤4-主界面加载: {time.time() - _t:.2f}s')
|
||||
|
||||
window.root.update()
|
||||
|
||||
# 步骤5: 完成
|
||||
self.progress("启动完成", 100)
|
||||
self.root.update()
|
||||
self.root.after(300, self._destroy)
|
||||
print(f'[计时] 总启动耗时: {time.time() - _t_total:.2f}s')
|
||||
|
||||
return window
|
||||
@@ -0,0 +1,98 @@
|
||||
# 统一网格逻辑
|
||||
|
||||
## 核心规则
|
||||
|
||||
对任意 `grid_index`,两个方向各挂一单:
|
||||
|
||||
| 方向 | 条件 | 价格 | 含义 |
|
||||
|------|------|------|------|
|
||||
| 卖出(上移) | `grid_index > 0` | `grid[grid_index - 1]` | 涨回到上一格时卖出获利 |
|
||||
| 买入(下移) | `grid_index < len(grid)-1` | `grid[grid_index + 1]` | 跌到下一格时补仓 |
|
||||
|
||||
不需要"建仓"概念,`grid_index=0` 自然表示空仓。
|
||||
|
||||
## 流程图
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START["refreshGridOrder()"] --> GUARD{"isMarketActive AND enabled ?"}
|
||||
GUARD -->|No| EXIT0["跳过不下单"]
|
||||
GUARD -->|Yes| QUERY["查询未成交订单<br/>queryPendingOrder()"]
|
||||
|
||||
QUERY --> IDX["currentIdx = grid_index"]
|
||||
|
||||
IDX --> SELL{"currentIdx > 0 ?"}
|
||||
|
||||
SELL -->|"No<br/>(空仓,无持仓可卖)"| BUY
|
||||
|
||||
SELL -->|"Yes"| SELL_IDX["sellIdx = currentIdx - 1<br/>卖价 = grid[sellIdx]"]
|
||||
SELL_IDX --> SELL_EXIST{"已有同 remark 卖单?"}
|
||||
SELL_EXIST -->|No| SELL_CHECK{"卖价 > 涨停价 ?"}
|
||||
SELL_CHECK -->|Yes| SELL_SKIP["跳过(超出涨停)"]
|
||||
SELL_CHECK -->|No| SELL_PLACE["挂卖出单<br/>orderGrid[sellIdx] = seq"]
|
||||
SELL_EXIST -->|Yes| SELL_DUP["跳过(已挂单)"]
|
||||
|
||||
SELL_SKIP --> BUY
|
||||
SELL_PLACE --> BUY
|
||||
SELL_DUP --> BUY
|
||||
|
||||
BUY{"currentIdx < len(grid)-1 ?"}
|
||||
|
||||
BUY -->|"No<br/>(已到最低价)"| EXIT["结束"]
|
||||
|
||||
BUY -->|"Yes"| BUY_IDX["buyIdx = currentIdx + 1<br/>买价 = grid[buyIdx]"]
|
||||
BUY_IDX --> BUY_EXIST{"已有同 remark 买单?"}
|
||||
BUY_EXIST -->|No| BUY_CHECK{"买价 < 跌停价 ?"}
|
||||
BUY_CHECK -->|Yes| BUY_SKIP["跳过(低于跌停)"]
|
||||
BUY_CHECK -->|No| BUY_PLACE["挂买入单<br/>orderGrid[buyIdx] = seq"]
|
||||
BUY_EXIST -->|Yes| BUY_DUP["跳过(已挂单)"]
|
||||
|
||||
BUY_SKIP --> EXIT
|
||||
BUY_PLACE --> EXIT
|
||||
BUY_DUP --> EXIT
|
||||
```
|
||||
|
||||
## 三种典型状态
|
||||
|
||||
```
|
||||
grid = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
|
||||
↑ ↑ ↑ ↑
|
||||
0 1 2 3 ...
|
||||
|
||||
|
||||
grid_index=0(空仓):
|
||||
┌────┬────┬────┬────┐
|
||||
│ 11 │ 10 │ 9 │ 8 │ ...
|
||||
└────┴────┴────┴────┘
|
||||
sell=无 buy=10 ← 第一笔买单
|
||||
|
||||
|
||||
grid_index=1(持1份@10元):
|
||||
┌────┬────┬────┬────┐
|
||||
│ 11 │ 10 │ 9 │ 8 │ ...
|
||||
└────┴────┴────┴────┘
|
||||
sell=11 → buy=9 →
|
||||
|
||||
|
||||
grid_index=3(持3份@8,9,10元):
|
||||
┌────┬────┬────┬────┐
|
||||
│ 11 │ 10 │ 9 │ 8 │ ...
|
||||
└────┴────┴────┴────┘
|
||||
↑ sell=9 buy=7 →
|
||||
当前位置=3
|
||||
|
||||
|
||||
成交后处理(onOrderTrade):
|
||||
卖单成交 gridIdx < currentIdx → grid_index -= 1(上移,赚差价)
|
||||
买单成交 gridIdx > currentIdx → grid_index += 1(下移,补仓)
|
||||
然后 refreshGridOrder → 在新位置重新挂单
|
||||
```
|
||||
|
||||
## 和之前的区别
|
||||
|
||||
| | 之前 | 之后 |
|
||||
|---|---|---|
|
||||
| 分支数 | 2 个(status=0 / status=1) | 1 个(统一网格逻辑) |
|
||||
| 空仓第一笔 | INIT 单 @ grid[0]=11 | 普通买单 @ grid[1]=10 |
|
||||
| grid[0]=11 的用途 | 建仓买入 | 永远只卖不买 |
|
||||
| 状态字段 | status + grid_index | 仅 grid_index |
|
||||
+19
-187
@@ -1,191 +1,23 @@
|
||||
# coding:utf-8
|
||||
import os
|
||||
"""
|
||||
启动入口 — 自动探测 QMT 环境。
|
||||
默认使用 Tkinter UI,使用 --flet 参数切换到 Flet (Flutter) UI。
|
||||
"""
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from tkinter import ttk, filedialog, messagebox
|
||||
import configparser
|
||||
import config as sdConstants
|
||||
|
||||
class ConfigWindow:
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
self.root.title("系统配置")
|
||||
self.root.geometry("500x300")
|
||||
self.root.resizable(False, False)
|
||||
|
||||
# 居中显示
|
||||
self.root.withdraw() # 先隐藏窗口
|
||||
self.root.update_idletasks()
|
||||
x = (self.root.winfo_screenwidth() // 2) - (500 // 2)
|
||||
y = (self.root.winfo_screenheight() // 2) - (300 // 2)
|
||||
self.root.geometry(f"500x300+{x}+{y}")
|
||||
self.root.deiconify() # 再显示窗口
|
||||
|
||||
self.miniQMTPath = tk.StringVar()
|
||||
self.account_no = tk.StringVar()
|
||||
|
||||
self.create_widgets()
|
||||
|
||||
def create_widgets(self):
|
||||
# 创建主框架
|
||||
main_frame = ttk.Frame(self.root, padding="20")
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
|
||||
# miniQMT路径配置
|
||||
path_frame = ttk.Frame(main_frame)
|
||||
path_frame.pack(fill=tk.X, pady=5)
|
||||
|
||||
path_label = ttk.Label(path_frame, text="miniQMT路径:")
|
||||
path_label.pack(side=tk.LEFT)
|
||||
|
||||
path_entry = ttk.Entry(path_frame, textvariable=self.miniQMTPath, width=40)
|
||||
path_entry.pack(side=tk.LEFT, padx=(10, 5), fill=tk.X, expand=True)
|
||||
|
||||
browse_btn = ttk.Button(path_frame, text="浏览", command=self.browse_folder)
|
||||
browse_btn.pack(side=tk.LEFT)
|
||||
|
||||
# 资金账号配置
|
||||
account_frame = ttk.Frame(main_frame)
|
||||
account_frame.pack(fill=tk.X, pady=5)
|
||||
|
||||
account_label = ttk.Label(account_frame, text="资金账号:")
|
||||
account_label.pack(side=tk.LEFT)
|
||||
|
||||
account_entry = ttk.Entry(account_frame, textvariable=self.account_no, width=40)
|
||||
account_entry.pack(side=tk.LEFT, padx=(10, 0))
|
||||
|
||||
# 模拟模式复选框
|
||||
self.use_simulated = tk.BooleanVar(value=False)
|
||||
simulated_check = ttk.Checkbutton(
|
||||
main_frame,
|
||||
text="使用模拟交易模式(无需真实 QMT 连接)",
|
||||
variable=self.use_simulated
|
||||
)
|
||||
simulated_check.pack(fill=tk.X, pady=5)
|
||||
|
||||
# 说明文本
|
||||
info_label = ttk.Label(
|
||||
main_frame,
|
||||
text="请配置miniQMT的userdata_mini路径和资金账号\n路径示例: D:/Programs/DTQMT/userdata_mini",
|
||||
foreground="gray"
|
||||
)
|
||||
info_label.pack(pady=10)
|
||||
|
||||
# 按钮框架
|
||||
button_frame = ttk.Frame(main_frame)
|
||||
button_frame.pack(fill=tk.X, pady=10)
|
||||
|
||||
save_btn = ttk.Button(button_frame, text="保存配置", command=self.save_config)
|
||||
save_btn.pack(side=tk.RIGHT)
|
||||
|
||||
cancel_btn = ttk.Button(button_frame, text="取消", command=self.root.destroy)
|
||||
cancel_btn.pack(side=tk.RIGHT, padx=(0, 10))
|
||||
|
||||
def browse_folder(self):
|
||||
folder_selected = filedialog.askdirectory()
|
||||
if folder_selected:
|
||||
self.miniQMTPath.set(folder_selected)
|
||||
|
||||
def save_config(self):
|
||||
mini_qmt_path = self.miniQMTPath.get().strip()
|
||||
account_number = self.account_no.get().strip()
|
||||
|
||||
# 检查miniQMT路径
|
||||
if not mini_qmt_path:
|
||||
messagebox.showerror("错误", "请选择miniQMT路径")
|
||||
return
|
||||
|
||||
if not os.path.exists(mini_qmt_path):
|
||||
messagebox.showerror("错误", "miniQMT路径不存在")
|
||||
return
|
||||
|
||||
# 检查账号
|
||||
if not account_number:
|
||||
messagebox.showerror("错误", "请输入资金账号")
|
||||
return
|
||||
|
||||
# 保存配置
|
||||
config = configparser.ConfigParser()
|
||||
config['config'] = {
|
||||
'miniQMTPath': mini_qmt_path.replace('\\', '/'),
|
||||
'account_no': account_number,
|
||||
'use_simulated_qmt': str(self.use_simulated.get()),
|
||||
'log_level': 'INFO'
|
||||
}
|
||||
|
||||
config_path = sdConstants.get_config_path()
|
||||
try:
|
||||
with open(config_path, 'w') as configfile:
|
||||
config.write(configfile)
|
||||
messagebox.showinfo("成功", "配置已保存")
|
||||
self.root.destroy()
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"保存配置失败: {str(e)}")
|
||||
|
||||
def check_and_create_config():
|
||||
"""检查配置文件,如果不存在则打开配置窗口"""
|
||||
root = tk.Tk()
|
||||
config_window = ConfigWindow(root)
|
||||
root.mainloop()
|
||||
|
||||
def resolve_simulated_mode() -> bool:
|
||||
"""确定是否使用模拟模式(CLI > 配置文件 > 默认 real)"""
|
||||
if '--simulated' in sys.argv:
|
||||
print('[配置] 命令行指定: 模拟交易模式')
|
||||
return True
|
||||
|
||||
if sdConstants.exist_config():
|
||||
sdConstants.initConfig()
|
||||
if sdConstants.use_simulated_qmt:
|
||||
print('[配置] 配置文件指定: 模拟交易模式')
|
||||
return True
|
||||
|
||||
print('[配置] 默认: 真实交易模式')
|
||||
return False
|
||||
|
||||
|
||||
def initialize_system():
|
||||
"""初始化系统"""
|
||||
simulated = resolve_simulated_mode()
|
||||
sdConstants.use_simulated_qmt = simulated
|
||||
|
||||
try:
|
||||
if simulated:
|
||||
from core.qmt_dummy import qmtv as selected_qmtv
|
||||
print("[模拟模式] 使用模拟交易器")
|
||||
sdConstants.miniQMTPath = '/dummy/path'
|
||||
sdConstants.account_no = 'DUMMY_ACCOUNT'
|
||||
sdConstants.log_level = 'INFO'
|
||||
selected_qmtv.init_qmtv()
|
||||
selected_qmtv.connect()
|
||||
from core.main_ui import MainWindow
|
||||
window = MainWindow(sdConstants.log_level)
|
||||
window.run()
|
||||
else:
|
||||
from core.qmt_real import qmtv as selected_qmtv
|
||||
while True:
|
||||
if sdConstants.exist_config() and sdConstants.initConfig():
|
||||
selected_qmtv.init_qmtv()
|
||||
connected = selected_qmtv.connect()
|
||||
if connected:
|
||||
from core.main_ui import MainWindow
|
||||
window = MainWindow(sdConstants.log_level)
|
||||
window.run()
|
||||
break
|
||||
else:
|
||||
option = messagebox.askokcancel("连接失败", "QMT连接失败,请检查")
|
||||
if option:
|
||||
check_and_create_config()
|
||||
else:
|
||||
break
|
||||
else:
|
||||
option = messagebox.askokcancel("错误", "请检查配置")
|
||||
if option:
|
||||
check_and_create_config()
|
||||
else:
|
||||
break
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"系统初始化失败: {str(e)}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
initialize_system()
|
||||
if '--flet2' in sys.argv:
|
||||
from core.ui.flet.app_v2 import run
|
||||
run()
|
||||
elif '--flet' in sys.argv:
|
||||
from core.ui.flet.app import run
|
||||
run()
|
||||
else:
|
||||
from tkinter import messagebox
|
||||
from core.ui.tkinter.splash import SplashWindow
|
||||
try:
|
||||
window = SplashWindow().run()
|
||||
if window:
|
||||
window.run()
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"系统初始化失败: {str(e)}")
|
||||
|
||||
+4
-4
@@ -4,7 +4,7 @@ a = Analysis(
|
||||
['starter.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('config.ini', '.'), ('xtquant/xtdata.ini', 'xtquant')], # 明确包含配置文件和xtdata.ini
|
||||
datas=[('xtquant/xtdata.ini', 'xtquant')], # xtdata 依赖的配置文件
|
||||
hiddenimports=['brotli', 'brotli.encoding'],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
@@ -24,8 +24,8 @@ exe = EXE(
|
||||
name='神之一手',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=True, # 去除调试符号
|
||||
upx=True,
|
||||
strip=False,
|
||||
upx=False,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False,
|
||||
@@ -34,5 +34,5 @@ exe = EXE(
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
icon='logo.png' # 添加图标文件
|
||||
icon='logo.ico',
|
||||
)
|
||||
Reference in New Issue
Block a user