update
This commit is contained in:
@@ -7,6 +7,7 @@ miniQMTPath = r'D:\\Programs\\DTQMT\\userdata_mini' # miniQMT软件的安装路
|
|||||||
account_no:str = '99082560'
|
account_no:str = '99082560'
|
||||||
console_log = True
|
console_log = True
|
||||||
log_level = "INFO"
|
log_level = "INFO"
|
||||||
|
use_simulated_qmt: bool = False
|
||||||
|
|
||||||
def get_config_path() -> Path:
|
def get_config_path() -> Path:
|
||||||
"""获取配置文件的正确路径(兼容开发环境和打包后的可执行文件)"""
|
"""获取配置文件的正确路径(兼容开发环境和打包后的可执行文件)"""
|
||||||
@@ -21,12 +22,14 @@ def get_config_path() -> Path:
|
|||||||
|
|
||||||
return base_path / 'config.ini'
|
return base_path / 'config.ini'
|
||||||
|
|
||||||
def save_config(miniQmtPath:str, account_no:str):
|
def save_config(miniQmtPath:str, account_no:str, use_simulated_qmt: bool = False):
|
||||||
"""创建默认配置文件"""
|
"""创建默认配置文件"""
|
||||||
config = configparser.ConfigParser()
|
config = configparser.ConfigParser()
|
||||||
config['config'] = {
|
config['config'] = {
|
||||||
'miniQMTPath': miniQmtPath,
|
'miniQMTPath': miniQmtPath,
|
||||||
'account_no': account_no
|
'account_no': account_no,
|
||||||
|
'use_simulated_qmt': str(use_simulated_qmt),
|
||||||
|
'log_level': 'INFO'
|
||||||
}
|
}
|
||||||
config_path = get_config_path()
|
config_path = get_config_path()
|
||||||
with open(config_path, 'w') as configfile:
|
with open(config_path, 'w') as configfile:
|
||||||
@@ -39,7 +42,7 @@ def exist_config() -> bool:
|
|||||||
return config_path.exists()
|
return config_path.exists()
|
||||||
|
|
||||||
def initConfig() -> bool:
|
def initConfig() -> bool:
|
||||||
global miniQMTPath, account_no, log_level
|
global miniQMTPath, account_no, log_level, use_simulated_qmt
|
||||||
|
|
||||||
# 获取配置文件路径
|
# 获取配置文件路径
|
||||||
config_path = get_config_path()
|
config_path = get_config_path()
|
||||||
@@ -49,6 +52,10 @@ def initConfig() -> bool:
|
|||||||
miniQMTPath = config.get('config','miniQMTPath')
|
miniQMTPath = config.get('config','miniQMTPath')
|
||||||
account_no = config.get('config','account_no')
|
account_no = config.get('config','account_no')
|
||||||
log_level = config.get('config','log_level')
|
log_level = config.get('config','log_level')
|
||||||
|
try:
|
||||||
|
use_simulated_qmt = config.get('config','use_simulated_qmt').lower() in ('true', '1', 'yes')
|
||||||
|
except (configparser.NoOptionError, configparser.NoSectionError):
|
||||||
|
use_simulated_qmt = False
|
||||||
|
|
||||||
# 判断miniQMTPath是否为空,并且目录是否存在
|
# 判断miniQMTPath是否为空,并且目录是否存在
|
||||||
if not miniQMTPath or not Path(miniQMTPath).exists():
|
if not miniQMTPath or not Path(miniQMTPath).exists():
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ EventMarketActiveSwitch = "market_active_switch" # 市场数据状态变更
|
|||||||
MarketDataUpdate = "market_data_update" # 市价更新
|
MarketDataUpdate = "market_data_update" # 市价更新
|
||||||
MarketOrderCreated = "market_order_created" # 市价单创建
|
MarketOrderCreated = "market_order_created" # 市价单创建
|
||||||
MarketOrderTraded = "market_order_traded" # 市价单成交
|
MarketOrderTraded = "market_order_traded" # 市价单成交
|
||||||
|
MarketOrderError = "market_order_error" # 市价单委托失败
|
||||||
# Pring Log
|
# Pring Log
|
||||||
EventPrintLog = "print_log" # 打印日志
|
EventPrintLog = "print_log" # 打印日志
|
||||||
|
|
||||||
|
|||||||
+9
-2
@@ -1,11 +1,17 @@
|
|||||||
"""
|
"""
|
||||||
QMT 模块统一入口
|
QMT 模块统一入口
|
||||||
根据环境自动选择真实 QMT 或模拟器
|
根据配置或环境自动选择真实 QMT 或模拟器
|
||||||
"""
|
"""
|
||||||
import sys
|
import sys
|
||||||
|
import config as _config
|
||||||
|
|
||||||
|
|
||||||
def _get_qmt():
|
def _get_qmt():
|
||||||
"""获取 QMT 模块"""
|
"""获取 QMT 模块(配置优先于平台检测)"""
|
||||||
|
if _config.use_simulated_qmt:
|
||||||
|
from core.qmt_dummy import qmtv
|
||||||
|
return qmtv
|
||||||
|
|
||||||
if sys.platform == 'win32':
|
if sys.platform == 'win32':
|
||||||
try:
|
try:
|
||||||
from core.qmt_real import qmtv as real_qmtv
|
from core.qmt_real import qmtv as real_qmtv
|
||||||
@@ -17,5 +23,6 @@ def _get_qmt():
|
|||||||
from core.qmt_dummy import qmtv
|
from core.qmt_dummy import qmtv
|
||||||
return qmtv
|
return qmtv
|
||||||
|
|
||||||
|
|
||||||
# 导出单例
|
# 导出单例
|
||||||
qmtv = _get_qmt()
|
qmtv = _get_qmt()
|
||||||
+2
-1
@@ -280,7 +280,8 @@ class DummyQmtV:
|
|||||||
eBus.event_bus.publish(eBus.MarketOrderCreated, response)
|
eBus.event_bus.publish(eBus.MarketOrderCreated, response)
|
||||||
|
|
||||||
def on_order_error(self, order_error):
|
def on_order_error(self, order_error):
|
||||||
print(f"\n模拟委托报错回调 {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):
|
def on_account_status(self, status):
|
||||||
print(datetime.datetime.now(), status)
|
print(datetime.datetime.now(), status)
|
||||||
|
|||||||
+8
-2
@@ -163,9 +163,10 @@ class RealQmtV:
|
|||||||
return -1
|
return -1
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
full_code = self._to_full_code(stock_code)
|
||||||
seq = self.xt_trader.order_stock_async(
|
seq = self.xt_trader.order_stock_async(
|
||||||
account=self.account,
|
account=self.account,
|
||||||
stock_code=stock_code,
|
stock_code=full_code,
|
||||||
order_volume=orderVolume,
|
order_volume=orderVolume,
|
||||||
order_type=orderType,
|
order_type=orderType,
|
||||||
price=orderPrice,
|
price=orderPrice,
|
||||||
@@ -306,6 +307,10 @@ class RealQmtV:
|
|||||||
seq = xtdata.subscribe_whole_quote(['SH', 'SZ'], self._on_market_data)
|
seq = xtdata.subscribe_whole_quote(['SH', 'SZ'], self._on_market_data)
|
||||||
PrintLog(LogLevel.INFO, f'- [市场数据订阅成功-真实] seq={seq}')
|
PrintLog(LogLevel.INFO, f'- [市场数据订阅成功-真实] seq={seq}')
|
||||||
|
|
||||||
|
# 订阅成功即标记市场活跃,避免策略初始化时因等待首条数据被误判为休市
|
||||||
|
self.isMarketActive = True
|
||||||
|
eBus.event_bus.publish(eBus.EventMarketActiveSwitch, True)
|
||||||
|
|
||||||
# 启动行情活跃监控线程
|
# 启动行情活跃监控线程
|
||||||
self._market_data_thread = threading.Thread(
|
self._market_data_thread = threading.Thread(
|
||||||
target=self._market_data_watchdog, daemon=True
|
target=self._market_data_watchdog, daemon=True
|
||||||
@@ -359,7 +364,8 @@ class RealQmtV:
|
|||||||
eBus.event_bus.publish(eBus.MarketOrderCreated, response)
|
eBus.event_bus.publish(eBus.MarketOrderCreated, response)
|
||||||
|
|
||||||
def on_order_error(self, order_error):
|
def on_order_error(self, order_error):
|
||||||
print(f"\n真实委托报错回调 {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):
|
def on_account_status(self, status):
|
||||||
print(datetime.datetime.now(), status)
|
print(datetime.datetime.now(), status)
|
||||||
|
|||||||
+450
-103
@@ -1,3 +1,24 @@
|
|||||||
|
"""
|
||||||
|
网格交易策略控制器
|
||||||
|
|
||||||
|
核心逻辑:在预设的价格网格上低买高卖,每个网格节点同时挂一对买卖单,
|
||||||
|
成交后自动切换到相邻网格并刷新订单。
|
||||||
|
|
||||||
|
网格结构示意(以 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.logger import LogLevel, PrintLog
|
||||||
from core.qmt import qmtv
|
from core.qmt import qmtv
|
||||||
from core.sfgrid import bus_events
|
from core.sfgrid import bus_events
|
||||||
@@ -7,113 +28,234 @@ from core.eventbus import event_bus
|
|||||||
from core.constants import OrderTypeBuy, OrderTypeSell, OrderTypeInit
|
from core.constants import OrderTypeBuy, OrderTypeSell, OrderTypeInit
|
||||||
|
|
||||||
from xtquant import xtconstant
|
from xtquant import xtconstant
|
||||||
from xtquant.xttype import XtOrderResponse, XtTrade
|
from xtquant.xttype import XtOrderError, XtOrderResponse, XtTrade
|
||||||
import threading
|
import threading
|
||||||
import core.eventbus as eBus
|
import core.eventbus as eBus
|
||||||
|
|
||||||
|
|
||||||
class SFGridStrategy:
|
class SFGridStrategy:
|
||||||
|
"""
|
||||||
|
单标的网格交易策略控制器
|
||||||
|
|
||||||
|
每个 SFGridTradeTarget 数据库记录对应一个 SFGridStrategy 实例。
|
||||||
|
负责:建仓 → 挂网格单 → 监听成交/错误事件 → 调整网格 → 刷新订单。
|
||||||
|
|
||||||
|
订单 remark 格式: "{订单类型},{网格索引},{股票代码}"
|
||||||
|
例: "BUY,3,000001" 表示在网格索引 3 处挂买入单,标的 000001
|
||||||
|
例: "INIT,1,000001" 表示建仓单,建仓在网格索引 1
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, tradeTarget: model.SFGridTradeTarget):
|
def __init__(self, tradeTarget: model.SFGridTradeTarget):
|
||||||
|
"""
|
||||||
|
初始化网格策略控制器
|
||||||
|
|
||||||
|
参数:
|
||||||
|
tradeTarget: 数据库中的交易标记录,包含网格参数、当前状态等
|
||||||
|
"""
|
||||||
self.tradeTarget: model.SFGridTradeTarget = tradeTarget
|
self.tradeTarget: model.SFGridTradeTarget = tradeTarget
|
||||||
|
|
||||||
|
# 订阅事件总线:监听订单创建、成交、失败三种事件
|
||||||
event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync)
|
event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync)
|
||||||
event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade)
|
event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade)
|
||||||
|
event_bus.subscribe(eBus.MarketOrderError, self.onOrderError)
|
||||||
|
|
||||||
|
# 获取当日涨跌停价格(用于价格边界校验)
|
||||||
self.todayUpStopPrice = qmtv.dailyUpStop(tradeTarget.stock_code) # type: ignore
|
self.todayUpStopPrice = qmtv.dailyUpStop(tradeTarget.stock_code) # type: ignore
|
||||||
self.todayDownStopPrice = qmtv.dailyDownStop(tradeTarget.stock_code) # type: ignore
|
self.todayDownStopPrice = qmtv.dailyDownStop(tradeTarget.stock_code) # type: ignore
|
||||||
PrintLog(LogLevel.INFO, f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造开始: grid_index={tradeTarget.grid_index}, status={tradeTarget.status}, enabled={tradeTarget.enabled}')
|
|
||||||
self.orderGrid = {} # grid index, order_seq | order_id
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造开始: '
|
||||||
|
f'grid_index={tradeTarget.grid_index}, status={tradeTarget.status}, '
|
||||||
|
f'enabled={tradeTarget.enabled}')
|
||||||
|
|
||||||
|
# orderGrid: 网格索引 → 订单编号(seq 或 order_id)的映射
|
||||||
|
# seq 是 xtquant 返回的下单序号(下单瞬间),order_id 是交易所返回的正式订单号(异步回调后更新)
|
||||||
|
self.orderGrid = {} # {grid_index: order_seq | order_id}
|
||||||
|
|
||||||
|
# 加载券商侧已存在的未成交订单,恢复到 orderGrid 中
|
||||||
self.loadExistOrders()
|
self.loadExistOrders()
|
||||||
self.enabledTrading(tradeTarget.enabled) # type: ignore
|
|
||||||
|
# 数据更新锁:保护 orderGrid 和 tradeTarget 的并发访问
|
||||||
|
# QMT 回调在独立线程中触发,必须在可能触发回调的操作之前创建
|
||||||
self.dataUpdateLock = threading.Lock()
|
self.dataUpdateLock = threading.Lock()
|
||||||
PrintLog(LogLevel.INFO, f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造结束: grid_index={self.tradeTarget.grid_index}')
|
|
||||||
|
# 根据数据库中的 enabled 字段决定是否启动交易
|
||||||
|
self.enabledTrading(tradeTarget.enabled) # type: ignore
|
||||||
|
|
||||||
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- [DEBUG] 标的{tradeTarget.targetName()} 构造结束: '
|
||||||
|
f'grid_index={self.tradeTarget.grid_index}')
|
||||||
|
|
||||||
|
# ── 订单加载 ──────────────────────────────────────────────
|
||||||
|
|
||||||
def loadExistOrders(self):
|
def loadExistOrders(self):
|
||||||
|
"""
|
||||||
|
从券商侧加载该策略的未成交订单,恢复到 orderGrid
|
||||||
|
|
||||||
|
用于程序重启后恢复状态:数据库中可能没有记录所有挂单,
|
||||||
|
通过 queryPendingOrder 从 QMT 获取实际存在的订单。
|
||||||
|
"""
|
||||||
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
||||||
for order in orders:
|
for order in orders:
|
||||||
|
# 只处理本策略的订单(通过 strategy_name 过滤)
|
||||||
if order.strategy_name != self.getName():
|
if order.strategy_name != self.getName():
|
||||||
continue
|
continue
|
||||||
gridIdx = int(order.order_remark.split(',')[1])
|
parsed = self._parse_remark(order.order_remark)
|
||||||
|
if parsed is None:
|
||||||
|
continue
|
||||||
|
_, gridIdx, _ = parsed
|
||||||
self.orderGrid[gridIdx] = order.order_id
|
self.orderGrid[gridIdx] = order.order_id
|
||||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 初始化: 加载现有订单, grid-{gridIdx} order_id:{self.orderGrid[gridIdx]}')
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- 标的[{self.tradeTarget.targetName()}] 初始化: '
|
||||||
|
f'加载现有订单, grid-{gridIdx} order_id:{self.orderGrid[gridIdx]}')
|
||||||
|
|
||||||
def printPendingOrder(self):
|
def printPendingOrder(self):
|
||||||
|
"""调试用:打印当前所有挂单"""
|
||||||
for idx, order_id in self.orderGrid.items():
|
for idx, order_id in self.orderGrid.items():
|
||||||
PrintLog(LogLevel.DEBUG, f" {idx} : {order_id}")
|
PrintLog(LogLevel.DEBUG, f" {idx} : {order_id}")
|
||||||
|
|
||||||
|
# ── 市场状态切换 ──────────────────────────────────────────
|
||||||
|
|
||||||
def onMarketActiveSwitch(self, isActive: bool):
|
def onMarketActiveSwitch(self, isActive: bool):
|
||||||
|
"""
|
||||||
|
市场数据状态切换回调(由 UI 层调用)
|
||||||
|
|
||||||
|
当市场数据从不可用变为可用时,如果策略已启用则刷新网格订单。
|
||||||
|
"""
|
||||||
if isActive and self.tradeTarget.enabled:
|
if isActive and self.tradeTarget.enabled:
|
||||||
self.refreshGridOrder()
|
self.refreshGridOrder()
|
||||||
|
|
||||||
def refreshGridOrder(self): # 下网格单
|
# ── 核心:网格下单逻辑 ────────────────────────────────────
|
||||||
|
|
||||||
|
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:
|
if not qmtv.isMarketActive or not self.tradeTarget.enabled:
|
||||||
PrintLog(LogLevel.INFO, f'|- 市场 {qmtv.isMarketActive}, 策略 {self.getName()} {self.tradeTarget.enabled}, 不下单')
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- 市场 {qmtv.isMarketActive}, 策略 {self.getName()} '
|
||||||
|
f'{self.tradeTarget.enabled}, 不下单')
|
||||||
return
|
return
|
||||||
|
|
||||||
currentIdx:int = 0
|
# 获取当前该标的所有未成交订单
|
||||||
|
|
||||||
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
||||||
|
|
||||||
if self.tradeTarget.status == 0 and len([order for order in orders if order.order_remark == f'{OrderTypeInit},1,{self.tradeTarget.stock_code}']) == 0: # status == 0 表示已配置好交易参数,且不存在执行中的建仓单
|
# ── 分支1: status=0 未建仓 → 下建仓单 ──
|
||||||
|
# 条件: 标的尚未建仓 且 不存在正在执行中的建仓单(防止重复建仓)
|
||||||
|
init_remark = self._make_remark(OrderTypeInit, 1)
|
||||||
|
if self.tradeTarget.status == 0 and not any(
|
||||||
|
o.order_remark == init_remark for o in orders
|
||||||
|
):
|
||||||
|
# 建仓价取价格网格中最高价(grid_index=0 即列表第一个元素)
|
||||||
price = self.tradeTarget.getPriceGrid()[0]
|
price = self.tradeTarget.getPriceGrid()[0]
|
||||||
remark = f'{OrderTypeInit},1,{self.tradeTarget.stock_code}'
|
|
||||||
tmpOrderSeq = qmtv.orderAsync(
|
tmpOrderSeq = qmtv.orderAsync(
|
||||||
str(self.tradeTarget.stock_code),
|
str(self.tradeTarget.stock_code),
|
||||||
self.tradeTarget.grid_volume,
|
self.tradeTarget.grid_volume,
|
||||||
xtconstant.STOCK_BUY,
|
xtconstant.STOCK_BUY, # 建仓 = 买入
|
||||||
price,
|
price,
|
||||||
xtconstant.FIX_PRICE,
|
xtconstant.FIX_PRICE, # 限价单
|
||||||
remark, # remark # type: ignore
|
init_remark,
|
||||||
self.getName(), # strategy_name
|
self.getName(),
|
||||||
)
|
)
|
||||||
self.orderGrid[1] = tmpOrderSeq # seq
|
self.orderGrid[1] = tmpOrderSeq # 建仓单固定在网格索引 1
|
||||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 初始化: 建仓单,建仓价: {price:.3f}')
|
PrintLog(LogLevel.INFO,
|
||||||
elif self.tradeTarget.status == 1: # 下网格单
|
f'|- 标的[{self.tradeTarget.targetName()}] 初始化: '
|
||||||
currentIdx = self.tradeTarget.grid_index # type: ignore
|
f'建仓单,建仓价: {price:.3f}')
|
||||||
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
|
||||||
|
|
||||||
# 向上下一单,向下下一单
|
# ── 分支2: status=1 已建仓 → 下网格买卖单 ──
|
||||||
if currentIdx > 0: # 可以下空单
|
elif self.tradeTarget.status == 1:
|
||||||
sellIdx = currentIdx - 1
|
currentIdx = self.tradeTarget.grid_index # type: ignore
|
||||||
|
|
||||||
|
# --- 上方挂卖出单(空单)---
|
||||||
|
# 条件: grid_index > 0,即当前位置不是价格最低点,还有向下(卖出)空间
|
||||||
|
if currentIdx > 0:
|
||||||
|
sellIdx = currentIdx - 1 # 向上一个网格
|
||||||
sellPrice = self.tradeTarget.getPriceGrid()[sellIdx]
|
sellPrice = self.tradeTarget.getPriceGrid()[sellIdx]
|
||||||
remark = f'{OrderTypeSell},{sellIdx},{self.tradeTarget.stock_code}'
|
sell_remark = self._make_remark(OrderTypeSell, sellIdx)
|
||||||
if len([order for order in orders if order.order_remark == remark]) == 0: # 网格节点没有卖单,下单
|
|
||||||
# 不存在策略内同价位订单,下单
|
# 检查是否已存在同 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(
|
tmpOrderSeq = qmtv.orderAsync(
|
||||||
str(self.tradeTarget.stock_code),
|
str(self.tradeTarget.stock_code),
|
||||||
self.tradeTarget.grid_volume,
|
self.tradeTarget.grid_volume,
|
||||||
xtconstant.STOCK_SELL,
|
xtconstant.STOCK_SELL, # 卖出
|
||||||
sellPrice,
|
sellPrice,
|
||||||
xtconstant.FIX_PRICE,
|
xtconstant.FIX_PRICE,
|
||||||
remark, # remark # type: ignore
|
sell_remark,
|
||||||
self.getName(), # strategy_name
|
self.getName(),
|
||||||
)
|
)
|
||||||
self.orderGrid[sellIdx] = tmpOrderSeq # seq
|
self.orderGrid[sellIdx] = tmpOrderSeq
|
||||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 下空单,价格: {sellPrice:.3f}')
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
|
||||||
|
f'下空单,价格: {sellPrice:.3f}')
|
||||||
else:
|
else:
|
||||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 已存在同价位空单,跳过下单')
|
PrintLog(LogLevel.INFO,
|
||||||
if currentIdx < len(self.tradeTarget.getPriceGrid()) - 1: # 可以下多单
|
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
|
||||||
print(f'length: {len(self.tradeTarget.getPriceGrid())}, currentIdx = {currentIdx}')
|
f'已存在同价位空单,跳过下单')
|
||||||
buyIdx = currentIdx + 1
|
|
||||||
|
# --- 下方挂买入单(多单)---
|
||||||
|
# 条件: grid_index < 价格网格长度-1,即当前位置不是价格最高点,还有向上(买入)空间
|
||||||
|
if currentIdx < len(self.tradeTarget.getPriceGrid()) - 1:
|
||||||
|
buyIdx = currentIdx + 1 # 向下一个网格
|
||||||
buyPrice = self.tradeTarget.getPriceGrid()[buyIdx]
|
buyPrice = self.tradeTarget.getPriceGrid()[buyIdx]
|
||||||
remark = f'{OrderTypeBuy},{buyIdx},{self.tradeTarget.stock_code}'
|
buy_remark = self._make_remark(OrderTypeBuy, buyIdx)
|
||||||
if len([order for order in orders if order.order_type == xtconstant.STOCK_BUY and order.price == buyPrice]) == 0:
|
|
||||||
|
# 检查是否已存在同 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(
|
tmpOrderSeq = qmtv.orderAsync(
|
||||||
str(self.tradeTarget.stock_code),
|
str(self.tradeTarget.stock_code),
|
||||||
self.tradeTarget.grid_volume,
|
self.tradeTarget.grid_volume,
|
||||||
xtconstant.STOCK_BUY,
|
xtconstant.STOCK_BUY, # 买入
|
||||||
buyPrice,
|
buyPrice,
|
||||||
xtconstant.FIX_PRICE,
|
xtconstant.FIX_PRICE,
|
||||||
remark, # remark # type: ignore
|
buy_remark,
|
||||||
self.getName(), # strategy_name
|
self.getName(),
|
||||||
)
|
)
|
||||||
self.orderGrid[buyIdx] = tmpOrderSeq # seq
|
self.orderGrid[buyIdx] = tmpOrderSeq
|
||||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 下多单,价格: {buyPrice:.3f}')
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
|
||||||
|
f'下多单,价格: {buyPrice:.3f}')
|
||||||
else:
|
else:
|
||||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 已存在同价位多单,跳过下单')
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
|
||||||
|
f'已存在同价位多单,跳过下单')
|
||||||
else:
|
else:
|
||||||
PrintLog(LogLevel.INFO, f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: 已过下边界,停止多单交易')
|
# grid_index 已到达价格网格上边界,无法再挂买入单(价格已经到顶)
|
||||||
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- 标的[{self.tradeTarget.targetName()}] 网格策略: '
|
||||||
|
f'已过下边界,停止多单交易')
|
||||||
|
|
||||||
|
# ── 标的管理 ──────────────────────────────────────────────
|
||||||
|
|
||||||
def deleteTradeTarget(self, tradeTarget: model.SFGridTradeTarget):
|
def deleteTradeTarget(self, tradeTarget: model.SFGridTradeTarget):
|
||||||
|
"""
|
||||||
|
从数据库中删除该交易标的
|
||||||
|
|
||||||
|
同时发布 EventTradeTargetDeleted 事件通知 UI 刷新。
|
||||||
|
"""
|
||||||
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}信息删除: START')
|
PrintLog(LogLevel.INFO, f'|- 标的{tradeTarget.targetName()}信息删除: START')
|
||||||
self.dataUpdateLock.acquire()
|
self.dataUpdateLock.acquire()
|
||||||
try:
|
try:
|
||||||
@@ -123,108 +265,313 @@ class SFGridStrategy:
|
|||||||
finally:
|
finally:
|
||||||
self.dataUpdateLock.release()
|
self.dataUpdateLock.release()
|
||||||
|
|
||||||
|
# ── 交易启停控制 ──────────────────────────────────────────
|
||||||
|
|
||||||
def enabledTrading(self, enabled: bool) -> model.SFGridTradeTarget:
|
def enabledTrading(self, enabled: bool) -> model.SFGridTradeTarget:
|
||||||
PrintLog(LogLevel.INFO, f" |- [DEBUG] enabledTrading({enabled}) 调用前: grid_index={self.tradeTarget.grid_index}, status={self.tradeTarget.status}")
|
"""
|
||||||
|
启用或停用该标的的网格交易
|
||||||
|
|
||||||
|
启用时 (enabled=True):
|
||||||
|
- status=0: 初始化网格索引后调用 refreshGridOrder 下建仓单
|
||||||
|
- status=1: 检查持仓是否满足当前网格位置要求,满足则刷新网格订单
|
||||||
|
不满足则回退 enabled=False(风控保护)
|
||||||
|
|
||||||
|
停用时 (enabled=False):
|
||||||
|
- 取消该标的所有未成交订单,停止交易监控
|
||||||
|
|
||||||
|
返回:
|
||||||
|
更新后的 tradeTarget 对象
|
||||||
|
"""
|
||||||
|
PrintLog(LogLevel.INFO,
|
||||||
|
f" |- [DEBUG] enabledTrading({enabled}) 调用前: "
|
||||||
|
f"grid_index={self.tradeTarget.grid_index}, status={self.tradeTarget.status}")
|
||||||
|
|
||||||
self.tradeTarget.enabled = enabled # type: ignore
|
self.tradeTarget.enabled = enabled # type: ignore
|
||||||
|
|
||||||
if enabled:
|
if enabled:
|
||||||
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}交易启动, 持仓量:{self.tradeTarget.current_position}")
|
# ── 启用交易 ──
|
||||||
if self.tradeTarget.status == 0: # 未建仓
|
PrintLog(LogLevel.INFO,
|
||||||
|
f" |- 标的{self.tradeTarget.targetName()}交易启动, "
|
||||||
|
f"持仓量:{self.tradeTarget.current_position}")
|
||||||
|
|
||||||
|
if self.tradeTarget.status == 0:
|
||||||
|
# 未建仓状态: 初始化网格索引
|
||||||
if self.tradeTarget.grid_index == 0:
|
if self.tradeTarget.grid_index == 0:
|
||||||
|
# grid_index=0 表示从未初始化过,设为 1(价格网格最高点建仓)
|
||||||
self.tradeTarget.grid_index = 1 # pyright: ignore[reportAttributeAccessIssue]
|
self.tradeTarget.grid_index = 1 # pyright: ignore[reportAttributeAccessIssue]
|
||||||
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}初始状态, 设置网格序号 1,")
|
PrintLog(LogLevel.INFO,
|
||||||
|
f" |- 标的{self.tradeTarget.targetName()}初始状态, "
|
||||||
|
f"设置网格序号 1,")
|
||||||
else:
|
else:
|
||||||
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}初始状态, 保留网格序号 {self.tradeTarget.grid_index},")
|
# grid_index 非零,保留之前设置的值(可能是手动修改的)
|
||||||
else: # 已建仓
|
PrintLog(LogLevel.INFO,
|
||||||
# 交易阶段,检查仓位,检查现有订单
|
f" |- 标的{self.tradeTarget.targetName()}初始状态, "
|
||||||
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}已有仓位或非初始状态 无需建初始仓 当前仓位: {self.tradeTarget.current_position} 状态: {self.tradeTarget.status}")
|
f"保留网格序号 {self.tradeTarget.grid_index},")
|
||||||
|
else:
|
||||||
|
# 已建仓状态: 检查现有持仓是否满足当前网格位置的仓位需求
|
||||||
|
# 最小需求仓位 = 每格股数 × 当前网格索引
|
||||||
|
# 例: grid_volume=100, grid_index=3 → 需持股 300 股
|
||||||
|
PrintLog(LogLevel.INFO,
|
||||||
|
f" |- 标的{self.tradeTarget.targetName()}已有仓位或非初始状态 "
|
||||||
|
f"无需建初始仓 当前仓位: {self.tradeTarget.current_position} "
|
||||||
|
f"状态: {self.tradeTarget.status}")
|
||||||
|
|
||||||
minRequirePosition: int = self.tradeTarget.grid_volume * int(self.tradeTarget.grid_index) # type: ignore
|
minRequirePosition: int = self.tradeTarget.grid_volume * int(self.tradeTarget.grid_index) # type: ignore
|
||||||
|
|
||||||
if minRequirePosition <= int(self.tradeTarget.current_position): # type: ignore
|
if minRequirePosition <= int(self.tradeTarget.current_position): # type: ignore
|
||||||
PrintLog(LogLevel.INFO, f' |- 仓位检查: 持仓需求充足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}')
|
# 持仓充足,可以继续网格交易
|
||||||
|
PrintLog(LogLevel.INFO,
|
||||||
|
f' |- 仓位检查: 持仓需求充足, '
|
||||||
|
f'(gridVolume*gridIndex)={minRequirePosition}, '
|
||||||
|
f'当前持仓:{self.tradeTarget.current_position}')
|
||||||
else:
|
else:
|
||||||
PrintLog(LogLevel.INFO, f' |- 仓位检查: 持仓需求不足, (gridVolume*gridIndex)={minRequirePosition}, 当前持仓:{self.tradeTarget.current_position}, 交易启动失败')
|
# 持仓不足(可能是之前部分成交或手动减仓),风控:拒绝启用
|
||||||
|
PrintLog(LogLevel.INFO,
|
||||||
|
f' |- 仓位检查: 持仓需求不足, '
|
||||||
|
f'(gridVolume*gridIndex)={minRequirePosition}, '
|
||||||
|
f'当前持仓:{self.tradeTarget.current_position}, '
|
||||||
|
f'交易启动失败')
|
||||||
self.tradeTarget.enabled = False # type: ignore
|
self.tradeTarget.enabled = False # type: ignore
|
||||||
|
|
||||||
|
# 无论 status=0 还是 status=1,最终都调用 refreshGridOrder 下对应的单
|
||||||
self.refreshGridOrder()
|
self.refreshGridOrder()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# ── 停用交易: 取消所有未成交订单 ──
|
||||||
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
orders = qmtv.queryPendingOrder(self.tradeTarget.stock_code, self.getName()) # type: ignore
|
||||||
for order in orders:
|
for order in orders:
|
||||||
qmtv.xttrader.cancel_order_stock_async(qmtv.account, order.order_id)
|
try:
|
||||||
|
qmtv.xt_trader.cancel_order_stock_async(qmtv.account, order.order_id)
|
||||||
|
except AttributeError:
|
||||||
|
pass # 模拟模式无 xt_trader,跳过撤单
|
||||||
|
|
||||||
if len(orders) > 0:
|
if len(orders) > 0:
|
||||||
PrintLog(LogLevel.INFO, f' |- 取消未成交订单 {len(orders)}')
|
PrintLog(LogLevel.INFO, f' |- 取消未成交订单 {len(orders)}')
|
||||||
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}交易监控暂停")
|
PrintLog(LogLevel.INFO, f" |- 标的{self.tradeTarget.targetName()}交易监控暂停")
|
||||||
|
|
||||||
|
# 持久化状态到数据库
|
||||||
self.saveProxy()
|
self.saveProxy()
|
||||||
return self.tradeTarget
|
return self.tradeTarget
|
||||||
|
|
||||||
def isEnabled(self) -> bool:
|
def isEnabled(self) -> bool:
|
||||||
print(f'|- 检查交易状态[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}] - {self.tradeTarget.enabled}')
|
"""查询交易是否已启用"""
|
||||||
return bool(self.tradeTarget.enabled) # 修复返回类型问题
|
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): # 下单成功回调,更新orderID到 self.orderGrid
|
# ── 事件回调: 订单创建 ────────────────────────────────────
|
||||||
remark = response.order_remark.split(',')
|
|
||||||
stockCode = remark[2] # 从remark中获取stockCode
|
def onOrderCreateAsync(self, response: XtOrderResponse):
|
||||||
if response.strategy_name != self.getName() or len(remark) < 3 or self.tradeTarget.stock_code != stockCode:
|
"""
|
||||||
|
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
|
return
|
||||||
|
_, gridIdx, _ = parsed
|
||||||
|
|
||||||
self.dataUpdateLock.acquire()
|
self.dataUpdateLock.acquire()
|
||||||
try:
|
try:
|
||||||
gridIdx = remark[1] # 从remark中获取gridIdx
|
PrintLog(LogLevel.INFO,
|
||||||
PrintLog(LogLevel.INFO, f"委托创建通知 onOrderCreateAsync[{self.tradeTarget.targetName()}]: {response.order_id}")
|
f"委托创建通知 onOrderCreateAsync[{self.tradeTarget.targetName()}]: "
|
||||||
|
f"{response.order_id}")
|
||||||
|
# 将 orderGrid 中的临时 seq 替换为正式 order_id
|
||||||
self.orderGrid[gridIdx] = response.order_id
|
self.orderGrid[gridIdx] = response.order_id
|
||||||
PrintLog(LogLevel.INFO, f"委托创建通知 onOrderCreateAsync 更新 grid-{gridIdx} seq:{response.seq} -> order_id:{response.order_id}")
|
PrintLog(LogLevel.INFO,
|
||||||
|
f"委托创建通知 onOrderCreateAsync 更新 grid-{gridIdx} "
|
||||||
|
f"seq:{response.seq} -> order_id:{response.order_id}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
PrintLog(LogLevel.ERROR, f"|- 委托创建通知 onOrderCreateAsync[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}]: {response.order_id} - {str(e)}")
|
PrintLog(LogLevel.ERROR,
|
||||||
|
f"|- 委托创建通知 onOrderCreateAsync"
|
||||||
|
f"[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}]: "
|
||||||
|
f"{response.order_id} - {str(e)}")
|
||||||
finally:
|
finally:
|
||||||
self.dataUpdateLock.release()
|
self.dataUpdateLock.release()
|
||||||
|
|
||||||
def onOrderTrade(self, trade:XtTrade): # TODO 委托成交通知,处理成交后网格切换
|
# ── 事件回调: 订单失败 ────────────────────────────────────
|
||||||
remark = trade.order_remark.split(',')
|
|
||||||
if trade.strategy_name != self.getName() or len(remark) < 3 or self.tradeTarget.stock_code != trade.stock_code:
|
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
|
return
|
||||||
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}] : {trade.order_id}')
|
_, gridIdx, _ = parsed
|
||||||
|
|
||||||
self.dataUpdateLock.acquire()
|
self.dataUpdateLock.acquire()
|
||||||
try:
|
try:
|
||||||
orderType = trade.order_remark.split(',')[0]
|
# 从 orderGrid 中移除失败的订单条目,后续 refreshGridOrder 会重新挂单
|
||||||
gridIdx = trade.order_remark.split(',')[1] # 从remark中获取gridIdx
|
if gridIdx in self.orderGrid:
|
||||||
type:str = ""
|
|
||||||
if orderType == OrderTypeInit:
|
|
||||||
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] - 建仓单成交')
|
|
||||||
self.tradeTarget.status = 1 # type: ignore
|
|
||||||
self.tradeTarget.init_price = trade.traded_price # type: ignore
|
|
||||||
PrintLog(LogLevel.INFO, f'|- [DEBUG] 建仓单成交: grid_index {self.tradeTarget.grid_index} → 1')
|
|
||||||
self.tradeTarget.grid_index = 1 # type: ignore
|
|
||||||
type = "建仓单"
|
|
||||||
else:
|
|
||||||
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] - 网格单成交')
|
|
||||||
oriIdx = self.tradeTarget.grid_index
|
|
||||||
if gridIdx > self.tradeTarget.grid_index:
|
|
||||||
type = "下移一格"
|
|
||||||
self.tradeTarget.grid_index +=1
|
|
||||||
elif gridIdx < self.tradeTarget.grid_index:
|
|
||||||
type = "上移一格"
|
|
||||||
self.tradeTarget.grid_match_count += 1
|
|
||||||
self.tradeTarget.grid_total_profit += self.tradeTarget.grid_size * trade.traded_volume
|
|
||||||
self.tradeTarget.grid_index -= 1
|
|
||||||
else:
|
|
||||||
type = "保持格, 理论上不应该输出"
|
|
||||||
PrintLog(LogLevel.INFO, f'|- 委托成交通知[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} - 原网格位置 {oriIdx}, 现网格位置 {self.tradeTarget.grid_index}')
|
|
||||||
|
|
||||||
self.saveProxy()
|
|
||||||
del self.orderGrid[gridIdx]
|
del self.orderGrid[gridIdx]
|
||||||
PrintLog(LogLevel.INFO, f"|- 成交报告[{self.tradeTarget.targetName()}] : ====================================")
|
|
||||||
PrintLog(LogLevel.INFO, f"|- 标的[{self.tradeTarget.targetName()}] {type}-单号{trade.order_id}已成交 ")
|
PrintLog(LogLevel.ERROR,
|
||||||
PrintLog(LogLevel.INFO, f' 成交价: {trade.traded_price} 成交量: {trade.traded_volume}')
|
f'委托失败[{self.tradeTarget.targetName()}] grid-{gridIdx}: '
|
||||||
PrintLog(LogLevel.INFO, f' 手续费 : {trade.commission:.3f}')
|
f'order_id={order_error.order_id}, error_id={order_error.error_id}, '
|
||||||
self.refreshGridOrder() # 更新网格订单
|
f'error_msg={order_error.error_msg}')
|
||||||
|
except Exception as e:
|
||||||
|
PrintLog(LogLevel.ERROR,
|
||||||
|
f'委托失败处理异常[{self.tradeTarget.stock_code}]: {str(e)}')
|
||||||
finally:
|
finally:
|
||||||
self.dataUpdateLock.release()
|
self.dataUpdateLock.release()
|
||||||
|
|
||||||
|
# ── 事件回调: 订单成交 ────────────────────────────────────
|
||||||
|
|
||||||
|
def onOrderTrade(self, trade: XtTrade):
|
||||||
|
"""
|
||||||
|
QMT 委托成交通知回调
|
||||||
|
|
||||||
|
成交后:
|
||||||
|
1. 更新网格索引(卖出上移 / 买入下移)
|
||||||
|
2. 如果是建仓单成交: status 0→1, 记录建仓价
|
||||||
|
3. 如果是网格单成交: 累计网格匹配次数和总利润
|
||||||
|
4. 从 orderGrid 删除已成交订单
|
||||||
|
5. 持久化状态到数据库
|
||||||
|
6. 调用 refreshGridOrder 挂新的网格单
|
||||||
|
|
||||||
|
trade.order_remark 格式: "{type},{gridIdx},{stockCode}"
|
||||||
|
"""
|
||||||
|
parsed = self._filter_event(trade.order_remark, trade.strategy_name)
|
||||||
|
if parsed is None:
|
||||||
|
return
|
||||||
|
orderType, gridIdx, _ = parsed
|
||||||
|
|
||||||
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- 委托成交通知'
|
||||||
|
f'[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name}-{trade.order_id}] : '
|
||||||
|
f'{trade.order_id}')
|
||||||
|
|
||||||
|
self.dataUpdateLock.acquire()
|
||||||
|
try:
|
||||||
|
desc: str = "" # 用于日志展示的成交类型描述
|
||||||
|
|
||||||
|
# ── 分支1: 建仓单成交 ──
|
||||||
|
if orderType == OrderTypeInit:
|
||||||
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] '
|
||||||
|
f'- 建仓单成交')
|
||||||
|
# 状态切换: 未建仓(0) → 已建仓(1)
|
||||||
|
self.tradeTarget.status = 1 # type: ignore
|
||||||
|
# 记录建仓价格
|
||||||
|
self.tradeTarget.init_price = trade.traded_price # type: ignore
|
||||||
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- [DEBUG] 建仓单成交: '
|
||||||
|
f'grid_index {self.tradeTarget.grid_index} → 1')
|
||||||
|
# 建仓后网格索引固定为 1(价格网格最高点)
|
||||||
|
self.tradeTarget.grid_index = 1 # type: ignore
|
||||||
|
desc = "建仓单"
|
||||||
|
|
||||||
|
# ── 分支2: 网格单成交 ──
|
||||||
|
else:
|
||||||
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- 委托成交通知[{self.tradeTarget.targetName()}-{trade.order_id}] '
|
||||||
|
f'- 网格单成交')
|
||||||
|
oriIdx = self.tradeTarget.grid_index # 记录原网格位置(用于日志)
|
||||||
|
|
||||||
|
# 判断成交方向: gridIdx > currentIdx → 买入成交(下移)
|
||||||
|
if gridIdx > self.tradeTarget.grid_index:
|
||||||
|
desc = "下移一格"
|
||||||
|
self.tradeTarget.grid_index += 1
|
||||||
|
|
||||||
|
# 判断成交方向: gridIdx < currentIdx → 卖出成交(上移)
|
||||||
|
elif gridIdx < self.tradeTarget.grid_index:
|
||||||
|
desc = "上移一格"
|
||||||
|
# 累计统计
|
||||||
|
self.tradeTarget.grid_match_count += 1 # 网格匹配次数+1
|
||||||
|
self.tradeTarget.grid_total_profit += (
|
||||||
|
self.tradeTarget.grid_size * trade.traded_volume
|
||||||
|
) # 累计利润 = 网格间距 × 成交量
|
||||||
|
self.tradeTarget.grid_index -= 1
|
||||||
|
|
||||||
|
# gridIdx == currentIdx: 理论上不应出现(同一个位置不会挂单给自己)
|
||||||
|
else:
|
||||||
|
desc = "保持格, 理论上不应该输出"
|
||||||
|
|
||||||
|
PrintLog(LogLevel.INFO,
|
||||||
|
f'|- 委托成交通知'
|
||||||
|
f'[{self.tradeTarget.stock_code}-{self.tradeTarget.stock_name} - '
|
||||||
|
f'原网格位置 {oriIdx}, 现网格位置 {self.tradeTarget.grid_index}')
|
||||||
|
|
||||||
|
# ── 成交后处理 ──
|
||||||
|
# 1. 持久化状态到数据库
|
||||||
|
self.saveProxy()
|
||||||
|
# 2. 从 orderGrid 中删除已成交的订单(pop 防重复推送 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):
|
def getName(self):
|
||||||
|
"""返回策略名称,用于在 QMT 中标识订单归属"""
|
||||||
return "SFGRID"
|
return "SFGRID"
|
||||||
|
|
||||||
def saveProxy(self):
|
def saveProxy(self):
|
||||||
PrintLog(LogLevel.DEBUG, f'|- [DEBUG] saveProxy: {self.tradeTarget.targetName()} grid_index={self.tradeTarget.grid_index}, status={self.tradeTarget.status}')
|
"""
|
||||||
|
持久化 tradeTarget 到数据库,并发布 UI 更新事件
|
||||||
|
|
||||||
|
每次状态变更后调用,确保数据库与内存一致,
|
||||||
|
同时通知 UI 刷新表格显示。
|
||||||
|
"""
|
||||||
|
PrintLog(LogLevel.DEBUG,
|
||||||
|
f'|- [DEBUG] saveProxy: {self.tradeTarget.targetName()} '
|
||||||
|
f'grid_index={self.tradeTarget.grid_index}, status={self.tradeTarget.status}')
|
||||||
rc = self.tradeTarget.save()
|
rc = self.tradeTarget.save()
|
||||||
event_bus.publish(EventTradeTargetUpdate, self.tradeTarget)
|
event_bus.publish(EventTradeTargetUpdate, self.tradeTarget)
|
||||||
return rc
|
return rc
|
||||||
+34
-20
@@ -1,24 +1,24 @@
|
|||||||
# coding:utf-8
|
# coding:utf-8
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk, filedialog, messagebox
|
from tkinter import ttk, filedialog, messagebox
|
||||||
import configparser
|
import configparser
|
||||||
from core.main_ui import MainWindow
|
|
||||||
import config as sdConstants
|
import config as sdConstants
|
||||||
|
|
||||||
class ConfigWindow:
|
class ConfigWindow:
|
||||||
def __init__(self, root):
|
def __init__(self, root):
|
||||||
self.root = root
|
self.root = root
|
||||||
self.root.title("系统配置")
|
self.root.title("系统配置")
|
||||||
self.root.geometry("500x250")
|
self.root.geometry("500x300")
|
||||||
self.root.resizable(False, False)
|
self.root.resizable(False, False)
|
||||||
|
|
||||||
# 居中显示
|
# 居中显示
|
||||||
self.root.withdraw() # 先隐藏窗口
|
self.root.withdraw() # 先隐藏窗口
|
||||||
self.root.update_idletasks()
|
self.root.update_idletasks()
|
||||||
x = (self.root.winfo_screenwidth() // 2) - (500 // 2)
|
x = (self.root.winfo_screenwidth() // 2) - (500 // 2)
|
||||||
y = (self.root.winfo_screenheight() // 2) - (250 // 2)
|
y = (self.root.winfo_screenheight() // 2) - (300 // 2)
|
||||||
self.root.geometry(f"500x250+{x}+{y}")
|
self.root.geometry(f"500x300+{x}+{y}")
|
||||||
self.root.deiconify() # 再显示窗口
|
self.root.deiconify() # 再显示窗口
|
||||||
|
|
||||||
self.miniQMTPath = tk.StringVar()
|
self.miniQMTPath = tk.StringVar()
|
||||||
@@ -54,6 +54,15 @@ class ConfigWindow:
|
|||||||
account_entry = ttk.Entry(account_frame, textvariable=self.account_no, width=40)
|
account_entry = ttk.Entry(account_frame, textvariable=self.account_no, width=40)
|
||||||
account_entry.pack(side=tk.LEFT, padx=(10, 0))
|
account_entry.pack(side=tk.LEFT, padx=(10, 0))
|
||||||
|
|
||||||
|
# 模拟模式复选框
|
||||||
|
self.use_simulated = tk.BooleanVar(value=False)
|
||||||
|
simulated_check = ttk.Checkbutton(
|
||||||
|
main_frame,
|
||||||
|
text="使用模拟交易模式(无需真实 QMT 连接)",
|
||||||
|
variable=self.use_simulated
|
||||||
|
)
|
||||||
|
simulated_check.pack(fill=tk.X, pady=5)
|
||||||
|
|
||||||
# 说明文本
|
# 说明文本
|
||||||
info_label = ttk.Label(
|
info_label = ttk.Label(
|
||||||
main_frame,
|
main_frame,
|
||||||
@@ -100,6 +109,7 @@ class ConfigWindow:
|
|||||||
config['config'] = {
|
config['config'] = {
|
||||||
'miniQMTPath': mini_qmt_path.replace('\\', '/'),
|
'miniQMTPath': mini_qmt_path.replace('\\', '/'),
|
||||||
'account_no': account_number,
|
'account_no': account_number,
|
||||||
|
'use_simulated_qmt': str(self.use_simulated.get()),
|
||||||
'log_level': 'INFO'
|
'log_level': 'INFO'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,26 +128,29 @@ def check_and_create_config():
|
|||||||
config_window = ConfigWindow(root)
|
config_window = ConfigWindow(root)
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|
||||||
def ask_mode():
|
def resolve_simulated_mode() -> bool:
|
||||||
"""询问用户选择模式"""
|
"""确定是否使用模拟模式(CLI > 配置文件 > 默认 real)"""
|
||||||
root = tk.Tk()
|
if '--simulated' in sys.argv:
|
||||||
root.withdraw() # 隐藏主窗口
|
print('[配置] 命令行指定: 模拟交易模式')
|
||||||
result = messagebox.askyesno(
|
return True
|
||||||
"选择交易模式",
|
|
||||||
"是否使用模拟交易模式?\n\n" +
|
if sdConstants.exist_config():
|
||||||
"是 → 模拟交易(无需 miniQMT,可在 macOS/Linux 运行)\n" +
|
sdConstants.initConfig()
|
||||||
"否 → 真实交易(需要 Windows + miniQMT)"
|
if sdConstants.use_simulated_qmt:
|
||||||
)
|
print('[配置] 配置文件指定: 模拟交易模式')
|
||||||
root.destroy()
|
return True
|
||||||
return result
|
|
||||||
|
print('[配置] 默认: 真实交易模式')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def initialize_system():
|
def initialize_system():
|
||||||
"""初始化系统"""
|
"""初始化系统"""
|
||||||
|
simulated = resolve_simulated_mode()
|
||||||
|
sdConstants.use_simulated_qmt = simulated
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 询问用户选择模式
|
if simulated:
|
||||||
if ask_mode():
|
|
||||||
# 模拟模式
|
|
||||||
from core.qmt_dummy import qmtv as selected_qmtv
|
from core.qmt_dummy import qmtv as selected_qmtv
|
||||||
print("[模拟模式] 使用模拟交易器")
|
print("[模拟模式] 使用模拟交易器")
|
||||||
sdConstants.miniQMTPath = '/dummy/path'
|
sdConstants.miniQMTPath = '/dummy/path'
|
||||||
@@ -145,16 +158,17 @@ def initialize_system():
|
|||||||
sdConstants.log_level = 'INFO'
|
sdConstants.log_level = 'INFO'
|
||||||
selected_qmtv.init_qmtv()
|
selected_qmtv.init_qmtv()
|
||||||
selected_qmtv.connect()
|
selected_qmtv.connect()
|
||||||
|
from core.main_ui import MainWindow
|
||||||
window = MainWindow(sdConstants.log_level)
|
window = MainWindow(sdConstants.log_level)
|
||||||
window.run()
|
window.run()
|
||||||
else:
|
else:
|
||||||
# 真实 QMT 模式
|
|
||||||
from core.qmt_real import qmtv as selected_qmtv
|
from core.qmt_real import qmtv as selected_qmtv
|
||||||
while True:
|
while True:
|
||||||
if sdConstants.exist_config() and sdConstants.initConfig():
|
if sdConstants.exist_config() and sdConstants.initConfig():
|
||||||
selected_qmtv.init_qmtv()
|
selected_qmtv.init_qmtv()
|
||||||
connected = selected_qmtv.connect()
|
connected = selected_qmtv.connect()
|
||||||
if connected:
|
if connected:
|
||||||
|
from core.main_ui import MainWindow
|
||||||
window = MainWindow(sdConstants.log_level)
|
window = MainWindow(sdConstants.log_level)
|
||||||
window.run()
|
window.run()
|
||||||
break
|
break
|
||||||
|
|||||||
Reference in New Issue
Block a user