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

133 lines
5.0 KiB
Python
Raw Normal View History

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