Files
big-qmt/py-client/strategy/trend/order.py
2026-09-01 14:28:49 +08:00

133 lines
5.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""趋势策略委托簿,对应 Go 客户端的 ``logic/order.go``。"""
from __future__ import annotations
import secrets
import logging as log
from dataclasses import dataclass
from datetime import datetime, timedelta
from threading import Lock
from typing import Any
from sdk import APIError, ORDER_SIDE_BY_OFFSET, Client, OrderItem
# 表示委托仍在处理、可能继续成交的 QMT 状态。
BUSY_STATUSES = {"48", "49", "50", "51", "52", "55"}
COMPLETED_STATUSES = {"56"}
TRACKED_STATUSES = BUSY_STATUSES | COMPLETED_STATUSES
CANCELABLE_STATUSES = {"49", "50", "51", "52"}
@dataclass(frozen=True, slots=True)
class PlaceOrderRequest:
"""``OrderBook.place`` 提交委托所需的全部参数。"""
client: Any
op: int
code: str
volume: int
order_id: str
strategy_name: str
class OrderBook:
"""线程安全的活动委托缓存。"""
def __init__(self, lock_timeout_sec: float = 180, cancel_timeout_sec: float = 10) -> None:
self.lock_timeout_sec = max(0.0, float(lock_timeout_sec))
self.cancel_timeout_sec = timedelta(seconds=cancel_timeout_sec)
self.data: list[OrderItem] = []
self.lock: dict[str, float] = {}
self.mutex = Lock()
@staticmethod
def new_order_id(leg: str) -> str:
"""生成短订单号,为 QMT 备注中的信号键预留空间。"""
return f"zt-{leg[:1]}-{secrets.token_hex(4)}"
def busy(self, code: str, side: str) -> bool:
"""判断证券是否存在仍在处理中的同方向委托。"""
with self.mutex:
key = f"{side}-{code}"
return key in self.lock
def refresh(self, client: Client) -> None:
"""从 QMT 刷新进行中和已完成委托,并撤销超时的活动委托。"""
orders = client.trade_detail_data("order")
current = datetime.now()
now_timestamp = current.timestamp()
data: list[OrderItem] = []
lock: dict[str, float] = {}
canceled = 0
for item in orders:
# 不处理状态不对的
if item.status not in TRACKED_STATUSES:
continue
# 清理过期的
if (
item.created_at is not None
and item.status in CANCELABLE_STATUSES
and current - item.created_at > self.cancel_timeout_sec
):
client.cancel_by_id(item.id)
canceled += 1
log.info("[订单] 超时撤单,代码=%s,方向=%s,柜台订单=%s", item.code, item.side, item.id)
continue
# 缓存本次有效订单
data.append(item)
if item.status in BUSY_STATUSES:
key = f"{item.side}-{item.code}"
lock[key] = (
item.created_at.timestamp()
if item.created_at is not None
else now_timestamp
)
with self.mutex:
self.data = data
self.lock = lock
log.info("[订单] 刷新完成,跟踪=%d,处理中=%d,撤销=%d", len(data), len(lock), canceled)
def place(self, request: PlaceOrderRequest) -> bool:
"""按最新价提交委托,并立即写入本地方向锁。"""
try:
result = request.client.passorder_latest_tagged(
request.op,
request.code,
request.volume,
request.strategy_name,
request.order_id,
)
except APIError as exc:
log.exception("[订单] 下单失败,代码=%s,本地订单=%sHTTP状态=%d,错误=%s", request.code, request.order_id, exc.status_code, exc.message or str(exc))
return False
if not isinstance(result, dict):
log.warning("[订单] 下单失败,代码=%s,本地订单=%s,原因=响应格式无效", request.code, request.order_id)
return False
order_ref = str(result.get("order_ref") or "").strip().lower()
if result.get("status") != "success" or order_ref in {"", "unknown", "none"}:
log.warning("[订单] 下单被拒绝,代码=%s,本地订单=%s,状态=%s,柜台订单=%s", request.code, request.order_id, result.get("status"), order_ref)
return False
side = ORDER_SIDE_BY_OFFSET.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.mutex:
key = f"{side}-{request.code}"
self.data.append(pending)
self.lock[key] = pending.created_at.timestamp()
log.info("[订单] 下单已受理,代码=%s,方向=%s,数量=%d,本地订单=%s,柜台订单=%s", request.code, side, request.volume, request.order_id, order_ref)
return True