Files
big-qmt/py-client/strategy/trend/order.py

179 lines
5.7 KiB
Python

"""趋势策略委托簿,对应 Go 客户端的 ``logic/order.go``。"""
from __future__ import annotations
import secrets
from dataclasses import dataclass
from datetime import datetime, timedelta
from threading import Lock
from typing import Any
# QMT 开平方向字段到本地买卖方向的映射。
OFFSET_FLAG = {"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"}
# 表示委托仍在处理、可能继续成交的 QMT 状态。
BUSY_STATUSES = {"48", "49", "50", "51", "52", "55"}
@dataclass(frozen=True, slots=True)
class PlaceOrderRequest:
"""``OrderBook.place`` 提交委托所需的全部参数。"""
client: Any
op: int
code: str
volume: int
order_id: str
strategy_name: str
@dataclass(slots=True)
class OrderItem:
"""从 QMT 委托明细转换得到的本地订单记录。"""
id: str
code: str
side: str
remark: str
status: str
created_at: datetime | None
volume: int
local_order_id: str = ""
class OrderBook:
"""线程安全的活动委托缓存。"""
def __init__(self, timeout_seconds: float = 300) -> None:
self.timeout = timedelta(seconds=timeout_seconds)
self.data: dict[str, OrderItem] = {}
self.index: list[str] = []
self.lock = Lock()
@staticmethod
def new_order_id(leg: str) -> str:
"""生成短订单号,为 QMT 备注中的信号键预留空间。"""
return f"zt-{leg[:1]}-{secrets.token_hex(4)}"
def is_lock(self, side: str, code: str) -> bool:
"""判断证券在指定买卖方向上是否已经被委托锁定。"""
with self.lock:
return f"{side}-{code}" in self.index
def busy(self, code: str, side: str) -> bool:
"""判断证券是否存在仍在处理中的同方向委托。"""
with self.lock:
key = f"{side}-{code}"
order = self.data.get(key)
return key in self.index or bool(order and order.status in BUSY_STATUSES)
def refresh(self, client: Any) -> None:
"""从 QMT 刷新当前委托明细和方向索引。"""
parsed_orders = [
parse_order(row) for row in client.trade_detail_data("order")
]
with self.lock:
self.data = {key: item for key, item in parsed_orders}
self.index = [
key for key, item in parsed_orders if item.status in BUSY_STATUSES
]
def cancel_expired(self, client: Any, now: datetime | None = None) -> None:
"""尝试撤销超过有效期且具有委托编号的订单。"""
self.refresh(client)
current = now or datetime.now()
# 使用快照遍历,避免网络调用期间长期持有互斥锁。
for order in list(self.data.values()):
if (
order.created_at is not None
and current - order.created_at > self.timeout
and order.id
):
client.cancel_by_id(order.id)
def place(self, request: PlaceOrderRequest) -> bool:
"""按最新价提交委托,并立即写入本地方向锁。"""
result = request.client.passorder_latest_tagged(
request.op,
request.code,
request.volume,
request.strategy_name,
request.order_id,
)
if not isinstance(result, dict):
return False
order_ref = str(result.get("order_ref") or "").strip().lower()
if result.get("status") != "success" or order_ref in {"", "unknown", "none"}:
return False
side = OFFSET_FLAG.get(str(request.op), "")
pending = OrderItem(
id=order_ref,
code=request.code,
side=side,
remark=request.order_id,
status="48",
created_at=datetime.now(),
volume=request.volume,
local_order_id=request.order_id,
)
with self.lock:
key = f"{side}-{request.code}"
self.data[key] = pending
if key not in self.index:
self.index.append(key)
return True
def parse_order(row: dict[str, Any]) -> tuple[str, OrderItem]:
"""把 QMT 原始委托字段转换为本地订单及其索引键。"""
volume = _as_int(row.get("m_nVolumeTotal")) + _as_int(
row.get("m_nVolumeTraded")
)
timestamp = _as_int(row.get("m_nOrderTime"))
if timestamp > 100_000_000_000:
# QMT 某些版本返回毫秒时间戳。
timestamp /= 1000
created_at = (
datetime.fromtimestamp(timestamp)
if timestamp
else _parse_insert_datetime(row)
)
item = OrderItem(
id=str(row.get("m_strOrderSysID") or ""),
code=str(row.get("m_strInstrumentID") or ""),
side=OFFSET_FLAG.get(str(row.get("m_nOffsetFlag")), ""),
remark=str(row.get("m_strRemark") or ""),
status=str(row.get("m_nOrderStatus") or ""),
created_at=created_at,
volume=volume,
local_order_id=_local_order_id(str(row.get("m_strRemark") or "")),
)
return f"{item.side}-{item.code}", item
def _as_int(value: Any) -> int:
"""安全转换整数,无效值按 0 处理。"""
try:
return int(value or 0)
except (TypeError, ValueError):
return 0
def _parse_insert_datetime(row: dict[str, Any]) -> datetime | None:
"""使用委托日期和时间字段构造本地时间。"""
date = str(row.get("m_strInsertDate") or "")
clock = str(row.get("m_strInsertTime") or "").replace(":", "").zfill(6)
try:
return datetime.strptime(date + clock, "%Y%m%d%H%M%S")
except ValueError:
return None
def _local_order_id(remark: str) -> str:
"""兼容 ``local_order_id|signal_key`` 形式的 QMT 备注。"""
return remark.split("|", 1)[0] if remark else ""