121 lines
3.9 KiB
Python
121 lines
3.9 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
|
|
|
|
from sdk import 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] = {}
|
|
|
|
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)
|
|
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
|
|
|
|
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 = 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[key] = pending
|
|
self.lock[key] = pending.created_at.timestamp()
|
|
return True
|