55 Commits

Author SHA1 Message Date
kyugao 2e3202968d flet v2 2026-06-16 11:07:09 +08:00
kyugao 2d8a0c3bca new ui 2026-06-12 16:25:41 +08:00
kyugao ef4c1cca32 update 2026-06-05 06:08:27 +08:00
kyugao 1816d585bf update 2026-06-04 18:17:48 +08:00
kyugao db910e03d6 添加dummy gateway 2026-06-02 18:07:55 +08:00
kyugao 6b3b1a1f76 代码整理 2025-11-25 17:27:58 +08:00
kyugao 60af627806 日志模块更新 2025-11-25 17:20:23 +08:00
kyugao 4787011177 完善日志系统 2025-11-25 17:13:40 +08:00
kyugao 81da3fe013 update 2025-11-24 16:54:54 +08:00
kyugao c12e394b8e update 2025-11-24 16:47:40 +08:00
kyugao 3ee47e95cf 完善部分逻辑 2025-11-24 16:30:53 +08:00
kyugao e5b1d80139 过滤一条日志 2025-11-20 15:47:09 +08:00
kyugao 5c40d5d64f update 2025-11-20 14:20:18 +08:00
kyugao 1618cad5a0 删除部分无用代码,添加当前订单查询功能 2025-11-19 16:28:04 +08:00
kyugao 64bdddde79 修复一些逻辑问题 2025-11-19 15:55:23 +08:00
kyugao 4c4c8730f2 初步完成网格交易统计重构 2025-11-19 14:14:37 +08:00
kyugao 0262bfc71b 完成预下单优化 2025-11-19 10:29:58 +08:00
kyugao d5fef7c0c1 update 2025-11-18 18:06:15 +08:00
kyugao fcadcb86d2 日志更新 2025-11-18 13:34:44 +08:00
kyugao 59a88c4365 移除不需要的控制台日志 2025-11-18 10:55:13 +08:00
kyugao c69b5bc1ae 更新控制台日志输出格式 2025-11-18 10:43:54 +08:00
kyugao f626545897 release 2025-11-17 18:10:41 +08:00
kyugao f499d9a413 update for first release 2025-11-17 16:36:09 +08:00
kyugao 6cae413956 调整一些日志,基本上OK了 2025-11-17 11:50:51 +08:00
kyugao 6810b55cbb 第一个Alpha版本 2025-11-14 18:09:20 +08:00
kyugao cd67c9ad60 update 2025-11-14 15:52:03 +08:00
kyugao a18c7be7eb 代码测试与整理 2025-11-14 11:45:42 +08:00
kyugao f5d37eaa7e update 2025-11-13 17:42:00 +08:00
kyugao 550126d060 update 2025-11-13 17:32:10 +08:00
kyugao c03a4adb53 update 2025-11-12 18:14:56 +08:00
kyugao 81d0131a7b update 2025-11-12 17:57:45 +08:00
kyugao 1193dc2f69 独立配置 2025-11-12 15:08:56 +08:00
kyugao 8ab5d83b1a update 2025-11-12 12:50:22 +08:00
kyugao 91d1cac384 update 2025-11-12 12:49:22 +08:00
kyugao 2ca0ab65f0 Merge branch 'new_structure'
# Conflicts:
#	config.ini
#	config.py
2025-11-12 11:53:35 +08:00
kyugao 5e64e93172 update 2025-11-12 11:51:40 +08:00
kyugao 0d54f8b05a update 2025-11-12 10:48:43 +08:00
kyugao ba9cd9a700 update 2025-11-12 10:15:14 +08:00
kyugao 0dbd8e8dde 完成添加,删除,开启,停止方法的重构 2025-11-11 18:09:42 +08:00
kyugao 54fd7c9545 new update 2025-11-11 17:45:56 +08:00
kyugao 662a1ea7c1 update 2025-11-11 16:47:55 +08:00
kyugao c42648d1b4 update for restructure 2025-11-11 12:15:40 +08:00
kyugao 7cfb433aaf update 2025-11-10 17:16:06 +08:00
kyugao 1ec27bb52f 去掉不用的配置 2025-11-10 17:15:35 +08:00
kyugao 7f0a73381d release 2025-11-10 17:05:43 +08:00
kyugao c2f34d09e9 去掉不用的配置项 2025-11-10 16:19:04 +08:00
kyugao 20a7453e8b 程序整理 2025-11-10 14:49:35 +08:00
kyugao 3a137b6aee 市场数据跟踪 2025-11-10 14:43:14 +08:00
kyugao 7733d6df32 网格修正 2025-11-10 10:57:21 +08:00
kyugao b7f9d29c54 调试 2025-11-07 11:00:41 +08:00
kyugao d988f5eb48 完善UI操作逻辑 2025-11-06 18:13:10 +08:00
kyugao 1ee8f0426e 初步测试通过 2025-11-06 14:39:09 +08:00
kyugao 88bd0b17c9 新增功能,网格修正 2025-11-05 18:09:00 +08:00
kyugao df0e9ecb22 添加编译文件、打包瘦身、UI逻辑调整 2025-11-05 18:04:52 +08:00
kyugao c77ff1c0ae 合并UI 2025-11-05 10:34:30 +08:00
36 changed files with 5963 additions and 1332 deletions
+7
View File
@@ -1 +1,8 @@
__pycache__/ __pycache__/
dist/
example.db
xtquant/
starter.dist/starter.dll
build/
.vscode/
example.db.bak
+233
View File
@@ -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(每格利润空间)
```
-7
View File
@@ -1,7 +0,0 @@
[config]
miniqmtpath = /Users/gao/Workspace/quant
grid_price = 10.9,10.0,9.1,8.2,7.3,6.4,5.5,4.6,3.7,2.8,1.9,1.0
grid_volume = 200
account_no = 99082560
max_enabled_targets = 10
+25
View File
@@ -0,0 +1,25 @@
"""
运行时配置 — 端口、路径、账号由自动探测设置,无需配置文件。
"""
import os
import sys
from pathlib import Path
# ---- 自动探测的配置项(默认值仅占位,启动时自动修正) ----
miniQMTPath: str = ''
account_no: str = ''
log_level: str = 'INFO'
console_log: bool = True
use_simulated_qmt: bool = False
def app_dir() -> Path:
"""应用根目录(兼容开发环境与打包后的 exe)"""
if getattr(sys, 'frozen', False):
return Path(sys.executable).parent
return Path(__file__).resolve().parent
def log_file_path() -> Path:
"""日志文件路径"""
return app_dir() / 'sfgrid.log'
+5
View File
@@ -0,0 +1,5 @@
import xtquant.xtconstant as xtconstant
OrderTypeBuy = f'{xtconstant.STOCK_BUY}' # 买
OrderTypeSell = f'{xtconstant.STOCK_SELL}' # 卖
OrderTypeNone = "None"
+12
View File
@@ -0,0 +1,12 @@
from peewee import SqliteDatabase, Model
from core.logger import LogLevel, PrintLog
# 连接到SQLite数据库
db: SqliteDatabase = SqliteDatabase('example.db')
db.connect()
PrintLog(LogLevel.INFO, '- [成功]数据库连接')
# 定义基础模型类
class BaseModel(Model):
class Meta:
database: SqliteDatabase = db
+33
View File
@@ -0,0 +1,33 @@
# 市场数据监听控制事件
EventMarketActiveSwitch = "market_active_switch" # 市场数据状态变更
MarketDataUpdate = "market_data_update" # 市价更新
MarketOrderCreated = "market_order_created" # 市价单创建
MarketOrderTraded = "market_order_traded" # 市价单成交
MarketOrderError = "market_order_error" # 市价单委托失败
# Pring Log
EventPrintLog = "print_log" # 打印日志
class EventBus:
def __init__(self):
self.listeners = {} # 管理各种event的订阅情况
def subscribe(self, event_type, listener):
if event_type not in self.listeners:
self.listeners[event_type] = []
self.listeners[event_type].append(listener)
def publish(self, event_type, data):
if event_type in self.listeners:
for listener in self.listeners[event_type]:
listener(data)
# # 订阅事件
# event_bus.subscribe('my_event', handle_event)
# # 发布事件
# event_bus.publish('my_event', {'key': 'value'})
# 创建事件总线实例
event_bus = EventBus()
+4
View File
@@ -0,0 +1,4 @@
import xtquant.xtconstant as xtconstant
HeatTypeUpStop = "UpStop" # 涨停
HeatTypeDragonTiger = "DragonTiger" # 龙虎榜
+11
View File
@@ -0,0 +1,11 @@
from peewee import CharField, DateField
from core.database import BaseModel, db
class HeatStock(BaseModel):
stock_code = CharField(unique=True)
stock_name = CharField()
heat_type = CharField()
date = DateField()
db.create_tables([HeatStock])
+49
View File
@@ -0,0 +1,49 @@
from datetime import datetime
from enum import Enum
import threading
from core.eventbus import EventPrintLog, event_bus
import config
class LogLevel(Enum):
DEBUG = 0
INFO = 1
WARNING = 2
ERROR = 3
CRITICAL = 4
def __le__(self, other):
return self.value <= other.value
class LogData:
def __init__(self, level: LogLevel, message: str):
self.level = level
self.message = message
_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(line)
# 写入日志文件
try:
with _log_lock:
with open(_log_file_path(), 'a', encoding='utf-8') as f:
f.write(line + '\n')
except Exception:
pass # 写文件失败不阻塞主流程
-310
View File
@@ -1,310 +0,0 @@
# coding:utf-8
from xtquant.xttrader import XtQuantTrader
import time, sys
from peewee import ModelSelect
import xtquant.xtconstant as xtconstant
sys.stdout.reconfigure(encoding='utf-8') # 设置标准输出编码为UTF-8 # type: ignore
import core.strategy_db as strategy_db
import sfgrid_constants
from core.sfgrid_strategy import SFGridStrategy
from core.util import getInstrumentName, getStockPosition
from xtquant.xttrader import XtQuantTrader
from xtquant.xttype import StockAccount, XtAsset, XtOrder, XtOrderResponse, XtPosition, XtTrade
from xtquant import xtdata
from xtquant.xttrader import XtQuantTraderCallback
import datetime
# 量化核心控制对象
class SFGridController(XtQuantTraderCallback):
def __init__(self, account_no: str, miniQmtPath: str):
super().__init__()
xtdata.enable_hello = False
session_id = int(time.time())
self.xt_trader: XtQuantTrader = XtQuantTrader(miniQmtPath, session_id)
self.xt_trader.register_callback(self)
self.xt_trader.start()
connect_result = self.xt_trader.connect()
print(f'- [{'成功' if self.xt_trader.connected else '失败'}]市场交易连接{connect_result}--: {miniQmtPath}')
if self.xt_trader.connected == False:
self.inited: bool = False
return
else:
self.inited = True
self.account= StockAccount(account_no, 'STOCK')
print(f'- [成功]交易账号对象初始化完成, 账号: {self.account.account_id}') # pyright: ignore[reportAttributeAccessIssue]
subscribe_result = self.xt_trader.subscribe(self.account)
print(f'- [{'成功' if subscribe_result == 0 else '失败'}:{subscribe_result}]交易状态订阅')
if subscribe_result == 0:
self.inited = True
else:
self.inited = False
return
self.stock_trade_ctrl = {}
self.init_instrument_pool(self.xt_trader, self.account) # type: ignore
self.seq = None
print('- [成功]三疯交易系统初始化完成')
self.startMarketData()
def startMarketData(self):
print('- 启动市场数据订阅')
self.seq = xtdata.subscribe_whole_quote(['SH', 'SZ'], callback=self.onDataUpdate)
if self.seq == -1:
print('- 市场数据订阅失败')
else:
print(f'- 市场数据订阅成功, 订阅号={self.seq}')
def stopMarketData(self):
print('- 停止市场数据订阅')
if self.seq is not None and self.seq > 0:
xtdata.unsubscribe_quote(self.seq)
def add_trade_target(self, stock_code: str):
try:
stock_name = getInstrumentName(stock_code)
new_target = strategy_db.TradeTarget.create(
stock_name=stock_name,
stock_code=stock_code,
current_position=0,
grid_index=0,
last_trade_price=0.0,
current_buy_price=0.0,
current_buy_order_no='',
current_sell_price=0.0,
current_sell_order_no=''
)
new_target.save()
print(f'新增交易标的 {stock_code} {stock_name}, {new_target.id}')
# 刷新标的持仓
pos = getStockPosition(stock_code, self.xt_trader, self.account) # type: ignore
strategy_db.TradeTarget.update(current_position=pos).where(strategy_db.TradeTarget.stock_code == stock_code).execute()
# 更新标的池
self.refresh_targets()
# 添加交易控制器
stockTradeController = SFGridStrategy(new_target, self.xt_trader, self.account, new_target.enabled) # type: ignore
self.stock_trade_ctrl[stock_code] = stockTradeController
except Exception as e:
print(f'新增交易标的失败 {stock_code} {e}')
def del_trade_target(self, index:int):
target: strategy_db.TradeTarget = self.instrument_pool[index]
# self.stock_trade_ctrl.
del self.stock_trade_ctrl[target.stock_code]
target.delete_instance()
self.refresh_targets()
def init_instrument_pool(self, xtTrader:XtQuantTrader, account:StockAccount):
self.refresh_targets()
for temp in self.instrument_pool:
tradeTarget:strategy_db.TradeTarget = temp
tradeTarget.current_position = getStockPosition(tradeTarget.stock_code, xtTrader, account) # type: ignore
result = tradeTarget.save()
print(f' |- 同步当前持仓信息 {tradeTarget.stock_code}, {tradeTarget.current_position}, result = {result}')
stockTradeController = SFGridStrategy(tradeTarget, self.xt_trader, self.account, tradeTarget.enabled) # type: ignore
self.stock_trade_ctrl[tradeTarget.stock_code] = stockTradeController
print(f'- [成功]交易标的信息初始化, 共 {len(self.instrument_pool)} 个标的')
def refresh_targets(self):
# 更新标的池
self.instrument_pool:ModelSelect = strategy_db.TradeTarget.select()
self.print_pool()
def print_pool(self):
print("- [信息]标的池信息")
for i in range(len(self.instrument_pool)):
target: strategy_db.TradeTarget = self.instrument_pool[i]
status = "新建" if target.status == 0 else "已建初始仓"
print(f' [序号-{i}] 股票代码: {target.stock_code}-{target.stock_name} 当前持仓: {getStockPosition(target.stock_code, self.xt_trader, self.account)} 网格索引: {target.grid_index} 基准价格 {sfgrid_constants.grid_price[target.grid_index]} 状态: {status} 启用交易线程: {'自动交易中' if target.enabled else '交易已停止'}') # type: ignore
def print_position_info(self):
positions:list[XtPosition] = self.xt_trader.query_stock_positions(self.account)
if positions:
print("\n- 持仓信息")
for temp in positions:
pos : XtPosition = temp
if pos.volume <=0:
continue
print(f"股票代码: {pos.stock_code}-{getInstrumentName(pos.stock_code)}")
print(f"总持仓: {pos.volume}")
print(f"可用持仓: {pos.can_use_volume}")
print(f"持仓成本: {pos.avg_price}")
print("---")
else:
print("\n当前无持仓")
def print_account_info(self):
temp = self.xt_trader.query_stock_asset(self.account)
asset: XtAsset = temp # type: ignore
print(f"=== 账户信息 {self.account.account_id} ===") # type: ignore
print(f"可用资金: {asset.cash}")
print(f"总资产: {asset.total_asset}")
print(f"证券市值: {asset.market_value}")
def print_stock_orders(self):
orders = self.xt_trader.query_stock_orders(self.account, cancelable_only=True)
if orders:
print("\n=== 委托信息 ===")
for order in orders:
print(f"委托编号: {order.order_id}")
print(f"股票代码: {order.stock_code} {getInstrumentName(order.stock_code)}")
print(f"委托方向: {order.offset_flag} ")
print(f"委托价格: {order.price}")
print(f"委托数量: {order.order_volume}")
print(f"已成交数量: {order.traded_volume}")
print(f"委托状态: {order.order_status} ")
print("---")
else:
print("\n当前无委托记录")
# 初始化指定标的交易控制器
def start_stock_trade(self, index: int):
tradeTarget = self.instrument_pool[index]
# check existing thread
if tradeTarget.stock_code in self.stock_trade_ctrl:
tradeController: SFGridStrategy = self.stock_trade_ctrl[tradeTarget.stock_code]
if tradeController.isEnabled():
print(f"标的交易控制器已存在且正在运行 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n")
else:
print(f"标的交易控制器已存在但未运行,重新启动 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n")
tradeController.enabledTrading(True)
else:
stockTradeController = SFGridStrategy(tradeTarget, self.xt_trader, self.account, tradeTarget.enabled) # type: ignore
self.stock_trade_ctrl[tradeTarget.stock_code] = stockTradeController
print(f"\t创建标的交易控制器 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}")
def pause_stock_trade(self, index: int):
tradeTarget = self.instrument_pool[index]
if tradeTarget.stock_code in self.stock_trade_ctrl:
tradeController: SFGridStrategy = self.stock_trade_ctrl[tradeTarget.stock_code]
if tradeController.isEnabled():
print(f"暂停标的交易 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n")
tradeController.enabledTrading(False)
else:
print(f"标的交易已暂停 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n")
else:
print(f"标的交易控制器不存在 {tradeTarget.stock_code} {getInstrumentName(tradeTarget.stock_code)}\n")
# ====== 市场回调方法 -- 以下方法由XtQuantData调用 ======
def onDataUpdate(self, data):
if sfgrid_constants.max_enabled_targets <= 0: # 全推
for stock_code, tickData in data.items():
lastPrice = tickData['lastPrice']
if lastPrice == 10.0 and stock_code not in self.stock_trade_ctrl:
print(f'New trade target = {stock_code} - {getInstrumentName(stock_code)} {tickData['lastPrice']}')
self.add_trade_target(stock_code)
self.stock_trade_ctrl[stock_code].enabledTrading(True)
else: # 指定目标 当前主要使用这种模式
for target in self.instrument_pool:
stock_code = target.stock_code
# 如果存在对应的StockTradeController,则调用其onDataUpdate方法
if stock_code not in self.stock_trade_ctrl or stock_code not in data:
# print(f"股票代码 {stock_code} 未在交易控制器中找到,跳过处理。\n")
continue
stock_controller: SFGridStrategy = self.stock_trade_ctrl[stock_code]
stock_controller.onDataUpdate(data)
# ====== 市场回调方法 -- 以下方法由XtQuantTrader调用 ======
def on_connected(self):
"""
连接成功推送
"""
print(datetime.datetime.now(), '连接成功回调')
def on_disconnected(self):
"""
连接断开
:return:
"""
print(datetime.datetime.now(), '连接断开回调')
def on_stock_order(self, order:XtOrder):
"""
委托回报推送
:param order: XtOrder对象
:return:
"""
stockCode = order.stock_code
ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode]
# 如果存在对应的StockTradeController,则调用其onDataUpdate方法
if ctrl is not None and order.strategy_name == ctrl.getName():
ctrl.onOrderTrade(trade=order) # type: ignore
else:
print(f"委托下单回调 投资备注 orderId: {order.order_sysid} [{order.stock_code}-{order.instrument_name}] volume: {order.order_volume} 订单策略: '{order.strategy_name}'<-->'{ctrl.getName()}'")
def test_sim_trade(self, index: int, orderType: int):
tradeTarget:strategy_db.TradeTarget = self.instrument_pool[index]
ctrl:SFGridStrategy = self.stock_trade_ctrl[tradeTarget.stock_code]
trade: XtTrade = None # type: ignore
if orderType == xtconstant.STOCK_BUY:
trade = XtTrade(
sfgrid_constants.account_no,
'300083.SZ',
xtconstant.STOCK_BUY,
1, 1, tradeTarget.current_buy_price, sfgrid_constants.grid_volume, 1000,
tradeTarget.current_buy_order_no,
None, ctrl.getName(), None, None, None, None, None, tradeTarget.stock_name)
else:
trade = XtTrade(sfgrid_constants.account_no, '300083.SZ', xtconstant.STOCK_SELL, 1, 1, price, sfgrid_constants.grid_volume, 1000, tradeTarget.current_sell_order_no, None, ctrl.getName(), None, None, None, None, None, tradeTarget.stock_name) # type: ignore
self.on_stock_trade(trade)
def on_stock_trade(self, trade:XtTrade):
"""
成交变动推送
:param trade: XtTrade对象
:return:
"""
stockCode = trade.stock_code
ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode]
# 如果存在对应的StockTradeController,则调用其onDataUpdate方法
if ctrl is not None and trade.strategy_name == ctrl.getName():
ctrl.onOrderTrade(trade)
else:
print(f"委托回调 投资备注 {trade.strategy_name} 不匹配 {ctrl.getName()}")
def on_order_stock_async_response(self, response:XtOrderResponse):
stockCode = response.order_remark
ctrl:SFGridStrategy = self.stock_trade_ctrl[stockCode]
# 如果存在对应的StockTradeController,则调用其onDataUpdate方法
if ctrl is not None and response.strategy_name == ctrl.getName():
ctrl.onAsyncOrderResponse(response)
else:
print(f"委托回调 投资备注 {response.strategy_name} 不匹配 {ctrl.getName()}")
def on_order_error(self, order_error):
"""
委托失败推送
:param order_error:XtOrderError 对象
:return:
"""
# print("on order_error callback")
# print(order_error.order_id, order_error.error_id, order_error.error_msg)
print(f"\n委托报错回调 {order_error.order_remark} {order_error.error_msg}")
def on_account_status(self, status):
"""
:param response: XtAccountStatus 对象
:return:
"""
print(datetime.datetime.now(), sys._getframe().f_code.co_name)
+32
View File
@@ -0,0 +1,32 @@
"""
QMT 模块统一入口
根据配置或环境自动选择真实 QMT 或模拟器
"""
import sys
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 as e:
print(f'[qmt] qmt_real 加载失败: {e},回退 qmt_dummy')
# 非 Windows 或导入失败,使用模拟器
print('[qmt] 使用模拟模式 qmt_dummy')
from core.qmt_dummy import qmtv
return qmtv
# 导出单例
qmtv = _get_qmt()
+298
View File
@@ -0,0 +1,298 @@
"""
Dummy QMT 模拟器 - 用于在非 Windows 环境下模拟 QMT 交易功能
"""
import datetime
import threading
import time
import random
import config
import core.eventbus as eBus
from core.logger import LogLevel, PrintLog
class DummyPosition:
"""模拟持仓"""
def __init__(self, stock_code, stock_name, volume, yesterday_vol=0):
self.stock_code = stock_code
self.stock_name = stock_name
self.volume = volume
self.can_use_volume = volume
self.yesterday_volume = yesterday_vol
class DummyOrder:
"""模拟订单"""
def __init__(self, stock_code, order_id, status, price, volume):
self.stock_code = stock_code
self.order_id = order_id
self.order_status = status
self.order_price = price
self.volume = volume
class DummyTrade:
"""模拟成交"""
def __init__(self, stock_code, trade_id, price, volume, strategy_name):
self.stock_code = stock_code
self.trade_id = trade_id
self.trade_price = price
self.trade_volume = volume
self.strategy_name = strategy_name
class DummyOrderResponse:
"""模拟下单响应"""
def __init__(self, order_id, stock_code, seq, error_msg, strategy_name):
self.order_id = order_id
self.stock_code = stock_code
self.seq = seq
self.error_msg = error_msg
self.strategy_name = strategy_name
class DummyQmtV:
"""
Dummy QMT 模拟器
模拟 QmtV 类的接口,用于在没有 miniQMT 的环境下运行和测试
"""
def __init__(self) -> None:
self.inited = False
self.details = {}
self.lastMarketDataUpdateTimestamp = time.time()
self.isMarketActive = True
self.connected = False
self.account = None
self._positions = {}
self._pending_orders = []
self._market_data_thread = None
self._counter = 0
def getTrader(self):
return self
def init_qmtv(self):
"""初始化交易器"""
PrintLog(LogLevel.INFO, f'- [模拟] QMT 交易器初始化')
self.connected = True
self.inited = True
def connect(self) -> bool:
"""连接 QMT (模拟总是成功)"""
PrintLog(LogLevel.INFO, f'- [成功] 市场交易连接 (模拟模式)')
# 创建模拟账号
try:
from xtquant.xttype import StockAccount
self.account = StockAccount(config.account_no, 'STOCK')
except ImportError:
self.account = type('StockAccount', (), {'account_id': config.account_no})()
PrintLog(LogLevel.INFO, f'- [成功] 交易账号: {config.account_no}')
self._init_dummy_positions()
self.startMarketDataSubscription()
return self.inited
def _init_dummy_positions(self):
"""初始化模拟持仓数据"""
dummy_stocks = [
('600519', '贵州茅台', 100, 2800.0),
('000858', '五粮液', 200, 180.0),
('600036', '招商银行', 500, 42.0),
('000001', '平安银行', 300, 13.5),
]
for code, name, volume, price in dummy_stocks:
self._positions[code] = {
'stock_code': code,
'stock_name': name,
'volume': volume,
'can_use_volume': volume,
'open_cost': price,
'market_value': volume * price
}
PrintLog(LogLevel.INFO, f'- [模拟] 已加载 {len(self._positions)} 个持仓')
def getAllPositions(self) -> dict:
"""获取全部持仓,返回 {stock_code: position_object}"""
result = {}
for code, pos_data in self._positions.items():
result[code] = type('DummyPos', (), pos_data)()
return result
def getStockPosition(self, stock_code: str):
"""获取持仓 (模拟)"""
if stock_code in self._positions:
pos = self._positions[stock_code]
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
if o.stock_code == stock_code and
(tag is None or getattr(o, 'strategy_name', None) == tag)]
def orderAsync(self, stock_code, orderVolume, orderType, orderPrice, priceType, orderRemark, strategy_name):
"""异步下单 (模拟)"""
self._counter += 1
order_id = f"DUMMY{self._counter:06d}"
seq = self._counter
order = DummyOrder(
stock_code=stock_code,
order_id=order_id,
status='reported',
price=orderPrice,
volume=orderVolume
)
order.strategy_name = strategy_name
order.order_remark = orderRemark
self._pending_orders.append(order)
response = DummyOrderResponse(
order_id=order_id,
stock_code=stock_code,
seq=seq,
error_msg='成功',
strategy_name=strategy_name
)
response.order_remark = orderRemark
eBus.event_bus.publish(eBus.MarketOrderCreated, response)
PrintLog(LogLevel.INFO, f'- [模拟下单] {stock_code} 数量:{orderVolume} 价格:{orderPrice} 订单号:{order_id}')
# 模拟成交 (80% 概率)
if random.random() > 0.2:
threading.Timer(random.uniform(0.5, 3.0), self._simulate_trade,
args=(stock_code, order_id, orderPrice, orderVolume, strategy_name)).start()
return 0
def _simulate_trade(self, stock_code, order_id, price, volume, strategy_name):
"""模拟成交"""
trade = DummyTrade(
stock_code=stock_code,
trade_id=f"TRADE{self._counter:06d}",
price=price,
volume=volume,
strategy_name=strategy_name
)
trade.trade_time = int(time.strftime('%H%M%S'))
trade.order_remark = stock_code
if stock_code in self._positions:
self._positions[stock_code]['volume'] += volume
self._positions[stock_code]['can_use_volume'] += volume
eBus.event_bus.publish(eBus.MarketOrderTraded, trade)
PrintLog(LogLevel.INFO, f'- [模拟成交] {stock_code} 数量:{volume} 价格:{price}')
def cacheStockDetail(self, stock_code: str):
"""获取股票详情 (模拟)"""
if stock_code not in self.details:
self.details[stock_code] = {
'InstrumentName': self._get_dummy_name(stock_code),
'UpStopPrice': 0,
'DownStopPrice': 0
}
return self.details[stock_code]
def _get_dummy_name(self, stock_code: str) -> str:
"""获取模拟股票名称"""
names = {
'600519': '贵州茅台', '000858': '五粮液', '600036': '招商银行',
'000001': '平安银行', '000002': '万科A', '600000': '浦发银行'
}
return names.get(stock_code, f'股票{stock_code}')
def getInstrumentName(self, stock_code: str) -> str:
"""获取股票名称"""
return self.cacheStockDetail(stock_code)['InstrumentName']
def dailyUpStop(self, stock_code: str):
"""获取涨停价 (模拟)"""
cacheStock = self.cacheStockDetail(stock_code)
PrintLog(LogLevel.INFO, f'- [模拟] 获取股票详情: {stock_code} {cacheStock["InstrumentName"]} 涨停价: 0')
return 0.0
def dailyDownStop(self, stock_code: str):
"""获取跌停价 (模拟)"""
return 0.0
def getLastPrice(self, stock_code: str) -> float:
"""主动获取最新市价(模拟)"""
if stock_code in self._positions:
return float(self._positions[stock_code].get('open_cost', 10.0))
# 给一个合理模拟价
return 10.0 + hash(stock_code) % 100
def startMarketDataSubscription(self):
"""启动市场数据订阅 (模拟)"""
try:
self._market_data_thread = threading.Thread(target=self._generate_market_data, daemon=True)
self._market_data_thread.start()
PrintLog(LogLevel.INFO, f'- [市场数据订阅成功-模拟]')
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [市场数据订阅失败-{e}]')
def stopMarketDataSubscription(self):
"""停止市场数据订阅"""
PrintLog(LogLevel.INFO, '- 停止市场数据订阅 (模拟)')
def _generate_market_data(self):
"""生成模拟市场数据"""
stocks = ['600519', '000858', '600036', '000001', '000002', '600000']
base_prices = [2800.0, 180.0, 42.0, 13.5, 10.0, 10.0]
while True:
try:
for i, stock in enumerate(stocks):
data = {
'stock_code': stock,
'last_price': base_prices[i] + random.uniform(-1, 1),
'open_price': base_prices[i],
'high_price': base_prices[i] + random.uniform(0, 2),
'low_price': base_prices[i] - random.uniform(0, 2),
'volume': random.randint(1000, 10000),
'timestamp': time.time()
}
eBus.event_bus.publish(eBus.MarketDataUpdate, data)
base_prices[i] = data['last_price']
self.lastMarketDataUpdateTimestamp = time.time()
self.isMarketActive = True
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, True)
time.sleep(3)
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [市场数据模拟异常-{e}]')
time.sleep(1)
def on_connected(self):
print(datetime.datetime.now(), '模拟连接成功')
def on_disconnected(self):
print(datetime.datetime.now(), '模拟连接断开')
def on_stock_order(self, order):
pass
def on_stock_trade(self, trade):
eBus.event_bus.publish(eBus.MarketOrderTraded, trade)
def on_order_stock_async_response(self, response):
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}")
eBus.event_bus.publish(eBus.MarketOrderError, order_error)
def on_account_status(self, status):
print(datetime.datetime.now(), status)
qmtv = DummyQmtV()
+598
View File
@@ -0,0 +1,598 @@
"""
QMT 真实交易实现 - 封装 xtquant SDK
"""
import datetime
import os
import subprocess
import threading
import time
import config
import core.eventbus as eBus
from core.logger import LogLevel, PrintLog
class RealQmtV:
"""
真实 QMT 交易器
封装 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'"""
return stock_code.split('.')[0] if '.' in stock_code else stock_code
@staticmethod
def _to_full_code(stock_code: str) -> str:
"""将数据库格式 '600519' 转换为 xtquant 格式 '600519.SH'"""
if '.' in stock_code:
return stock_code # already has suffix
code = stock_code
if code.startswith(('6', '5', '9')):
return f'{code}.SH'
elif code.startswith(('0', '3', '2')):
return f'{code}.SZ'
# fallback: try both, prefer SH
return f'{code}.SH'
def __init__(self) -> None:
self.inited = False
self.connected = False
self.account = None
self.xt_trader = None
self.mini_qmt_path = ""
self._positions = {}
self._pending_orders = []
self._market_data_thread = None
self.isMarketActive = False
self.lastMarketDataUpdateTimestamp = time.time()
self.details = {}
def getTrader(self):
return self
def init_qmtv(self):
"""初始化 QMT 交易器"""
try:
from xtquant.xttrader import XtQuantTrader
from xtquant.xttype import StockAccount
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 方法
self.xt_trader.register_callback(self)
self.inited = True
PrintLog(LogLevel.INFO, f'- [真实] QMT 交易器初始化成功')
except Exception as e:
self.inited = False
PrintLog(LogLevel.ERROR, f'- [失败] QMT 初始化: {e}')
def connect(self) -> bool:
"""连接 MiniQMT,失败自动探测端口并重试"""
if not self.inited:
PrintLog(LogLevel.ERROR, '[QMT] 连接失败: 未初始化')
return False
_connect_errors = {
0: '成功',
-1: '一般错误(miniQMT 可能未启动)',
-2: 'miniQMT 未运行(请先启动极简QMT)',
-3: '连接超时',
}
def _do_connect() -> int:
self.xt_trader.start()
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:
PrintLog(LogLevel.INFO, f'[QMT] 订阅账户...')
self.xt_trader.subscribe(self.account)
PrintLog(LogLevel.INFO, '[QMT] 订阅完成')
self.connected = True
self.startMarketDataSubscription()
PrintLog(LogLevel.INFO, f'[QMT] 连接成功 (账号: {config.account_no[-4:]}****)')
return True
else:
PrintLog(LogLevel.ERROR, f'[QMT] 连接失败: {result_desc}')
return False
except Exception as e:
PrintLog(LogLevel.ERROR, f'[QMT] 连接异常: {e}')
return False
def getAllPositions(self) -> dict:
"""获取全部持仓,返回 {plain_code: position_object}"""
if not self.connected:
return {}
try:
positions = self.xt_trader.query_stock_positions(self.account)
result = {}
for pos in positions:
code = self._to_plain_code(getattr(pos, 'stock_code', ''))
result[code] = pos
# 缓存以供 getStockPosition 使用
self._position_cache = result
return result
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [获取全部持仓失败]: {e}')
return {}
def getStockPosition(self, stock_code: str):
"""获取单只股票持仓(优先使用缓存)"""
if not self.connected:
return None
try:
# 优先查缓存
if hasattr(self, '_position_cache') and stock_code in self._position_cache:
return self._position_cache[stock_code]
# 回退查询
positions = self.xt_trader.query_stock_positions(self.account)
for pos in positions:
pos_code = self._to_plain_code(getattr(pos, 'stock_code', ''))
if pos_code == stock_code:
return pos
return None
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [持仓查询失败] {stock_code}: {e}')
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) 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:
PrintLog(LogLevel.ERROR, '- [下单失败] 未连接')
return -1
try:
full_code = self._to_full_code(stock_code)
seq = self.xt_trader.order_stock_async(
account=self.account,
stock_code=full_code,
order_volume=orderVolume,
order_type=orderType,
price=orderPrice,
price_type=priceType,
order_remark=orderRemark,
strategy_name=strategy_name
)
PrintLog(LogLevel.INFO,
f'- [下单] {stock_code} 数量:{orderVolume} 价格:{orderPrice} 类型:{orderType} seq:{seq}')
return 0
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [下单失败] {stock_code}: {e}')
return -1
def cacheStockDetail(self, stock_code: str):
"""获取股票详情"""
if stock_code not in self.details:
try:
from xtquant import xtdata
# xtquant 需要带后缀的完整代码
full_code = self._to_full_code(stock_code)
detail = xtdata.get_instrument_detail(full_code)
if detail:
# xtquant 返回 dict,使用 .get() 读取
self.details[stock_code] = {
'InstrumentName': detail.get('InstrumentName', stock_code) if isinstance(detail, dict) else getattr(detail, 'InstrumentName', stock_code),
'UpStopPrice': detail.get('UpStopPrice', 0) if isinstance(detail, dict) else getattr(detail, 'UpStopPrice', 0),
'DownStopPrice': detail.get('DownStopPrice', 0) if isinstance(detail, dict) else getattr(detail, 'DownStopPrice', 0)
}
else:
self.details[stock_code] = {
'InstrumentName': stock_code,
'UpStopPrice': 0,
'DownStopPrice': 0
}
except Exception:
self.details[stock_code] = {
'InstrumentName': stock_code,
'UpStopPrice': 0,
'DownStopPrice': 0
}
return self.details[stock_code]
def getInstrumentName(self, stock_code: str) -> str:
"""获取股票名称"""
return self.cacheStockDetail(stock_code)['InstrumentName']
def dailyUpStop(self, stock_code: str):
"""获取涨停价"""
detail = self.cacheStockDetail(stock_code)
up_stop = detail.get('UpStopPrice', 0)
PrintLog(LogLevel.DEBUG, f'- [详情] {stock_code} {detail["InstrumentName"]} 涨停价: {up_stop}')
return up_stop or 0.0
def dailyDownStop(self, stock_code: str):
"""获取跌停价"""
detail = self.cacheStockDetail(stock_code)
down_stop = detail.get('DownStopPrice', 0)
return down_stop or 0.0
def getLastPrice(self, stock_code: str) -> float:
"""主动获取最新市价(拉取模式,作为推送的兜底)"""
try:
from xtquant import xtdata
import json
full_code = self._to_full_code(stock_code)
# 方式1: 尝试 get_full_tick(参数是 list[str],返回 dict {code: {...}}
raw = xtdata.get_full_tick([full_code])
if raw:
tick = json.loads(raw) if isinstance(raw, str) else raw
if isinstance(tick, dict):
# 格式: {'600519.SH': {'lastPrice': 8.97, ...}}
for code, info in tick.items():
if isinstance(info, dict) and info.get('lastPrice', 0) > 0:
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → tick: {info["lastPrice"]:.3f}')
return float(info['lastPrice'])
# 方式2: get_market_data 取最新1分钟K线收盘价
data = xtdata.get_market_data(
field_list=['close'],
stock_list=[full_code],
period='1m',
count=1
)
if data:
vals = None
if full_code in data:
row = data[full_code]
if hasattr(row, '__iter__') and not isinstance(row, str):
row = list(row)
if row:
vals = row
if not vals and 'close' in data:
field_data = data['close']
if full_code in field_data:
vals = list(field_data[full_code])
if vals and len(vals) > 0 and float(vals[0]) > 0:
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → kline: {float(vals[0]):.3f}')
return float(vals[0])
# 方式3: 下载历史数据后再试
xtdata.download_history_data(full_code, '1m', '')
data = xtdata.get_market_data(
field_list=['close'],
stock_list=[full_code],
period='1m',
count=1
)
if data:
vals = None
if full_code in data:
row = data[full_code]
if hasattr(row, '__iter__') and not isinstance(row, str):
row = list(row)
if row:
vals = row
if not vals and 'close' in data:
field_data = data['close']
if full_code in field_data:
vals = list(field_data[full_code])
if vals and len(vals) > 0 and float(vals[0]) > 0:
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → download+kline: {float(vals[0]):.3f}')
return float(vals[0])
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → 失败: 所有方式均无数据, raw={raw}')
except Exception as e:
PrintLog(LogLevel.DEBUG, f'[getLastPrice] {stock_code} → 异常: {e}')
return 0.0
def startMarketDataSubscription(self):
"""启动市场数据订阅"""
try:
from xtquant import xtdata
# 订阅沪深全市场实时行情
seq = xtdata.subscribe_whole_quote(['SH', 'SZ'], self._on_market_data)
PrintLog(LogLevel.INFO, f'- [市场数据订阅成功-真实] seq={seq}')
# 启动行情活跃监控线程(默认不活跃,收到行情后激活)
self._market_data_thread = threading.Thread(
target=self._market_data_watchdog, daemon=True
)
self._market_data_thread.start()
except Exception as e:
PrintLog(LogLevel.ERROR, f'- [市场数据订阅失败-{e}]')
def _on_market_data(self, datas: dict):
"""xtquant 行情回调 — 收到行情即标记市场活跃"""
self.lastMarketDataUpdateTimestamp = time.time()
if not self.isMarketActive:
self.isMarketActive = True
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, True)
eBus.event_bus.publish(eBus.MarketDataUpdate, datas)
def _market_data_watchdog(self):
"""行情活跃监控 — 超过 120 秒无行情则标记市场不活跃"""
while True:
time.sleep(15)
if self.isMarketActive:
elapsed = time.time() - self.lastMarketDataUpdateTimestamp
if elapsed > 120:
self.isMarketActive = False
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, False)
PrintLog(LogLevel.WARNING, f'- [行情] 超过 {elapsed:.0f} 秒无更新,市场标记为不活跃')
def stopMarketDataSubscription(self):
"""停止市场数据订阅"""
self.isMarketActive = False
PrintLog(LogLevel.INFO, '- [市场数据订阅已停止]')
# ---- xtquant 回调处理 (xtquant 通过回调对象调用 on_xxx 方法) ----
def on_connected(self):
PrintLog(LogLevel.INFO, f'[QMT] on_connected: 真实 QMT 连接成功 {datetime.datetime.now()}')
def on_disconnected(self):
PrintLog(LogLevel.WARNING, f'[QMT] on_disconnected: 真实 QMT 连接断开 {datetime.datetime.now()}')
def on_stock_order(self, order):
self._pending_orders.append(order)
def on_stock_trade(self, trade):
eBus.event_bus.publish(eBus.MarketOrderTraded, trade)
def on_order_stock_async_response(self, response):
eBus.event_bus.publish(eBus.MarketOrderCreated, response)
def on_order_error(self, order_error):
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):
PrintLog(LogLevel.INFO, f'[QMT] on_account_status: {datetime.datetime.now()} {status}')
qmtv = RealQmtV()
+3
View File
@@ -0,0 +1,3 @@
# 删除交易标的事件
EventTradeTargetUpdate = "trade_target_update"
EventTradeTargetDeleted = "trade_target_deleted"
+60
View File
@@ -0,0 +1,60 @@
from peewee import CharField, IntegerField, FloatField, BooleanField
from core.database import BaseModel, db
# 策略类型常量
STRATEGY_TYPE_UNCLASSIFIED = 0 # 未分类持仓
STRATEGY_TYPE_GRID = 1 # 网格策略
# 定义Target类,对应targets表
class SFGridTradeTarget(BaseModel):
stock_code = CharField(unique=True)
stock_name = CharField()
current_position = IntegerField()
grid_index = IntegerField(default=0)
init_price = FloatField(null=True) # 建仓成本
grid_match_count = IntegerField(default=0)
grid_total_profit = FloatField(default=0.0)
status = IntegerField(default=0) # 已废弃,改用 strategy_type + grid_index
enabled = BooleanField(default=False) # 是否启动交易线程
strategy_type = IntegerField(default=0) # 0=未分类, 1=网格策略
grid_start_price = FloatField(default=10.0) # 基线价格
grid_size = FloatField(default=1.0) # 网格价位差
grid_volume = IntegerField(default=200) # 网格交易量
grid_upper_count = IntegerField(default=1) # 基线价格上方网格数
grid_lower_count = IntegerField(default=10) # 基线价格下方网格数
def targetName(self):
return f'{self.stock_code}-{self.stock_name}'
def getPriceGrid(self) -> list:
self.priceGrid: list = []
# 网格大小,数量
if self.priceGrid is None or len(self.priceGrid) == 0:
for i in range(self.grid_upper_count): # type: ignore
upperPrice = self.grid_start_price + (self.grid_upper_count - i) * self.grid_size
self.priceGrid.append(round(upperPrice, 3))
self.priceGrid.append(self.grid_start_price)
for i in range(self.grid_lower_count): # type: ignore 5
lowerPrice = self.grid_start_price - (i + 1) * self.grid_size
self.priceGrid.append(round(lowerPrice, 3))
return self.priceGrid
db.create_tables([SFGridTradeTarget])
# 数据库迁移: 为已有表添加 strategy_type 字段(如果不存在)
try:
from playhouse.migrate import migrate, SqliteMigrator
migrator = SqliteMigrator(db)
migrate(
migrator.add_column('sfgridtradetarget', 'strategy_type', SFGridTradeTarget.strategy_type),
)
except Exception:
# 字段已存在或迁移失败 — 静默跳过
pass
+518
View File
@@ -0,0 +1,518 @@
"""
网格交易策略控制器
核心逻辑:在预设的价格网格上低买高卖,每个网格节点同时挂一对买卖单,
成交后自动切换到相邻网格并刷新订单。
网格结构示意(以 grid_index 为中心):
价格从高到低排列在 getPriceGrid() 列表中
grid_index=0 是最低价(底部),越大价格越高(顶部)
卖出方向(上移): grid_index - 1 (价格更低,空单)
买入方向(下移): grid_index + 1 (价格更高,多单)
成交 → 上移一格(卖出成交): grid_index -= 1,赚取一格差价
成交 → 下移一格(买入成交): grid_index += 1,持仓成本降低
状态机:
status=0: 未建仓,需先下建仓单买入初始仓位
status=1: 已建仓,运行网格交易(上下各挂一单)
"""
from core.logger import LogLevel, PrintLog
from core.qmt import qmtv
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
from xtquant import xtconstant
from xtquant.xttype import XtOrderError, XtOrderResponse, XtTrade
import threading
import core.eventbus as eBus
class SFGridStrategy:
"""
单标的网格交易策略控制器
每个 SFGridTradeTarget 数据库记录对应一个 SFGridStrategy 实例。
负责:建仓 → 挂网格单 → 监听成交/错误事件 → 调整网格 → 刷新订单。
订单 remark 格式: "{订单类型},{网格索引},{股票代码}"
例: "BUY,3,000001" 表示在网格索引 3 处挂买入单,标的 000001
例: "INIT,1,000001" 表示建仓单,建仓在网格索引 1
"""
def __init__(self, tradeTarget: model.SFGridTradeTarget):
"""
初始化网格策略控制器
参数:
tradeTarget: 数据库中的交易标记录,包含网格参数、当前状态等
"""
self.tradeTarget: model.SFGridTradeTarget = tradeTarget
# 订阅事件总线:监听订单创建、成交、失败三种事件
event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync)
event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade)
event_bus.subscribe(eBus.MarketOrderError, self.onOrderError)
# 获取当日涨跌停价格(用于价格边界校验)
self.todayUpStopPrice = qmtv.dailyUpStop(tradeTarget.stock_code) # type: ignore
self.todayDownStopPrice = qmtv.dailyDownStop(tradeTarget.stock_code) # type: ignore
PrintLog(LogLevel.INFO,
f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造开始: '
f'网格={tradeTarget.grid_index}, 启用={tradeTarget.enabled}')
# orderGrid: 网格索引 → 订单编号(seq 或 order_id)的映射
# seq 是 xtquant 返回的下单序号(下单瞬间),order_id 是交易所返回的正式订单号(异步回调后更新)
self.orderGrid = {} # {grid_index: order_seq | order_id}
# 加载券商侧已存在的未成交订单,恢复到 orderGrid 中
self.loadExistOrders()
# 数据更新锁:保护 orderGrid 和 tradeTarget 的并发访问
# QMT 回调在独立线程中触发,必须在可能触发回调的操作之前创建
self.dataUpdateLock = threading.Lock()
# 根据数据库中的 enabled 字段决定是否启动交易
self.enabledTrading(tradeTarget.enabled) # type: ignore
PrintLog(LogLevel.INFO,
f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造结束: '
f'grid_index={self.tradeTarget.grid_index}')
# ── 订单加载 ──────────────────────────────────────────────
def loadExistOrders(self):
"""
从券商侧加载该策略的未成交订单,恢复到 orderGrid
用于程序重启后恢复状态:数据库中可能没有记录所有挂单,
通过 queryPendingOrder 从 QMT 获取实际存在的订单。
"""
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
for order in orders:
# 只处理本策略的订单(通过 strategy_name 过滤)
if order.strategy_name != self.getName():
continue
parsed = self._parse_remark(order.order_remark)
if parsed is None:
continue
_, gridIdx, _ = parsed
self.orderGrid[gridIdx] = order.order_id
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] 初始化: '
f'加载现有订单, grid-{gridIdx} order_id:{self.orderGrid[gridIdx]}')
def printPendingOrder(self):
"""调试用:打印当前所有挂单"""
for idx, order_id in self.orderGrid.items():
PrintLog(LogLevel.DEBUG, f" {idx} : {order_id}")
# ── 市场状态切换 ──────────────────────────────────────────
def onMarketActiveSwitch(self, isActive: bool):
"""
市场数据状态切换回调(由 UI 层调用)
当市场数据从不可用变为可用时,如果策略已启用则刷新网格订单。
"""
if isActive and self.tradeTarget.enabled:
self.refreshGridOrder()
# ── 核心:网格下单逻辑 ────────────────────────────────────
def refreshGridOrder(self):
"""
刷新网格挂单 —— 策略的核心下单方法
逻辑分支:
1. 前置检查: 市场未激活 或 策略未启用 → 跳过不下单
2. status=0 (未建仓): 下一个建仓单(买入初始仓位)
3. status=1 (已建仓): 在 grid_index 上下各挂一单
- 上方 (sellIdx = grid_index - 1): 挂卖出单(价格更低时卖出获利)
- 下方 (buyIdx = grid_index + 1): 挂买入单(价格更低时补仓)
每个方向都先检查是否已存在同价位订单,避免重复下单
"""
# ── 前置检查:市场和策略状态 ──
if not qmtv.isMarketActive or not self.tradeTarget.enabled:
PrintLog(LogLevel.INFO,
f'|- 市场 {qmtv.isMarketActive}, 策略 {self.getName()} '
f'{self.tradeTarget.enabled}, 不下单')
return
# 获取当前该标的所有未成交订单
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
# ── 统一网格逻辑 ──
# 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
# --- 上方挂卖出单(空单)---
# 条件: grid_index > 0,即当前位置不是价格最低点,还有向下(卖出)空间
if currentIdx > 0:
sellIdx = currentIdx - 1 # 向上一个网格
sellPrice = self.tradeTarget.getPriceGrid()[sellIdx]
sell_remark = self._make_remark(OrderTypeSell, sellIdx)
# 检查是否已存在同 remark 的卖单(避免重复挂单)
if not any(o.order_remark == sell_remark for o in orders):
# 卖单价格超过涨停价 → 今日无法成交,跳过下单
if sellPrice > self.todayUpStopPrice:
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] '
f'上方网格[{sellIdx}]卖价 {sellPrice:.3f} > 涨停价 {self.todayUpStopPrice:.3f}'
f'今日无法下卖单 (当前网格基准 grid-{currentIdx})')
else:
tmpOrderSeq = qmtv.orderAsync(
str(self.tradeTarget.stock_code),
self.tradeTarget.grid_volume,
xtconstant.STOCK_SELL, # 卖出
sellPrice,
xtconstant.FIX_PRICE,
sell_remark,
self.getName(),
)
self.orderGrid[sellIdx] = tmpOrderSeq
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
f'下空单,价格: {sellPrice:.3f}')
else:
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
f'已存在同价位空单,跳过下单')
# --- 下方挂买入单(多单)---
# 条件: grid_index < 价格网格长度-1,即当前位置不是价格最高点,还有向上(买入)空间
if currentIdx < len(self.tradeTarget.getPriceGrid()) - 1:
buyIdx = currentIdx + 1 # 向下一个网格
buyPrice = self.tradeTarget.getPriceGrid()[buyIdx]
buy_remark = self._make_remark(OrderTypeBuy, buyIdx)
# 检查是否已存在同 remark 的买单(避免重复挂单)
if not any(o.order_remark == buy_remark for o in orders):
# 买单价格低于跌停价 → 今日无法成交,跳过下单
if buyPrice < self.todayDownStopPrice:
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] '
f'下方网格[{buyIdx}]买价 {buyPrice:.3f} < 跌停价 {self.todayDownStopPrice:.3f}'
f'今日无法下买单 (当前网格基准 grid-{currentIdx})')
else:
tmpOrderSeq = qmtv.orderAsync(
str(self.tradeTarget.stock_code),
self.tradeTarget.grid_volume,
xtconstant.STOCK_BUY, # 买入
buyPrice,
xtconstant.FIX_PRICE,
buy_remark,
self.getName(),
)
self.orderGrid[buyIdx] = tmpOrderSeq
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
f'下多单,价格: {buyPrice:.3f}')
else:
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
f'已存在同价位多单,跳过下单')
else:
# grid_index 已到达价格网格上边界,无法再挂买入单(价格已经到顶)
PrintLog(LogLevel.INFO,
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
f'已过下边界,停止多单交易')
# ── 标的管理 ──────────────────────────────────────────────
def deleteTradeTarget(self, tradeTarget: model.SFGridTradeTarget):
"""
从数据库中删除该交易标的
同时发布 EventTradeTargetDeleted 事件通知 UI 刷新。
"""
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}信息删除: START')
self.dataUpdateLock.acquire()
try:
tradeTarget.delete_instance()
event_bus.publish(bus_events.EventTradeTargetDeleted, tradeTarget)
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}信息删除: END')
finally:
self.dataUpdateLock.release()
# ── 交易启停控制 ──────────────────────────────────────────
def enabledTrading(self, enabled: bool) -> model.SFGridTradeTarget:
"""
启用或停用该标的的网格交易
启用时 (enabled=True):
- grid_index=0 空仓: 直接调用 refreshGridOrder(只挂买单)
- grid_index>0 有仓: 检查持仓是否满足 grid_volume × grid_index
满足则刷新网格单,不满足则回退 enabled=False(风控保护)
停用时 (enabled=False):
- 取消该标的所有未成交订单,停止交易监控
"""
PrintLog(LogLevel.INFO,
f" |- [DEBUG] enabledTrading({enabled}) 调用前: "
f"grid_index={self.tradeTarget.grid_index}")
self.tradeTarget.enabled = enabled # type: ignore
if enabled:
# ── 启用交易 ──
PrintLog(LogLevel.INFO,
f" |- 标的{self.tradeTarget.targetName()}交易启动, "
f"持仓量:{self.tradeTarget.current_position}")
if self.tradeTarget.grid_index == 0:
# 空仓: refreshGridOrder 会在 grid[1] 挂第一笔买单
PrintLog(LogLevel.INFO,
f" |- 标的{self.tradeTarget.targetName()}空仓, "
f"等待首次买入建仓")
else:
# 有仓: 检查现有持仓是否满足当前网格位置的仓位需求
# 最小需求仓位 = 每格股数 × 当前网格索引
# 例: grid_volume=100, grid_index=3 → 需持股 300 股
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}, '
f'当前持仓:{self.tradeTarget.current_position}, '
f'交易启动失败')
self.tradeTarget.enabled = False # type: ignore
# 刷新网格订单(空仓只挂买单,有仓买卖对冲)
self.refreshGridOrder()
else:
# ── 停用交易: 取消所有未成交订单 ──
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
for order in orders:
try:
qmtv.xt_trader.cancel_order_stock_async(qmtv.account, order.order_id)
except AttributeError:
pass # 模拟模式无 xt_trader,跳过撤单
if len(orders) > 0:
PrintLog(LogLevel.INFO, f' |- 取消未成交订单 {len(orders)}')
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}交易监控暂停")
# 持久化状态到数据库
self.saveProxy()
return self.tradeTarget
def isEnabled(self) -> bool:
"""查询交易是否已启用"""
PrintLog(LogLevel.DEBUG, f'|- 检查交易状态[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - {self.tradeTarget.enabled}')
return bool(self.tradeTarget.enabled)
# ── 事件回调: 订单创建 ────────────────────────────────────
def onOrderCreateAsync(self, response: XtOrderResponse):
"""
QMT 异步下单成功回调
xtquant 下单是异步的:orderAsync() 返回 seq(序号),
交易所确认后通过此回调返回正式的 order_id。
此处将 orderGrid 中的临时 seq 替换为正式 order_id。
"""
parsed = self._filter_event(response.order_remark, response.strategy_name)
if parsed is None:
return
_, gridIdx, _ = parsed
self.dataUpdateLock.acquire()
try:
PrintLog(LogLevel.INFO,
f"委托创建通知 onOrderCreateAsync[{self.tradeTarget.targetName()}]: "
f"{response.order_id}")
# 将 orderGrid 中的临时 seq 替换为正式 order_id
self.orderGrid[gridIdx] = response.order_id
PrintLog(LogLevel.INFO,
f"委托创建通知 onOrderCreateAsync 更新 grid-{gridIdx} "
f"seq:{response.seq} -> order_id:{response.order_id}")
except Exception as e:
PrintLog(LogLevel.ERROR,
f"|- 委托创建通知 onOrderCreateAsync"
f"[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}]: "
f"{response.order_id} - {str(e)}")
finally:
self.dataUpdateLock.release()
# ── 事件回调: 订单失败 ────────────────────────────────────
def onOrderError(self, order_error: XtOrderError):
"""
QMT 委托失败回调
当 xtquant 拒绝订单时触发(如资金不足、代码格式错误、涨跌停限制等)。
清理 orderGrid 中对应网格索引的孤立条目,防止后续 refreshGridOrder
误判"已有同价位订单"而跳过重新下单。
"""
parsed = self._filter_event(order_error.order_remark, order_error.strategy_name)
if parsed is None:
return
_, gridIdx, _ = parsed
self.dataUpdateLock.acquire()
try:
# 从 orderGrid 中移除失败的订单条目,后续 refreshGridOrder 会重新挂单
if gridIdx in self.orderGrid:
del self.orderGrid[gridIdx]
PrintLog(LogLevel.ERROR,
f'委托失败[{self.tradeTarget.targetName()}] grid-{gridIdx}: '
f'order_id={order_error.order_id}, error_id={order_error.error_id}, '
f'error_msg={order_error.error_msg}')
except Exception as e:
PrintLog(LogLevel.ERROR,
f'委托失败处理异常[{self.tradeTarget.stock_code}]: {str(e)}')
finally:
self.dataUpdateLock.release()
# ── 事件回调: 订单成交 ────────────────────────────────────
def onOrderTrade(self, trade: XtTrade):
"""
QMT 委托成交通知回调
收到成交后:
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
_, gridIdx, _ = parsed # gridIdx: 成交订单对应的网格索引(int)
PrintLog(LogLevel.INFO,
f'|- 委托成交通知'
f'[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}] : '
f'{trade.order_id}')
self.dataUpdateLock.acquire()
try:
# ── 首次建仓:记录建仓价 ──
# grid_index==0 表示成交前处于空仓状态,这笔成交就是首次建仓
if self.tradeTarget.grid_index == 0:
self.tradeTarget.init_price = trade.traded_price # type: ignore
# ── 网格方向判断 ──
# 比较成交单的网格索引 vs 当前网格索引,判断价格移动方向
oriIdx = self.tradeTarget.grid_index # 成交前的网格位置
if gridIdx > self.tradeTarget.grid_index:
# 成交单在下方(更大索引 = 更低价格)→ 买入成交,持仓下移
self.tradeTarget.grid_index += 1 # type: ignore
# 首次建仓时 oriIdx==0,加上"建仓单"前缀便于识别
desc = "建仓单(下移)" if oriIdx == 0 else "下移一格"
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 = "上移一格"
else:
# gridIdx == grid_index: 同格成交,正常情况下不会出现
desc = "同格(异常)"
PrintLog(LogLevel.INFO,
f'|- [{self.tradeTarget.targetName()}] '
f'原网格 {oriIdx} → 现网格 {self.tradeTarget.grid_index}'
f'{desc}')
# ── 成交后统一处理 ──
# 1. 持久化状态到数据库(grid_index、持仓量等已变更)
self.saveProxy()
# 2. 从 orderGrid 清理已成交订单(pop 防 xtquant 重复推送 KeyError
self.orderGrid.pop(gridIdx, None)
# 3. 打印成交报告
PrintLog(LogLevel.INFO,
f"|- 成交报告[{self.tradeTarget.targetName()}] : "
f"====================================")
PrintLog(LogLevel.INFO,
f"|- 标的[{self.tradeTarget.targetName()}] "
f"{desc}-单号{trade.order_id}已成交 ")
PrintLog(LogLevel.INFO,
f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
PrintLog(LogLevel.INFO,
f' 手续费 : {trade.commission:.3f}')
# 4. 刷新网格订单:在新的 grid_index 位置重新挂买卖单
self.refreshGridOrder()
finally:
self.dataUpdateLock.release()
# ── 工具方法 ──────────────────────────────────────────────
def _make_remark(self, order_tag: str, grid_idx: int) -> str:
"""构建订单 remark: '{type},{gridIdx},{stockCode}'"""
return f'{order_tag},{grid_idx},{self.tradeTarget.stock_code}'
@staticmethod
def _parse_remark(remark: str):
"""
解析订单 remark → (orderType:str, gridIdx:int, stockCode:str)
格式不符返回 None
"""
if not remark:
return None
parts = remark.split(',')
if len(parts) < 3:
return None
try:
return parts[0], int(parts[1]), parts[2]
except (ValueError, IndexError):
return None
def _filter_event(self, remark: str, strategy_name: str):
"""
事件过滤器:解析 remark 并校验是否属于本策略本标的
通过返回 parsed tuple,不通过返回 None
"""
parsed = self._parse_remark(remark)
if parsed is None:
return None
if strategy_name != self.getName() or self.tradeTarget.stock_code != parsed[2]:
return None
return parsed
def getName(self):
"""返回策略名称,用于在 QMT 中标识订单归属"""
return "SFGRID"
def saveProxy(self):
"""
持久化 tradeTarget 到数据库,并发布 UI 更新事件
每次状态变更后调用,确保数据库与内存一致,
同时通知 UI 刷新表格显示。
"""
PrintLog(LogLevel.DEBUG,
f'|- [DEBUG] saveProxy: {self.tradeTarget.targetName()} '
f'网格={self.tradeTarget.grid_index}')
rc = self.tradeTarget.save()
event_bus.publish(EventTradeTargetUpdate, self.tradeTarget)
return rc
-182
View File
@@ -1,182 +0,0 @@
from re import L
from core import util
from core.strategy_db import TradeTarget
from core.util import queryPendingOrder
from xtquant import xttrader, xtconstant
from xtquant.xttype import StockAccount, XtOrderResponse, XtTrade
import sfgrid_constants
import threading
class SFGridStrategy:
def __init__(self, tradeTarget: TradeTarget, xt_trader: xttrader.XtQuantTrader, account: StockAccount, enabled: bool = False):
self.tradeTarget:TradeTarget = tradeTarget
self.xt_trader: xttrader.XtQuantTrader = xt_trader
self.account:StockAccount = account
self.enabledTrading(enabled)
self.dataUpdateLock = threading.Lock()
def getName(self):
return "SFGRID"
def enabledTrading(self, enabled: bool):
self.tradeTarget.enabled = enabled # type: ignore
self.tradeTarget.save()
pendingOrders = queryPendingOrder(str(self.tradeTarget.stock_code),self.getName(), self.xt_trader,self.account)
if len(pendingOrders) > 0:
print(f' |- 已存在{len(pendingOrders)}订单,全部取消,按需要重下。')
for order in pendingOrders:
self.xt_trader.cancel_order_stock(self.account, order.order_id)
if enabled:
print(f" |- 标的{self.tradeTarget.targetName()}交易启动, position {self.tradeTarget.current_position}")
# 建仓状态检查
if int(self.tradeTarget.current_position) == 0 and int(self.tradeTarget.status) == 0: # type: ignore
self.tradeTarget.grid_index = 1 # type: ignore
self.tradeTarget.save()
self.initBuyOrderId = self.xt_trader.order_stock_async(
self.account,
str(self.tradeTarget.stock_code),
xtconstant.STOCK_BUY,
sfgrid_constants.grid_volume,
xtconstant.FIX_PRICE,
sfgrid_constants.grid_price[int(self.tradeTarget.grid_index)], # type: ignore
'sf_grid', f'{self.tradeTarget.stock_code}_init_buy')
print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 建初始仓 买单已发出 InitBuyOrderSeq: {self.initBuyOrderId} Price: {sfgrid_constants.grid_price[int(self.tradeTarget.grid_index)]} Volume: {sfgrid_constants.grid_volume}\n") # type: ignore
else:
# 交易阶段,检查仓位,检查现有订单
print(f" |- 标的{self.tradeTarget.targetName()}已有仓位或非初始状态 无需建初始仓 当前仓位: {self.tradeTarget.current_position} 状态: {self.tradeTarget.status}")
minRequirePosition:int = sfgrid_constants.grid_volume * int(self.tradeTarget.grid_index) # type: ignore
if minRequirePosition <= int(self.tradeTarget.current_position): # type: ignore
print(f' |- 仓位检查: 持仓需求充足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}')
else:
print(f' |- 仓位检查: 持仓需求不足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}')
def isEnabled(self) -> bool:
return bool(self.tradeTarget.enabled)
def onDataUpdate(self, data):
print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - START')
self.dataUpdateLock.acquire()
print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - LOCKED')
try:
index = self.tradeTarget.grid_index
price = sfgrid_constants.grid_price[int(index)] # pyright: ignore[reportArgumentType]
lowPrice = sfgrid_constants.grid_price[int(index) + 1] if len(sfgrid_constants.grid_price)>int(index) + 1 else -1.0 # pyright: ignore[reportArgumentType]
highPrice = sfgrid_constants.grid_price[int(index) - 1] # pyright: ignore[reportArgumentType]
lastPrice = float("{:.3f}".format(data[self.tradeTarget.stock_code]['lastPrice']))
print(f"|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - 价格: {lastPrice}, 网格序号: {index}, 网格价格: {price}, 计划多单价: {lowPrice}, 计划空单价: {highPrice}")
if lastPrice <= lowPrice: # 下下方多单
orders = queryPendingOrder(str(self.tradeTarget.stock_code), self.getName(), self.xt_trader, self.account)
if len([order for order in orders if order.order_type == xtconstant.STOCK_BUY and order.price == lowPrice]) > 0:
# 已存在未交易的多单
print(f' |- 已存在未交易的多单,不重复下单')
else:
print(f' |- 下网格多单')
self.tradeTarget.current_buy_order_no = self.xt_trader.order_stock_async(
self.account,
str(self.tradeTarget.stock_code),
xtconstant.STOCK_BUY,
sfgrid_constants.grid_volume,
xtconstant.FIX_PRICE,
lowPrice,
self.getName(), # strategy_name
self.tradeTarget.stock_code # remark # type: ignore
)
self.tradeTarget.current_buy_price = float(lowPrice) # type: ignore
print(f' |- 下网格多单号 {self.tradeTarget.current_buy_order_no}, 网格基准价 {price}, 下单价 {lowPrice}, 下单量 {sfgrid_constants.grid_volume}')
elif lastPrice == highPrice: # 下上方空单
orders = queryPendingOrder(str(self.tradeTarget.stock_code), self.getName(), self.xt_trader, self.account)
if len([order for order in orders if order.order_type == xtconstant.STOCK_SELL and order.price == highPrice]) > 0:
# 已存在未交易的空单
print(f' |- 已存在未交易的空单,不重复下单')
else:
print(f' |- 下网格空单')
self.tradeTarget.current_sell_order_no = self.xt_trader.order_stock_async(
self.account,
str(self.tradeTarget.stock_code),
xtconstant.STOCK_SELL,
sfgrid_constants.grid_volume,
xtconstant.FIX_PRICE,
highPrice,
self.getName(),
self.tradeTarget.stock_code) # type: ignore
self.tradeTarget.current_sell_price = float(highPrice) # type: ignore
print(f' |- 下网格空单号 {self.tradeTarget.current_sell_order_no}, 网格基准价 {price}, 下单价 {highPrice}, 下单量 {sfgrid_constants.grid_volume}')
self.tradeTarget.save()
finally:
print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - release lock')
self.dataUpdateLock.release()
print(f'|- 市价更新[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - END')
def onAsyncOrderResponse(self, order:XtOrderResponse):
print(f' |- 委托回调[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{order.order_id}]:START')
self.dataUpdateLock.acquire()
print(f' |- 委托回调[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{order.order_id}]:LOCKED')
try:
stockCode = order.order_remark
orderSeq = order.seq
if (self.tradeTarget.status == 1): # 正常交易阶段订单下单成功
if self.tradeTarget.current_buy_order_no == order.seq:
self.tradeTarget.current_buy_order_no = order.order_id
elif self.tradeTarget.current_sell_order_no == order.seq:
self.tradeTarget.current_sell_order_no = order.order_id
else:
print(f' |- 委托回调[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{order.order_id}]: 不在策略监控范围内')
rc = self.tradeTarget.save()
finally:
print(f' |- 委托回调[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{order.order_id}]:release lock')
self.dataUpdateLock.release()
print(f' |- 委托回调[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{order.order_id}]:END')
def onOrderTrade(self, trade:XtTrade):
print(f' |- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}]:START')
self.dataUpdateLock.acquire()
print(f' |- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}]:LOCKED')
try:
if int(self.tradeTarget.status) == 0 and trade.order_id == self.initBuyOrderId : # type: ignore
# 此时为建仓成交
self.tradeTarget.current_position = int(self.tradeTarget.current_position) + trade.traded_volume # 当前持仓数,账户原有持仓不在策略范围内 # type: ignore
self.tradeTarget.last_trade_price = float(trade.traded_price) # type: ignore
self.tradeTarget.grid_index = 1 # type: ignore
self.tradeTarget.status = 1 # type: ignore
self.tradeTarget.save()
print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 建初始仓订单ID: {self.initBuyOrderId}已成交 ")
print(f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
print(f' 当前持仓: {self.tradeTarget.current_position}')
print(f' 网格坐标: {self.tradeTarget.grid_index}')
elif trade.order_id == self.tradeTarget.current_sell_order_no and int(self.tradeTarget.status) == 1: # type: ignore
# 上涨一格:此时空单成交
self.tradeTarget.current_position = int(self.tradeTarget.current_position) - trade.traded_volume # type: ignore
self.tradeTarget.last_trade_price = float(trade.traded_price) # type: ignore
self.tradeTarget.grid_index = int(self.tradeTarget.grid_index) - 1 # type: ignore
self.tradeTarget.save()
print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 上涨 卖单已成交 订单ID: {self.tradeTarget.current_sell_order_no} Price: {sfgrid_constants.grid_price[int(self.tradeTarget.grid_index)]} Volume: {sfgrid_constants.grid_volume} 手续费: {trade.commission}\n") # type: ignore
print(f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
print(f' 当前持仓: {self.tradeTarget.current_position}')
print(f' 网格坐标: {self.tradeTarget.grid_index}')
elif trade.order_id == self.tradeTarget.current_buy_order_no and int(self.tradeTarget.status) == 1: # type: ignore
# 下跌一格:此时多单成交
self.tradeTarget.current_position = int(self.tradeTarget.current_position) + trade.traded_volume # type: ignore
self.tradeTarget.last_trade_price = float(trade.traded_price) # type: ignore
self.tradeTarget.grid_index = int(self.tradeTarget.grid_index) + 1 # type: ignore
self.tradeTarget.save()
print(f"|- 标的{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} 下跌 买单已成交 订单ID: {self.tradeTarget.current_buy_order_no} Price: {trade.traded_price} Volume: {sfgrid_constants.grid_volume} 手续费: {trade.commission}")
print(f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
print(f' 当前持仓: {self.tradeTarget.current_position}')
print(f' 网格坐标: {self.tradeTarget.grid_index}')
else:
# 打印订单信息和订单状态
print(f'|- 非策略内部订单,或订单状态不满足监控条件 {trade.order_id} {trade.stock_code}-{trade.instrument_name} {trade.commission}')
finally:
print(f' |- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}]:release lock')
self.dataUpdateLock.release()
print(f' |- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}]:END')
-26
View File
@@ -1,26 +0,0 @@
from peewee import SqliteDatabase, Model, CharField, IntegerField, FloatField, BooleanField
# 连接到SQLite数据库
db = SqliteDatabase('example.db')
# 定义基础模型类
class BaseModel(Model):
class Meta:
database = db
# 定义Target类,对应targets表
class TradeTarget(BaseModel):
stock_code = CharField(unique=True)
stock_name = CharField()
current_position = IntegerField()
grid_index = IntegerField()
last_trade_price = FloatField()
current_buy_price = FloatField()
current_buy_order_no = CharField(default='')
current_sell_price = FloatField()
current_sell_order_no = CharField(default='')
status = IntegerField(default=0) # 0表示新标的,1表示已建初始仓,正常交易中
enabled = BooleanField(default=False) # 是否启动交易线程
def targetName(self):
return f'{self.stock_name}[{self.stock_code}]'
View File
View File
+836
View File
@@ -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 = []
_TERMINAL = {54, 56, 57}
for o in self._orders:
if _plain(getattr(o, 'stock_code', '')) != stock_code:
continue
if getattr(o, 'order_status', 0) in _TERMINAL:
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 去重
_TERMINAL = {54, 56, 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 _TERMINAL:
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)
File diff suppressed because it is too large Load Diff
View File
+142
View File
@@ -0,0 +1,142 @@
import tkinter as tk
from tkinter import ttk
from core.logger import LogLevel, LogData, PrintLog
from core.ui.tkinter.sfgrid_view import TradeTargetUI
# 检测运行环境,决定使用真实或模拟 QMT
def get_qmt_module():
try:
# 尝试导入真实 QMT,如果失败则使用模拟
from core.qmt import qmtv
return qmtv
except ImportError:
from core.qmt_dummy import qmtv
return qmtv
qmtv = get_qmt_module()
from core.eventbus import EventPrintLog
from core.eventbus import event_bus as eBus
class MainWindow:
def __init__(self, configLogLevel:str, progress=None):
self.root = tk.Tk()
self.root.title("神之一手 - 交易系统")
self.root.geometry("1400x700")
self.logLevel = LogLevel[configLogLevel]
PrintLog(LogLevel.DEBUG, f"系统启动成功 {self.logLevel.name}")
# 存储各个Frame的引用
self.strategy_frames = {}
# 日志面板可见性标志
self.log_visible = False
self.create_ui(progress)
eBus.subscribe(EventPrintLog, self.on_log_event)
def create_ui(self, progress=None):
"""创建UI界面"""
# 主容器
main_container = ttk.Frame(self.root)
main_container.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 中间主体区域
content_area = ttk.Frame(main_container)
content_area.pack(fill=tk.BOTH, expand=True)
# 右侧内容区域容器
self.content_container = ttk.Frame(content_area)
self.content_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# 创建策略Frame
strategy_names = ["网格"]
self.create_strategy_frames(strategy_names, progress)
# 创建全局日志面板(默认隐藏)
self.create_global_log_panel(main_container)
# 默认显示第一个策略
self.show_strategy_frame(0)
def create_global_log_panel(self, parent):
"""创建全局日志面板"""
# 日志区域(默认隐藏)
self.log_frame = ttk.LabelFrame(parent, text="操作日志", padding=10)
# 默认不显示,通过工具栏按钮控制
# 创建日志表格
columns = ("timestamp", "level", "message")
self.log_table = ttk.Treeview(self.log_frame, columns=columns, show='headings', height=8)
log_column_configs = {
"timestamp": ("时间", 100),
"level": ("级别", 50),
"message": ("消息", 1150) # 调整宽度适应全局布局
}
for col in columns:
title, width = log_column_configs[col]
self.log_table.heading(col, text=title)
self.log_table.column(col, width=width, anchor=tk.W)
# 添加初始日志
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.log_table.insert('', tk.END, values=(timestamp, "INFO", "系统启动成功"))
# 滚动条
scrollbar = ttk.Scrollbar(self.log_frame, orient=tk.VERTICAL, command=self.log_table.yview)
self.log_table.configure(yscrollcommand=scrollbar.set)
self.log_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
def on_log_event(self, event:LogData):
if self.logLevel.value <= event.level.value:
self.add_log(event.level, event.message)
def add_log(self, level:LogLevel, message):
"""添加日志记录 - 全局方法"""
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.log_table.insert('', 0, values=(timestamp, level.name, message))
def clear_logs(self):
"""清空日志记录"""
# 删除所有日志项
for item in self.log_table.get_children():
self.log_table.delete(item)
def create_strategy_frames(self, strategy_names, progress=None):
"""创建各个策略的Frame"""
frame = TradeTargetUI(self.content_container, progress=progress)
self.strategy_frames[0] = frame
def show_strategy_frame(self, index):
"""显示策略Frame"""
if index in self.strategy_frames:
self.strategy_frames[index].pack(fill=tk.BOTH, expand=True)
def toggle_log_panel(self):
"""切换日志面板的显示/隐藏"""
if self.log_visible:
self.log_frame.pack_forget()
self.log_visible = False
else:
self.log_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5, 0))
self.log_visible = True
def on_exit(self):
"""退出程序"""
from tkinter import messagebox
result = messagebox.askyesno("确认退出", "确定要退出系统吗?")
if result:
self.root.destroy()
def run(self):
"""运行程序"""
self.root.mainloop()
File diff suppressed because it is too large Load Diff
+112
View File
@@ -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
+1 -35
View File
@@ -1,12 +1,5 @@
from typing import Any
import sfgrid_constants
import xtquant.xtconstant as xtconstant
from xtquant import xtdata, xttrader
from xtquant.xttype import StockAccount, XtOrder, XtPosition
import datetime import datetime
def is_trading_time(): def is_trading_time():
""" """
判断当前时间是否在周一至周五的9:30~11:30或13:00~15:00时间段内 判断当前时间是否在周一至周五的9:30~11:30或13:00~15:00时间段内
@@ -40,30 +33,3 @@ def is_trading_time():
return False return False
def getInstrumentName(stock_code):
# print(f"getInstrumentName: 获取标的名称 {stock_code}")
detail = xtdata.get_instrument_detail(stock_code, False)
return detail['InstrumentName']
def getStockPosition(stock_code: str, xt_trader: xttrader.XtQuantTrader, account: StockAccount):
volume = 0
positions = xt_trader.query_stock_positions(account)
if positions:
for temp in positions:
pos:XtPosition = temp
if pos.stock_code == stock_code:
volume = pos.volume
break
return volume
def minPosition(gridIndex:int):
return sfgrid_constants.grid_volume * gridIndex
def queryPendingOrder(stock_code:str, tag: str, xt_trader: xttrader.XtQuantTrader, account: StockAccount) -> list[XtOrder]:
if stock_code == None or tag == None:
return []
orders = xt_trader.query_stock_orders(account)
result = [order for order in orders if order.order_status == xtconstant.ORDER_REPORTED and order.stock_code == stock_code and order.strategy_name == tag]
return result
BIN
View File
Binary file not shown.
+98
View File
@@ -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 |
+259
View File
@@ -0,0 +1,259 @@
from kuanke.wizard import *
from jqdata import *
import pandas as pd
import numpy as np
# ==================== 初始化 ====================
def initialize(context):
set_params(context)
# 开启防未来函数
set_option('avoid_future_data', True)
# 用真实价格交易
set_option('use_real_price', True)
# 过滤order中低于error级别的日志
log.set_level('order', 'error')
log.set_level('system', 'error')
log.set_level('strategy', 'debug')
set_benchmark('000001.XSHG')
set_order_cost(OrderCost(open_tax=0, close_tax=0.001, open_commission=0.0002, close_commission=0.0002, min_commission=5), type='stock')
set_slippage(FixedSlippage(0.01))
run_daily(before_trading, '9:30')
# -------------------- 参数设置 --------------------
def set_params(context):
context.max_price = 6
context.min_price = 5.01
context.grid_base_min = 1 # 最小价格
context.grid_base_max = 5 # 建仓价格
context.grid_interval = 0.5 # 下跌n元加仓
context.profit_target = 0.5 # 上涨n元清仓
context.min_stocks = 10
context.max_stocks = 25
context.base_max_stocks = 25
context.max_layers = 7
context.base_position_pct = 0.15
context.max_position_pct = 0.15
context.target_usage = 0.98
context.reserve_ratio = 0.02
context.first_round_max = 10
context.add_batch_size = 3
context.add_cash_threshold = 0.4
g.stock_pool = []
g.grid_info = {}
g.monitoring_stocks = set()
g.first_round_done = False
# ==================== 盘前 ====================
def before_trading(context):
january_clear(context)
if context.current_dt.month == 1:
return
stock_pool = get_stock_pool(context)
g.stock_pool = stock_pool
g.monitoring_stocks.update([s for s in stock_pool if s not in g.grid_info])
g.first_round_done = len(g.grid_info) >= context.first_round_max
# -------------------- 股票池 --------------------
def get_stock_pool(context):
# 1. 全部 A 股(不含退市)
df_sec = get_all_securities(types=['stock'], date=context.previous_date)
codes = list(df_sec.index)
# 2. 过滤 ST、科创板、北交所
def is_valid(code):
name = df_sec.loc[code, 'display_name']
if 'ST' in name or '退' in name or 'st' in name:
return False
if code.startswith('688'): # 科创板
return False
if code.startswith('83') or code.startswith('87') or code.startswith('9'): # 北交所
return False
return True
codes = [c for c in codes if is_valid(c)]
if not codes:
return []
# 3. 过滤停牌 & 价格区间
try:
price_df = get_price(codes,
end_date=context.current_dt,
count=1,
fields=['pre_close'],
panel=False)
if price_df is None or price_df.empty:
return []
# 过滤价格区间
price_df = price_df[
(price_df['pre_close'].notna()) &
(price_df['pre_close'] >= context.min_price) &
(price_df['pre_close'] <= context.max_price)
]
valid_codes = price_df['code'].tolist()
except Exception as e:
log.error(f"获取价格数据失败: {e}")
return []
if not valid_codes:
return []
# 4. 过滤停牌(开盘价缺失)
try:
open_df = get_price(valid_codes,
end_date=context.current_dt,
count=1,
fields=['open'],
panel=False)
if open_df is None or open_df.empty:
return []
# 过滤掉开盘价为空的股票
open_df = open_df[open_df['open'].notna()]
final_codes = open_df['code'].tolist()
except Exception as e:
log.error(f"获取开盘价数据失败: {e}")
return []
return final_codes
# -------------------- 一月清仓 --------------------
def january_clear(context):
if context.current_dt.month == 1:
log.info("进入1月,执行年度清仓...")
for stock in list(context.portfolio.positions.keys()):
order_target(stock, 0)
if stock in g.grid_info:
del g.grid_info[stock]
g.monitoring_stocks.add(stock)
# ==================== 盘中 ====================
def handle_data(context, data):
if context.current_dt.month == 1:
return
manage_positions(context, data)
usage = (context.portfolio.total_value - context.portfolio.available_cash) / context.portfolio.total_value
dynamic_max = get_dynamic_max_stocks(context)
if len(g.grid_info) < dynamic_max and usage < context.target_usage:
try_build_new(context, data)
# -------------------- 动态上限 --------------------
def get_dynamic_max_stocks(context):
return context.max_stocks if g.first_round_done else context.first_round_max
# -------------------- 建仓 --------------------
def try_build_new(context, data):
position_pct = context.max_position_pct
dynamic_max = get_dynamic_max_stocks(context)
count = 0
for stock in list(g.monitoring_stocks):
if len(g.grid_info) >= dynamic_max or count >= 3:
break
price = data[stock].close
if context.grid_base_min <= price <= context.grid_base_max:
total_value = context.portfolio.total_value
stock_amount = total_value * position_pct
grid = GridInfo(price, stock_amount, context.max_layers, context.grid_interval, context.profit_target)
layer_amount = grid.get_layer_amount(0)
buy_amount = int(layer_amount / price / 100) * 100
if buy_amount > 0:
order(stock, buy_amount)
grid.add_position(price, buy_amount, 0)
g.grid_info[stock] = grid
g.monitoring_stocks.discard(stock)
count += 1
log.info(f"[建仓] {stock} 价格{price:.2f} 数量{buy_amount}")
# -------------------- 管理持仓 --------------------
def manage_positions(context, data):
for stock, grid in list(g.grid_info.items()):
price = data[stock].close
# 止盈
sellable = grid.get_sellable_positions(price)
if sellable:
for idx, pos in reversed(sellable):
order(stock, -pos['amount'])
grid.remove_position(idx)
profit = (price - pos['price']) * pos['amount']
log.info(f"[止盈] {stock} 盈利{profit:.2f}")
# 加仓
layer = grid.should_add_layer(price)
if layer is not None:
layer_amount = grid.get_layer_amount(layer)
buy_amount = int(layer_amount / price / 100) * 100
if buy_amount > 0:
order(stock, buy_amount)
grid.add_position(price, buy_amount, layer)
log.info(f"[加仓] {stock} 层级{layer} 数量{buy_amount}")
else:
log.info(f"[加仓失败] {stock} 层级{layer} 金额不足")
# 清仓
if len(grid.positions) == 0:
del g.grid_info[stock]
g.monitoring_stocks.add(stock)
log.info(f"[清仓] {stock}")
# ==================== 盘后 ====================
def after_trading_end(context):
log.info(f"持仓数:{len(g.grid_info)},监控数:{len(g.monitoring_stocks)}")
# ==================== 网格类 ====================
class GridInfo:
def __init__(self, base_price, total_amount, max_layers, interval, profit_target):
self.base_price = float(base_price)
self.total_amount = float(total_amount)
self.max_layers = int(max_layers)
self.interval = float(interval)
self.profit_target = float(profit_target)
self.layer_prices = {i: base_price - i * interval for i in range(self.max_layers)}
self.layer_weights = self._calc_weights()
self.positions = []
def _calc_weights(self):
weights = {i: 1.0 + 0.05 * i for i in range(self.max_layers)}
total = sum(list(weights.values()))
return {k: v / total for k, v in weights.items()}
def get_layer_amount(self, layer):
return self.total_amount * self.layer_weights[layer]
def add_position(self, price, amount, layer):
self.positions.append({'price': price, 'amount': amount, 'layer': layer})
def get_sellable_positions(self, current_price):
return [(i, p) for i, p in enumerate(self.positions) if current_price >= p['price'] + self.profit_target]
def remove_position(self, index):
return self.positions.pop(index)
def should_add_layer(self, current_price):
for layer in range(self.max_layers):
target = self.layer_prices[layer]
diff = abs(current_price - target)
# 获取该层级的所有持仓
layer_positions = [p for p in self.positions if p['layer'] == layer]
has_position = len(layer_positions) > 0
if diff <= 0.1 and not has_position:
return layer
return None
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

-27
View File
@@ -1,27 +0,0 @@
from typing import List
import configparser
# miniQMTPath = r'D:\\Programs\\DTQMT_MN\\userdata_mini' # miniQMT软件的安装路径
miniQMTPath = r'D:\\Programs\\DTQMT\\userdata_mini' # miniQMT软件的安装路径
# miniQMTPath = ''
# grid_price = [11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1] # 网格价格设置,从高到低
grid_price:List[float] = [] # 网格价格设置,从高到低
grid_volume:int = 100 # 每个网格的交易手数
account_no:str = '99082560'
# account_no:str = '89009170' # 交易账号
max_enabled_targets:int = 10
def initConfig():
global miniQMTPath, grid_price, grid_volume, account_no, max_enabled_targets
config = configparser.ConfigParser()
config.read('config.ini')
miniQMTPath = config.get('config','miniQMTPath')
print(f'QMTPath: {miniQMTPath}')
str_list = config.get('config','grid_price').split(',')
grid_price = [float(item) for item in str_list]
print(f'网格设置:{grid_price}')
grid_volume = config.getint('config','grid_volume')
# account_no = config.get('config','account_no')
print(f'账号: {account_no}')
max_enabled_targets = config.getint('config','max_enabled_targets')
print(f'最大启用目标数: {max_enabled_targets}')
+19 -78
View File
@@ -1,82 +1,23 @@
# coding:utf-8 # coding:utf-8
from mimetypes import init """
启动入口 — 自动探测 QMT 环境。
默认使用 Tkinter UI,使用 --flet 参数切换到 Flet (Flutter) UI。
"""
import sys import sys
sys.stdout.reconfigure(encoding='utf-8') # 设置标准输出编码为UTF-8 # type: ignore
from core import strategy_db
# from core.main_controller import SFGridController
# import core.util as util
import sfgrid_constants as sdConstants
# from xtquant import xtdata
import ui
def interact():
"""执行后进入repl模式"""
import code
code.InteractiveConsole(locals=globals()).interact()
# def startMarketData():
# ctrl.startMarketData()
# def stopMarketData():
# ctrl.stopMarketData()
# def pool():
# ctrl.print_pool()
# def addTarget(stock_code):
# ctrl.add_trade_target(stock_code)
# def delTarget(index:int):
# ctrl.del_trade_target(index)
# def accountInfo():
# ctrl.print_account_info()
# def positionInfo():
# ctrl.print_position_info()
# def startTrade(index:int):
# ctrl.start_stock_trade(index)
# def pauseTrade(index:int):
# ctrl.pause_stock_trade(index)
# def stockTradeCtrl(index: int):
# return ctrl.stock_trade_ctrl[ctrl.instrument_pool[index].stock_code]
def help():
print("基础指令:")
print(" ===================================================")
print(" startMarketData() - 启动市场数据接收")
print(" stopMarketData() - 停止市场数据接收\n")
print(" pool() - 打印标的池信息")
print(" addTarget(stock_code) - 添加交易标的")
print(" delTarget(index) - 删除交易标的\n")
print(" accountInfo() - 打印账户信息")
print(" positionInfo() - 打印持仓信息\n")
print(" startTrade(index) - 启动标的交易")
print(" pauseTrade(index) - 暂停标的交易")
print(" ===================================================")
print("内部指令:")
print(" stockTradeCtrl(index) - 获取标的交易控制器")
print(" ctrl - 访问控制器实例")
if __name__ == '__main__': if __name__ == '__main__':
if '--flet2' in sys.argv:
strategy_db.db.connect() from core.ui.flet.app_v2 import run
strategy_db.db.create_tables([strategy_db.TradeTarget]) run()
print('- [成功]数据库模块初始化') elif '--flet' in sys.argv:
from core.ui.flet.app import run
targets = strategy_db.TradeTarget.select() run()
else:
from tkinter import messagebox
app = ui.TradeTargetUI(trade_targets=targets) from core.ui.tkinter.splash import SplashWindow
app.run() try:
window = SplashWindow().run()
# sdConstants.initConfig() if window:
# print(f'{sdConstants.account_no} : {sdConstants.miniQMTPath}') window.run()
# ctrl: SFGridController = SFGridController(sdConstants.account_no, sdConstants.miniQMTPath) except Exception as e:
# if ctrl.inited: messagebox.showerror("错误", f"系统初始化失败: {str(e)}")
# interact()
# else:
# print("控制器初始化失败")
+38
View File
@@ -0,0 +1,38 @@
# -*- mode: python ; coding: utf-8 -*-
a = Analysis(
['starter.py'],
pathex=[],
binaries=[],
datas=[('xtquant/xtdata.ini', 'xtquant')], # xtdata 依赖的配置文件
hiddenimports=['brotli', 'brotli.encoding'],
hookspath=[],
hooksconfig={},
runtime_hooks=[],
excludes=['PyQt5', 'PyQt6', 'PySide2', 'PySide6', 'matplotlib', 'numpy', 'pandas', 'jupyter', 'notebook', 'ipython'], # 排除不必要的包
noarchive=False,
optimize=2, # 启用最高级别优化
)
pyz = PYZ(a.pure)
exe = EXE(
pyz,
a.scripts,
a.binaries,
a.datas,
[],
name='神之一手',
debug=False,
bootloader_ignore_signals=False,
strip=False,
upx=False,
upx_exclude=[],
runtime_tmpdir=None,
console=False,
disable_windowed_traceback=False,
argv_emulation=False,
target_arch=None,
codesign_identity=None,
entitlements_file=None,
icon='logo.ico',
)
-667
View File
@@ -1,667 +0,0 @@
import random
import tkinter as tk
from tkinter import ttk, messagebox, filedialog
from typing import List, Optional
from datetime import datetime
from core.strategy_db import TradeTarget
import configparser
import sfgrid_constants
class TradeTargetUI:
def __init__(self, trade_targets: Optional[List[TradeTarget]] = None):
if trade_targets is not None:
self.trade_targets = trade_targets
else:
self.trade_targets = []
self.root = tk.Tk()
self.root.title("三疯交易系统")
self.root.geometry("1200x700")
# 创建界面
self.create_ui()
def create_ui(self):
"""创建UI界面"""
# 创建菜单栏
self.create_menu_bar()
# 主框架
main_frame = ttk.Frame(self.root)
main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 表格区域
self.create_tables_area(main_frame)
def create_menu_bar(self):
"""创建菜单栏"""
menubar = tk.Menu(self.root)
self.root.config(menu=menubar)
# 系统菜单
system_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="系统", menu=system_menu)
system_menu.add_command(label="系统设置", command=self.system_settings)
system_menu.add_separator()
system_menu.add_command(label="退出", command=self.root.quit)
def create_tables_area(self, parent):
"""创建表格区域"""
# 上方交易标的区域
trade_frame = ttk.LabelFrame(parent, text="交易标的详情", padding=10)
trade_frame.pack(fill=tk.BOTH, expand=True, pady=(0, 5))
# 创建交易标的表格
self.create_trade_target_table(trade_frame)
# 下方操作日志区域(默认隐藏)
self.log_frame = ttk.LabelFrame(parent, text="操作日志", padding=10)
# 默认不显示,通过工具栏按钮控制
# self.log_frame.pack(fill=tk.X, pady=(5, 0))
self.log_visible = False # 日志区域可见性标志
# 创建操作日志表格
self.create_log_table(self.log_frame)
def create_trade_target_table(self, parent):
"""创建交易标的表格"""
# 创建工具栏
toolbar_frame = ttk.Frame(parent)
toolbar_frame.pack(fill=tk.X, pady=(0, 10))
# 工具栏按钮
ttk.Button(toolbar_frame, text="▶️ 启动交易",
command=self.start_selected_trade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="⏸ 暂停交易",
command=self.pause_selected_trade, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text=" 添加标的",
command=self.add_trade_target, width=12).pack(side=tk.LEFT, padx=2)
ttk.Button(toolbar_frame, text="🗑 删除标的",
command=self.delete_selected_trade, width=12).pack(side=tk.LEFT, padx=2)
# 添加分隔符
ttk.Separator(toolbar_frame, orient='vertical').pack(side=tk.LEFT, fill=tk.Y, padx=10)
# 日志显示/隐藏按钮
self.log_toggle_btn = ttk.Button(toolbar_frame, text="📋 显示日志",
command=self.toggle_log_panel, width=12)
self.log_toggle_btn.pack(side=tk.LEFT, padx=2)
# 添加分隔线
ttk.Separator(parent, orient='horizontal').pack(fill=tk.X, pady=5)
columns = ("ID",
"股票代码", "股票名称", "持仓数量", "网格索引",
"最新成交价", "计划买入价", "买入订单号", "计划卖出价", "卖出订单号",
"启用状态", "交易状态"
)
self.trade_table = ttk.Treeview(parent, columns=columns, show='headings', height=15)
# 专业化的列配置
column_configs = {
"ID": (50, tk.CENTER),
"股票代码": (90, tk.CENTER),
"股票名称": (100, tk.CENTER),
"持仓数量": (90, tk.CENTER),
"网格索引": (80, tk.CENTER),
"最新成交价": (100, tk.CENTER),
"计划买入价": (100, tk.CENTER),
"买入订单号": (100, tk.CENTER),
"计划卖出价": (100, tk.CENTER),
"卖出订单号": (100, tk.CENTER),
"启用状态": (80, tk.CENTER),
"交易状态": (80, tk.CENTER)
}
for col in columns:
width, anchor = column_configs[col]
self.trade_table.heading(col, text=col)
self.trade_table.column(col, width=width, anchor=anchor) # type: ignore
# 填充数据
self.populate_trade_table()
# 滚动条
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.trade_table.yview)
self.trade_table.configure(yscrollcommand=scrollbar.set)
self.trade_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
# 绑定双击事件
self.trade_table.bind("<Double-1>", self.on_table_double_click)
def get_status_indicator(self, target: TradeTarget) -> str:
"""获取状态指示器(带颜色色块的文本)"""
if target.status == 1:
# 绿色圆点表示交易中
return "🟢 已建仓"
elif target.status == 0:
# 黄色圆点表示暂停
return "🟡 未建仓"
else:
return "🔴 错误状态"
def get_trade_status_indicator(self, status: int) -> str:
"""获取交易状态指示器"""
if status == 1:
return "🟢 策略运行"
else:
return "🟡 策略暂停"
def populate_trade_table(self):
"""填充交易标的表格数据"""
for temp in self.trade_targets:
target: TradeTarget = temp
values = [
target.id, # type: ignore
target.stock_code,
target.stock_name,
target.current_position,
target.grid_index,
f"{target.last_trade_price:.2f}",
f"{target.current_buy_price:.2f}",
target.current_buy_order_no,
f"{target.current_sell_price:.2f}",
target.current_sell_order_no,
self.get_status_indicator(target),
self.get_trade_status_indicator(target.status) # type: ignore
]
self.trade_table.insert('', tk.END, values=values)
def create_log_table(self, parent):
"""创建操作日志表格"""
columns = ("timestamp", "level", "message")
self.log_table = ttk.Treeview(parent, columns=columns, show='headings', height=8)
log_column_configs = {
"timestamp": ("时间", 120),
"level": ("级别", 60),
"message": ("消息", 200)
}
for col in columns:
title, width = log_column_configs[col]
self.log_table.heading(col, text=title)
self.log_table.column(col, width=width, anchor=tk.W)
# 填充示例日志
sample_logs = [
("2024-01-15 10:30:15", "INFO", "系统启动成功"),
("2024-01-15 10:31:22", "DEBUG", "加载交易标的: 5个"),
("2024-01-15 10:32:45", "INFO", "000001 - 网格交易线程启动"),
("2024-01-15 10:33:10", "WARNING", "601318 - 未启用交易"),
("2024-01-15 10:34:30", "ERROR", "300750 - 订单提交失败"),
("2024-01-15 10:35:18", "INFO", "600036 - 买入订单创建成功"),
("2024-01-15 10:36:05", "INFO", "数据刷新完成")
]
for log in sample_logs:
self.log_table.insert('', tk.END, values=log)
# 滚动条
scrollbar = ttk.Scrollbar(parent, orient=tk.VERTICAL, command=self.log_table.yview)
self.log_table.configure(yscrollcommand=scrollbar.set)
self.log_table.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
def get_status_text(self, status):
"""获取状态文本"""
status_map = {
0: "新标的",
1: "交易中"
}
return status_map.get(status, "未知")
def on_table_double_click(self, event):
"""表格双击事件"""
selected = self.trade_table.selection()
if selected:
item = selected[0]
values = self.trade_table.item(item)['values']
self.add_log("DEBUG", f"双击查看详情: {values[0]} - {values[1]}")
def get_selected_target(self):
"""获取选中的交易标的"""
selected = self.trade_table.selection()
if not selected:
messagebox.showwarning("未选中", "请先选择一个交易标的")
return None
# 获取选中行的ID
item = selected[0]
values = self.trade_table.item(item)['values']
target_id = values[0]
# 从列表中找到对应的target对象
for target in self.trade_targets:
if target.id == target_id: # type: ignore
return target
return None
def start_selected_trade(self):
"""启动选中的交易"""
target = self.get_selected_target()
if not target:
return
if target.enabled: # type: ignore
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经在运行中")
return
result = messagebox.askyesno(
"确认启动",
f"确定要启动以下交易标的吗?\n\n"
f"股票代码: {target.stock_code}\n"
f"股票名称: {target.stock_name}"
)
if result:
target.enabled = True # type: ignore
self.add_log("INFO", f"已启动交易: {target.stock_code} - {target.stock_name}")
self.refresh_table()
messagebox.showinfo("启动成功", f"已启动 {target.stock_code} ({target.stock_name}) 的交易")
def pause_selected_trade(self):
"""暂停选中的交易"""
target = self.get_selected_target()
if not target:
return
if not target.enabled: # type: ignore
messagebox.showinfo("提示", f"{target.stock_code} ({target.stock_name}) 已经是暂停状态")
return
result = messagebox.askyesno(
"确认暂停",
f"确定要暂停以下交易标的吗?\n\n"
f"股票代码: {target.stock_code}\n"
f"股票名称: {target.stock_name}"
)
if result:
target.enabled = False # type: ignore
self.add_log("INFO", f"已暂停交易: {target.stock_code} - {target.stock_name}")
self.refresh_table()
messagebox.showinfo("暂停成功", f"已暂停 {target.stock_code} ({target.stock_name}) 的交易")
def delete_selected_trade(self):
"""删除选中的交易标的"""
target = self.get_selected_target()
if not target:
return
result = messagebox.askyesno(
"确认删除",
f"确定要删除以下交易标的吗?\n\n"
f"股票代码: {target.stock_code}\n"
f"股票名称: {target.stock_name}\n\n"
f"⚠️ 此操作不可恢复!",
icon='warning'
)
if result:
try:
self.trade_targets.remove(target)
self.add_log("WARNING", f"已删除交易标的: {target.stock_code} - {target.stock_name}")
self.refresh_table()
messagebox.showinfo("删除成功", f"已删除 {target.stock_code} ({target.stock_name})")
except Exception as e:
self.add_log("ERROR", f"删除失败: {str(e)}")
messagebox.showerror("删除失败", f"删除交易标的时出错:{str(e)}")
def add_trade_target(self):
"""添加新的交易标的"""
# TODO: 实现添加交易标的的对话框
messagebox.showinfo("提示", "添加交易标的功能待实现")
self.add_log("INFO", "点击添加交易标的按钮")
def toggle_log_panel(self):
"""切换日志面板的显示/隐藏"""
if self.log_visible:
# 隐藏日志面板
self.log_frame.pack_forget()
self.log_visible = False
self.log_toggle_btn.config(text="📋 显示日志")
else:
# 显示日志面板
self.log_frame.pack(fill=tk.X, pady=(5, 0))
self.log_visible = True
self.log_toggle_btn.config(text="📋 隐藏日志")
def refresh_table(self):
"""刷新表格数据"""
# 清空表格
for item in self.trade_table.get_children():
self.trade_table.delete(item)
# 重新填充
self.populate_trade_table()
def add_log(self, level, message):
"""添加日志记录"""
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
self.log_table.insert('', 0, values=(timestamp, level, message))
def system_settings(self):
"""系统设置"""
settings_window = tk.Toplevel(self.root)
settings_window.title("网格交易系统配置")
# 设置窗口大小
window_width = 700
window_height = 600
# 先设置为模态窗口
settings_window.transient(self.root)
# 确保主窗口完全初始化
self.root.update_idletasks()
# 获取主窗口的实际大小(包括边框)
# 使用winfo_rootx/rooty获取窗口在屏幕上的绝对位置
main_x = self.root.winfo_rootx()
main_y = self.root.winfo_rooty()
main_width = self.root.winfo_width()
main_height = self.root.winfo_height()
# 计算设置窗口相对于主窗口的居中位置
x = main_x + (main_width - window_width) // 2
y = main_y + (main_height - window_height) // 2
# 设置窗口大小和位置
settings_window.geometry(f"{window_width}x{window_height}+{x}+{y}")
settings_window.resizable(False, False)
# 设置为模态窗口
settings_window.grab_set()
# 添加底部按钮区域(先创建,确保固定在底部)
button_frame = ttk.Frame(settings_window)
button_frame.pack(side=tk.BOTTOM, fill=tk.X, padx=20, pady=10)
# 创建选项卡(在按钮之后创建,填充剩余空间)
notebook = ttk.Notebook(settings_window)
notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=(10, 0))
# 基础设置
basic_frame = ttk.Frame(notebook)
notebook.add(basic_frame, text="基础设置")
# 高级设置
advanced_frame = ttk.Frame(notebook)
notebook.add(advanced_frame, text="高级设置")
# 读取当前配置
config = configparser.ConfigParser()
config.read('config.ini')
# 创建输入框字典用于保存引用
entries = {}
# 网格价格计算参数
grid_params = {}
# 添加网格价格设置(特殊处理)
grid_price_frame = ttk.LabelFrame(basic_frame, text="网格价格设置", padding=15)
grid_price_frame.pack(fill=tk.X, padx=20, pady=10)
# 基准价格
base_price_row = ttk.Frame(grid_price_frame)
base_price_row.pack(fill=tk.X, pady=5)
ttk.Label(base_price_row, text="基准价格:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
base_price_entry = ttk.Entry(base_price_row, width=15, font=('Arial', 10))
base_price_entry.insert(0, "10.0")
base_price_entry.pack(side=tk.LEFT, padx=5)
ttk.Label(base_price_row, text="", foreground='gray', font=('Arial', 9)).pack(side=tk.LEFT)
grid_params['base_price'] = base_price_entry
# 网格类型
grid_type_row = ttk.Frame(grid_price_frame)
grid_type_row.pack(fill=tk.X, pady=5)
ttk.Label(grid_type_row, text="网格类型:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
grid_type_var = tk.StringVar(value="金额差")
ttk.Radiobutton(grid_type_row, text="百分比", variable=grid_type_var,
value="百分比", command=lambda: on_grid_type_change()).pack(side=tk.LEFT, padx=5)
ttk.Radiobutton(grid_type_row, text="金额差", variable=grid_type_var,
value="金额差", command=lambda: on_grid_type_change()).pack(side=tk.LEFT, padx=5)
grid_params['grid_type'] = grid_type_var
# 网格大小
grid_size_row = ttk.Frame(grid_price_frame)
grid_size_row.pack(fill=tk.X, pady=5)
ttk.Label(grid_size_row, text="网格大小:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
grid_size_entry = ttk.Entry(grid_size_row, width=15, font=('Arial', 10))
grid_size_entry.insert(0, "1.0")
grid_size_entry.pack(side=tk.LEFT, padx=5)
grid_size_unit_label = ttk.Label(grid_size_row, text="", foreground='gray', font=('Arial', 9))
grid_size_unit_label.pack(side=tk.LEFT)
grid_params['grid_size'] = grid_size_entry
grid_params['grid_size_unit_label'] = grid_size_unit_label
# 网格类型改变时更新单位
def on_grid_type_change():
if grid_type_var.get() == "百分比":
grid_size_unit_label.config(text="%")
grid_size_entry.delete(0, tk.END)
grid_size_entry.insert(0, "1.0")
else:
grid_size_unit_label.config(text="")
grid_size_entry.delete(0, tk.END)
grid_size_entry.insert(0, "1.0")
update_preview()
# 上方网格数量
upper_grid_row = ttk.Frame(grid_price_frame)
upper_grid_row.pack(fill=tk.X, pady=5)
ttk.Label(upper_grid_row, text="上方网格数量:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
upper_grid_entry = ttk.Entry(upper_grid_row, width=15, font=('Arial', 10))
upper_grid_entry.insert(0, "1")
upper_grid_entry.pack(side=tk.LEFT, padx=5)
ttk.Label(upper_grid_row, text="", foreground='gray', font=('Arial', 9)).pack(side=tk.LEFT)
grid_params['upper_count'] = upper_grid_entry
# 下方网格数量
lower_grid_row = ttk.Frame(grid_price_frame)
lower_grid_row.pack(fill=tk.X, pady=5)
ttk.Label(lower_grid_row, text="下方网格数量:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
lower_grid_entry = ttk.Entry(lower_grid_row, width=15, font=('Arial', 10))
lower_grid_entry.insert(0, "10")
lower_grid_entry.pack(side=tk.LEFT, padx=5)
ttk.Label(lower_grid_row, text="", foreground='gray', font=('Arial', 9)).pack(side=tk.LEFT)
grid_params['lower_count'] = lower_grid_entry
# 预览按钮和结果显示
preview_row = ttk.Frame(grid_price_frame)
preview_row.pack(fill=tk.X, pady=10)
preview_result = tk.StringVar(value="点击'预览'查看生成的网格价格序列")
def calculate_grid_prices():
"""计算网格价格序列"""
try:
base_price = float(base_price_entry.get())
grid_size = float(grid_size_entry.get())
upper_count = int(upper_grid_entry.get())
lower_count = int(lower_grid_entry.get())
grid_type = grid_type_var.get()
prices = []
# 计算上方网格价格
for i in range(upper_count, 0, -1):
if grid_type == "百分比":
price = base_price * (1 + grid_size / 100 * i)
else: # 金额差
price = base_price + grid_size * i
prices.append(round(price, 3))
# 添加基准价格
prices.append(base_price)
# 计算下方网格价格
for i in range(1, lower_count + 1):
if grid_type == "百分比":
price = base_price * (1 - grid_size / 100 * i)
else: # 金额差
price = base_price - grid_size * i
# 确保价格不为负
if price >= 0:
prices.append(round(price, 3))
else:
break
return prices
except ValueError as e:
return None
def update_preview():
"""自动更新网格价格序列预览"""
prices = calculate_grid_prices()
if prices:
price_str = ",".join([str(p) for p in prices])
preview_result.set(f"网格价格序列: {price_str}")
else:
preview_result.set("参数错误,请检查!")
# 绑定输入变化自动预览
for entry_widget in (base_price_entry, grid_size_entry, upper_grid_entry, lower_grid_entry):
entry_widget.bind("<KeyRelease>", lambda e: update_preview())
entry_widget.bind("<FocusOut>", lambda e: update_preview())
# 初始预览
update_preview()
ttk.Label(preview_row, textvariable=preview_result,
font=('Arial', 10)).pack(side=tk.LEFT, padx=10)
# 添加其他基础配置选项
other_basic_frame = ttk.LabelFrame(basic_frame, text="交易设置", padding=15)
other_basic_frame.pack(fill=tk.X, padx=20, pady=10)
other_basic_settings = [
("网格交易手数", "grid_volume", config.get('config', 'grid_volume'), "每个网格的交易手数"),
("最大启用目标数", "max_enabled_targets", config.get('config', 'max_enabled_targets'), "同时运行的最大标的数量")
]
for i, (label, key, default, tooltip) in enumerate(other_basic_settings):
frame = ttk.Frame(other_basic_frame)
frame.pack(fill=tk.X, pady=5)
label_widget = ttk.Label(frame, text=label + ":", width=15, font=('Arial', 10))
label_widget.pack(side=tk.LEFT)
entry = ttk.Entry(frame, width=15, font=('Arial', 10))
entry.insert(0, default)
entry.pack(side=tk.LEFT, padx=5)
entries[key] = entry
# 添加提示信息
tip_label = ttk.Label(frame, text=tooltip, font=('Arial', 9), foreground='gray')
tip_label.pack(side=tk.LEFT, padx=5)
# 添加高级设置选项
account_frame = ttk.LabelFrame(advanced_frame, text="账号设置", padding=15)
account_frame.pack(fill=tk.X, padx=20, pady=10)
# 交易账号
account_row = ttk.Frame(account_frame)
account_row.pack(fill=tk.X, pady=5)
ttk.Label(account_row, text="交易账号:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
account_entry = ttk.Entry(account_row, width=15, font=('Arial', 10))
account_entry.insert(0, config.get('config', 'account_no'))
account_entry.pack(side=tk.LEFT, padx=5)
entries['account_no'] = account_entry
ttk.Label(account_row, text="QMT交易账号", font=('Arial', 9), foreground='gray').pack(side=tk.LEFT, padx=5)
# QMT路径特殊处理 - 使用文件浏览器
qmt_path_frame = ttk.LabelFrame(advanced_frame, text="软件路径", padding=15)
qmt_path_frame.pack(fill=tk.X, padx=20, pady=10)
qmt_row = ttk.Frame(qmt_path_frame)
qmt_row.pack(fill=tk.X, pady=5)
ttk.Label(qmt_row, text="QMT路径:", width=15, font=('Arial', 10)).pack(side=tk.LEFT)
qmt_entry = ttk.Entry(qmt_row, width=30, font=('Arial', 10))
qmt_entry.insert(0, config.get('config', 'miniQMTPath'))
qmt_entry.pack(side=tk.LEFT, padx=5)
entries['miniQMTPath'] = qmt_entry
def browse_qmt_path():
"""打开文件夹浏览器选择QMT路径"""
initial_dir = qmt_entry.get() if qmt_entry.get() else "/"
folder_path = filedialog.askdirectory(
title="选择miniQMT安装路径",
initialdir=initial_dir
)
if folder_path:
qmt_entry.delete(0, tk.END)
qmt_entry.insert(0, folder_path)
ttk.Button(qmt_row, text="📁 浏览...", command=browse_qmt_path, width=10).pack(side=tk.LEFT, padx=5)
ttk.Label(qmt_row, text="miniQMT软件安装路径", font=('Arial', 9), foreground='gray').pack(side=tk.LEFT, padx=5)
# 定义保存和取消按钮的功能(button_frame已在上方创建)
def save_settings():
"""保存配置"""
try:
# 计算网格价格序列
grid_prices = calculate_grid_prices()
if not grid_prices:
messagebox.showerror("错误", "网格价格参数有误,请检查输入!")
return
grid_price_str = ",".join([str(p) for p in grid_prices])
# 更新配置对象
config.set('config', 'miniQMTPath', entries['miniQMTPath'].get())
config.set('config', 'grid_price', grid_price_str)
config.set('config', 'grid_volume', entries['grid_volume'].get())
config.set('config', 'account_no', entries['account_no'].get())
config.set('config', 'max_enabled_targets', entries['max_enabled_targets'].get())
# 写入配置文件
with open('config.ini', 'w') as configfile:
config.write(configfile)
# 重新加载配置到内存中
sfgrid_constants.initConfig()
messagebox.showinfo("成功", f"配置已保存!\n网格价格序列: {grid_price_str}\n部分配置可能需要重启程序后生效。")
self.add_log("INFO", f"系统配置已更新 - 网格数量: {len(grid_prices)}")
settings_window.destroy()
except Exception as e:
messagebox.showerror("错误", f"保存配置失败:{str(e)}")
self.add_log("ERROR", f"保存配置失败: {str(e)}")
def cancel_settings():
"""取消设置"""
settings_window.destroy()
# 在button_frame中添加按钮
ttk.Button(button_frame, text="💾 保存配置", command=save_settings, width=15).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="❌ 取消", command=cancel_settings, width=15).pack(side=tk.LEFT, padx=5)
def run(self):
"""运行程序"""
self.root.mainloop()
# 使用示例
if __name__ == "__main__":
print("交易标的监控系统启动...")
print("功能说明:")
print(" - 左侧表格显示所有交易标的详细信息")
print(" - 右侧表格显示操作日志")
print(" - 底部五个功能按钮提供操作")
# 创建并运行界面
app = TradeTargetUI()
app.run()