Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e3202968d | |||
| 2d8a0c3bca | |||
| ef4c1cca32 | |||
| 1816d585bf | |||
| db910e03d6 | |||
| 6b3b1a1f76 | |||
| 60af627806 | |||
| 4787011177 | |||
| 81da3fe013 | |||
| c12e394b8e | |||
| 3ee47e95cf | |||
| e5b1d80139 | |||
| 5c40d5d64f | |||
| 1618cad5a0 | |||
| 64bdddde79 | |||
| 4c4c8730f2 | |||
| 0262bfc71b | |||
| d5fef7c0c1 | |||
| fcadcb86d2 | |||
| 59a88c4365 | |||
| c69b5bc1ae | |||
| f626545897 | |||
| f499d9a413 | |||
| 6cae413956 | |||
| 6810b55cbb | |||
| cd67c9ad60 | |||
| a18c7be7eb | |||
| f5d37eaa7e | |||
| 550126d060 | |||
| c03a4adb53 | |||
| 81d0131a7b | |||
| 1193dc2f69 | |||
| 8ab5d83b1a | |||
| 91d1cac384 | |||
| 2ca0ab65f0 | |||
| 5e64e93172 | |||
| 0d54f8b05a | |||
| ba9cd9a700 | |||
| 0dbd8e8dde | |||
| 54fd7c9545 | |||
| 662a1ea7c1 | |||
| c42648d1b4 | |||
| 7cfb433aaf | |||
| 1ec27bb52f | |||
| 7f0a73381d | |||
| c2f34d09e9 | |||
| 20a7453e8b | |||
| 3a137b6aee | |||
| 7733d6df32 | |||
| b7f9d29c54 | |||
| d988f5eb48 | |||
| 1ee8f0426e | |||
| 88bd0b17c9 | |||
| df0e9ecb22 | |||
| c77ff1c0ae |
@@ -1 +1,8 @@
|
||||
__pycache__/
|
||||
dist/
|
||||
example.db
|
||||
xtquant/
|
||||
starter.dist/starter.dll
|
||||
build/
|
||||
.vscode/
|
||||
example.db.bak
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
# SFGrid 网格交易策略流程图
|
||||
|
||||
## 1. 总览:策略生命周期
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["SFGridStrategy.__init__()"] --> B["订阅事件总线<br/>onOrderCreateAsync / onOrderTrade / onOrderError"]
|
||||
B --> C["获取涨跌停价<br/>todayUpStopPrice / todayDownStopPrice"]
|
||||
C --> D["loadExistOrders()<br/>从券商侧恢复未成交订单到 orderGrid"]
|
||||
D --> E["enabledTrading(enabled)"]
|
||||
E --> F{"enabled ?"}
|
||||
F -->|True| G["启用交易流程 → 见 §3"]
|
||||
F -->|False| H["停用交易流程 → 见 §3"]
|
||||
G --> I["saveProxy() 持久化"]
|
||||
H --> I
|
||||
I --> J["构造完成,进入事件循环<br/>等待 QMT 回调 / UI 操作"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 核心:refreshGridOrder() 网格下单
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START["refreshGridOrder()"] --> CHECK1{"qmtv.isMarketActive<br/>AND<br/>tradeTarget.enabled ?"}
|
||||
CHECK1 -->|No| SKIP["跳过不下单"]
|
||||
CHECK1 -->|Yes| QUERY["查询未成交订单<br/>queryPendingOrder()"]
|
||||
|
||||
QUERY --> STATUS{"tradeTarget.status ?"}
|
||||
|
||||
STATUS -->|"= 0 未建仓"| CHECK_INIT{"已存在建仓单?<br/>remark = 'INIT,1,{code}'"}
|
||||
CHECK_INIT -->|"No 没有"| PLACE_INIT["下建仓单 (STOCK_BUY)<br/>价格 = getPriceGrid()[0]<br/>remark = 'INIT,1,{code}'"]
|
||||
CHECK_INIT -->|"Yes 已有"| DONE_INIT["建仓单已在途,跳过"]
|
||||
|
||||
STATUS -->|"= 1 已建仓"| GET_IDX["currentIdx = grid_index"]
|
||||
GET_IDX --> SELL_CHECK{"currentIdx > 0 ?<br/>(grid_index 不是最低点)"}
|
||||
SELL_CHECK -->|"Yes 可挂卖单"| SELL_EXIST{"已存在同 remark 卖单?<br/>remark='SELL,{idx-1},{code}'"}
|
||||
SELL_EXIST -->|"No 没有"| SELL_PLACE["下卖出单 (STOCK_SELL)<br/>价格 = grid[sellIdx]<br/>sellIdx = currentIdx - 1"]
|
||||
SELL_EXIST -->|"Yes 已有"| SELL_SKIP["跳过,避免重复"]
|
||||
SELL_CHECK -->|"No 价格已最低"| SELL_SKIP2["无卖出空间"]
|
||||
|
||||
SELL_PLACE --> BUY_CHECK
|
||||
SELL_SKIP --> BUY_CHECK
|
||||
SELL_SKIP2 --> BUY_CHECK
|
||||
|
||||
BUY_CHECK{"currentIdx < len(grid)-1 ?<br/>(grid_index 不是最高点)"}
|
||||
BUY_CHECK -->|"Yes 可挂买单"| BUY_EXIST{"已存在同价同类型买单?<br/>order_type=BUY AND price=buyPrice"}
|
||||
BUY_EXIST -->|"No 没有"| BUY_PLACE["下买入单 (STOCK_BUY)<br/>价格 = grid[buyIdx]<br/>buyIdx = currentIdx + 1"]
|
||||
BUY_EXIST -->|"Yes 已有"| BUY_SKIP["跳过,避免重复"]
|
||||
BUY_CHECK -->|"No 价格已最高"| BUY_SKIP2["无买入空间"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. 交易启停:enabledTrading()
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START["enabledTrading(enabled)"] --> SET["self.tradeTarget.enabled = enabled"]
|
||||
SET --> BRANCH{"enabled ?"}
|
||||
|
||||
BRANCH -->|"True 启用"| STATUS{"tradeTarget.status ?"}
|
||||
STATUS -->|"= 0 未建仓"| INIT_IDX{"grid_index == 0 ?"}
|
||||
INIT_IDX -->|"Yes"| SET1["grid_index = 1<br/>(默认建仓位置)"]
|
||||
INIT_IDX -->|"No"| KEEP["保留现有 grid_index"]
|
||||
SET1 --> REFRESH1["refreshGridOrder()"]
|
||||
KEEP --> REFRESH1
|
||||
|
||||
STATUS -->|"= 1 已建仓"| CALC["计算最小需求仓位<br/>min = grid_volume × grid_index"]
|
||||
CALC --> CHECK{"current_position >= min ?"}
|
||||
CHECK -->|"Yes 充足"| REFRESH2["refreshGridOrder()"]
|
||||
CHECK -->|"No 不足"| DENY["拒绝启用<br/>enabled = False<br/>(风控保护)"]
|
||||
|
||||
BRANCH -->|"False 停用"| CANCEL["取消所有未成交订单<br/>cancel_order_stock_async()"]
|
||||
CANCEL --> LOG["记录取消数量"]
|
||||
|
||||
REFRESH1 --> SAVE["saveProxy() 持久化"]
|
||||
REFRESH2 --> SAVE
|
||||
DENY --> SAVE
|
||||
LOG --> SAVE
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 事件回调链
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph QMT["QMT / xtquant 层"]
|
||||
OA["orderAsync()<br/>返回 seq"]
|
||||
PUSH_ERR["C扩展推送<br/>XtOrderError"]
|
||||
PUSH_RESP["C扩展推送<br/>XtOrderResponse"]
|
||||
PUSH_TRADE["C扩展推送<br/>XtTrade"]
|
||||
end
|
||||
|
||||
subgraph BUS["事件总线 event_bus"]
|
||||
EVT_ERR["MarketOrderError"]
|
||||
EVT_RESP["MarketOrderCreated"]
|
||||
EVT_TRADE["MarketOrderTraded"]
|
||||
end
|
||||
|
||||
subgraph STG["SFGridStrategy 回调"]
|
||||
OE["onOrderError()"]
|
||||
OC["onOrderCreateAsync()"]
|
||||
OT["onOrderTrade()"]
|
||||
end
|
||||
|
||||
OA --> PUSH_RESP
|
||||
OA --> PUSH_ERR
|
||||
PUSH_ERR --> EVT_ERR --> OE
|
||||
PUSH_RESP --> EVT_RESP --> OC
|
||||
PUSH_TRADE --> EVT_TRADE --> OT
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. onOrderError() 委托失败处理
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START["onOrderError(order_error)"] --> CHK1{"order_remark 非空 ?"}
|
||||
CHK1 -->|"No 空"| EXIT1["无法解析,忽略"]
|
||||
CHK1 -->|"Yes"| PARSE["解析 remark<br/>'{type},{gridIdx},{stockCode}'"]
|
||||
PARSE --> CHK2{"len(parts) >= 3 ?"}
|
||||
CHK2 -->|"No"| EXIT1
|
||||
CHK2 -->|"Yes"| CHK3{"strategy_name == 'SFGRID'<br/>AND<br/>stockCode 匹配本标的 ?"}
|
||||
CHK3 -->|"No 不匹配"| EXIT1
|
||||
CHK3 -->|"Yes"| LOCK["获取 dataUpdateLock"]
|
||||
LOCK --> DEL{"gridIdx in orderGrid ?"}
|
||||
DEL -->|"Yes"| REMOVE["del orderGrid[gridIdx]<br/>清理孤立条目"]
|
||||
DEL -->|"No"| LOG_ERR["记录错误日志<br/>error_id / error_msg"]
|
||||
REMOVE --> LOG_ERR
|
||||
LOG_ERR --> UNLOCK["释放 dataUpdateLock"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. onOrderCreateAsync() 订单确认
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START["onOrderCreateAsync(response)"] --> PARSE["解析 remark<br/>'{type},{gridIdx},{stockCode}'"]
|
||||
PARSE --> FILTER{"strategy_name == 'SFGRID'<br/>AND len(parts) >= 3<br/>AND stockCode 匹配 ?"}
|
||||
FILTER -->|"No"| EXIT["忽略"]
|
||||
FILTER -->|"Yes"| LOCK["获取 dataUpdateLock"]
|
||||
LOCK --> UPDATE["orderGrid[gridIdx] = response.order_id<br/>seq → order_id 替换"]
|
||||
UPDATE --> UNLOCK["释放 dataUpdateLock"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. onOrderTrade() 成交处理
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
START["onOrderTrade(trade)"] --> PARSE["解析 remark<br/>'{type},{gridIdx},{stockCode}'"]
|
||||
PARSE --> FILTER{"strategy_name == 'SFGRID'<br/>AND len(parts) >= 3<br/>AND stockCode 匹配 ?"}
|
||||
FILTER -->|"No"| EXIT["忽略"]
|
||||
FILTER -->|"Yes"| LOCK["获取 dataUpdateLock"]
|
||||
|
||||
LOCK --> TYPE{"orderType ?"}
|
||||
|
||||
TYPE -->|"INIT 建仓单"| INIT["status = 1<br/>init_price = traded_price<br/>grid_index = 1"]
|
||||
TYPE -->|"BUY / SELL 网格单"| CMP{"gridIdx vs grid_index ?"}
|
||||
|
||||
CMP -->|"gridIdx > grid_index<br/>(买入成交)"| DOWN["grid_index += 1<br/>下移一格"]
|
||||
CMP -->|"gridIdx < grid_index<br/>(卖出成交)"| UP["grid_index -= 1<br/>上移一格<br/>match_count += 1<br/>total_profit += grid_size × volume"]
|
||||
CMP -->|"gridIdx == grid_index<br/>(异常)"| SAME["日志: 理论上不应该输出"]
|
||||
|
||||
INIT --> POST
|
||||
DOWN --> POST
|
||||
UP --> POST
|
||||
SAME --> POST
|
||||
|
||||
POST["成交后处理"] --> SAVE["saveProxy() 持久化状态"]
|
||||
SAVE --> DEL["del orderGrid[gridIdx]<br/>移除已成交订单"]
|
||||
DEL --> REPORT["打印成交报告<br/>成交价/量/手续费"]
|
||||
REPORT --> REFRESH["refreshGridOrder()<br/>在新位置挂新的网格单"]
|
||||
REFRESH --> UNLOCK["释放 dataUpdateLock"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. 网格交易完整状态机
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> 未建仓: 创建 SFGridStrategy
|
||||
未建仓 --> 建仓中: enabledTrading(True)<br/>下建仓单 INIT
|
||||
|
||||
建仓中 --> 已建仓: onOrderTrade(INIT)<br/>建仓单成交
|
||||
建仓中 --> 建仓失败: onOrderError(INIT)<br/>委托被拒
|
||||
|
||||
建仓失败 --> 建仓中: refreshGridOrder()<br/>重新下建仓单
|
||||
|
||||
已建仓 --> 网格运行: refreshGridOrder()<br/>上下各挂一单
|
||||
|
||||
网格运行 --> 网格运行: onOrderTrade(SELL)<br/>卖出成交 → 上移<br/>重新挂单
|
||||
网格运行 --> 网格运行: onOrderTrade(BUY)<br/>买入成交 → 下移<br/>重新挂单
|
||||
网格运行 --> 单边挂单: onOrderError<br/>某方向委托失败
|
||||
|
||||
单边挂单 --> 网格运行: refreshGridOrder()<br/>重新补挂失败方向的单
|
||||
|
||||
已建仓 --> 已停用: enabledTrading(False)<br/>取消所有挂单
|
||||
网格运行 --> 已停用: enabledTrading(False)
|
||||
单边挂单 --> 已停用: enabledTrading(False)
|
||||
|
||||
已停用 --> 已建仓: enabledTrading(True)<br/>仓位检查通过
|
||||
已停用 --> 已停用: enabledTrading(True)<br/>仓位不足,回退
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. 网格价格示意
|
||||
|
||||
```
|
||||
价格
|
||||
↑
|
||||
│ grid[5] = 12.00 ← 最贵(顶部)
|
||||
│ grid[4] = 11.50
|
||||
│ grid[3] = 11.00 ← 当前位置 grid_index=3
|
||||
│ grid[2] = 10.50 上方挂卖单 @10.50 (sellIdx=2, grid_index-1)
|
||||
│ grid[1] = 10.00 下方挂买单 @10.00 (buyIdx=1, 已成交位置)
|
||||
│ grid[0] = 9.50 ← 最便宜(底部/建仓价)
|
||||
│
|
||||
└──────────────────────→
|
||||
|
||||
grid_index=3 时:
|
||||
卖单挂在 grid[2] @10.50 → 价格跌到 10.50 卖出(上移一格,赚差价)
|
||||
买单挂在 grid[4] @11.50 → 价格涨到 11.50 买入(下移一格,补仓)
|
||||
|
||||
grid_size = grid[i] - grid[i-1] = 0.50(每格利润空间)
|
||||
```
|
||||
@@ -1,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
|
||||
|
||||
@@ -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'
|
||||
@@ -0,0 +1,5 @@
|
||||
import xtquant.xtconstant as xtconstant
|
||||
|
||||
OrderTypeBuy = f'{xtconstant.STOCK_BUY}' # 买
|
||||
OrderTypeSell = f'{xtconstant.STOCK_SELL}' # 卖
|
||||
OrderTypeNone = "None"
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -0,0 +1,4 @@
|
||||
import xtquant.xtconstant as xtconstant
|
||||
|
||||
HeatTypeUpStop = "UpStop" # 涨停
|
||||
HeatTypeDragonTiger = "DragonTiger" # 龙虎榜
|
||||
@@ -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])
|
||||
@@ -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 # 写文件失败不阻塞主流程
|
||||
@@ -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
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -0,0 +1,3 @@
|
||||
# 删除交易标的事件
|
||||
EventTradeTargetUpdate = "trade_target_update"
|
||||
EventTradeTargetDeleted = "trade_target_deleted"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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')
|
||||
@@ -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}]'
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
def is_trading_time():
|
||||
"""
|
||||
判断当前时间是否在周一至周五的9:30~11:30或13:00~15:00时间段内
|
||||
@@ -40,30 +33,3 @@ def is_trading_time():
|
||||
|
||||
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
Binary file not shown.
@@ -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
@@ -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
|
||||
@@ -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
@@ -1,82 +1,23 @@
|
||||
# coding:utf-8
|
||||
from mimetypes import init
|
||||
"""
|
||||
启动入口 — 自动探测 QMT 环境。
|
||||
默认使用 Tkinter UI,使用 --flet 参数切换到 Flet (Flutter) UI。
|
||||
"""
|
||||
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__':
|
||||
|
||||
strategy_db.db.connect()
|
||||
strategy_db.db.create_tables([strategy_db.TradeTarget])
|
||||
print('- [成功]数据库模块初始化')
|
||||
|
||||
targets = strategy_db.TradeTarget.select()
|
||||
|
||||
|
||||
app = ui.TradeTargetUI(trade_targets=targets)
|
||||
app.run()
|
||||
|
||||
# sdConstants.initConfig()
|
||||
# print(f'{sdConstants.account_no} : {sdConstants.miniQMTPath}')
|
||||
# ctrl: SFGridController = SFGridController(sdConstants.account_no, sdConstants.miniQMTPath)
|
||||
# if ctrl.inited:
|
||||
# interact()
|
||||
# else:
|
||||
# print("控制器初始化失败")
|
||||
if '--flet2' in sys.argv:
|
||||
from core.ui.flet.app_v2 import run
|
||||
run()
|
||||
elif '--flet' in sys.argv:
|
||||
from core.ui.flet.app import run
|
||||
run()
|
||||
else:
|
||||
from tkinter import messagebox
|
||||
from core.ui.tkinter.splash import SplashWindow
|
||||
try:
|
||||
window = SplashWindow().run()
|
||||
if window:
|
||||
window.run()
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"系统初始化失败: {str(e)}")
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user