544 lines
27 KiB
Python
544 lines
27 KiB
Python
"""
|
||
网格交易策略控制器
|
||
|
||
核心逻辑:在预设的价格网格上低买高卖,每个网格节点同时挂一对买卖单,
|
||
成交后自动切换到相邻网格并刷新订单。
|
||
|
||
网格结构示意(以 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
|
||
|
||
# 数据更新锁:保护 orderGrid 和 tradeTarget 的并发访问
|
||
# QMT 回调在独立线程中触发,必须在可能触发回调的操作之前创建
|
||
# 注意:这个锁必须在订阅事件之前创建,防止事件在初始化期间触发
|
||
self.dataUpdateLock = threading.Lock()
|
||
|
||
# 订阅事件总线:监听订单创建、成交、失败三种事件
|
||
event_bus.subscribe(eBus.MarketOrderCreated, self.onOrderCreateAsync)
|
||
event_bus.subscribe(eBus.MarketOrderTraded, self.onOrderTrade)
|
||
event_bus.subscribe(eBus.MarketOrderError, self.onOrderError)
|
||
event_bus.subscribe(eBus.EventMarketActiveSwitch, self.onMarketActiveSwitch)
|
||
|
||
# 获取当日涨跌停价格(用于价格边界校验)
|
||
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()
|
||
|
||
# 根据数据库中的 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 层调用)
|
||
|
||
当市场数据从不可用变为可用时,如果策略已启用则刷新网格订单。
|
||
"""
|
||
PrintLog(LogLevel.INFO,
|
||
f'|- [市场状态切换] 标的{self.tradeTarget.targetName()} '
|
||
f'isActive={isActive}, enabled={self.tradeTarget.enabled}')
|
||
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): 挂买入单(价格更低时补仓)
|
||
每个方向都先检查是否已存在同价位订单,避免重复下单
|
||
"""
|
||
# ── 前置检查:市场和策略状态 ──
|
||
# 注意:这里用 dataUpdateLock 包裹检查和下单操作,防止竞态条件:
|
||
# 主线程在检查 isMarketActive 时,另一线程的行情回调可能同时将其设为 True,
|
||
# 导致部分标的通过检查下单,部分被拦截(表现为一票有单、一票无单)
|
||
with self.dataUpdateLock:
|
||
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 not hasattr(self, 'todayUpStopPrice') or self.todayUpStopPrice is None:
|
||
self.todayUpStopPrice = qmtv.dailyUpStop(self.tradeTarget.stock_code) # type: ignore
|
||
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 not hasattr(self, 'todayDownStopPrice') or self.todayDownStopPrice is None:
|
||
self.todayDownStopPrice = qmtv.dailyDownStop(self.tradeTarget.stock_code) # type: ignore
|
||
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
|
||
|
||
# 刷新网格订单(空仓只挂买单,有仓买卖对冲)
|
||
# 只有市场活跃时才下单,收盘后不再尝试下单
|
||
if qmtv.isMarketActive:
|
||
self.refreshGridOrder()
|
||
else:
|
||
PrintLog(LogLevel.INFO,
|
||
f' |- 市场已休市,跳过刷新网格订单')
|
||
|
||
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 位置重新挂买卖单
|
||
# 只有市场活跃时才下单,收盘后不再尝试下单
|
||
if qmtv.isMarketActive:
|
||
self.refreshGridOrder()
|
||
else:
|
||
PrintLog(LogLevel.INFO,
|
||
f'|- 成交后市场已休市,跳过刷新网格订单')
|
||
|
||
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
|