update api,libs
This commit is contained in:
@@ -8,8 +8,7 @@ from datetime import datetime, timedelta
|
||||
from threading import Lock
|
||||
from typing import Any
|
||||
|
||||
# QMT 开平方向字段到本地买卖方向的映射。
|
||||
OFFSET_FLAG = {"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"}
|
||||
from sdk import ORDER_SIDE_BY_OFFSET, Client, OrderItem
|
||||
|
||||
# 表示委托仍在处理、可能继续成交的 QMT 状态。
|
||||
BUSY_STATUSES = {"48", "49", "50", "51", "52", "55"}
|
||||
@@ -27,56 +26,45 @@ class PlaceOrderRequest:
|
||||
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)
|
||||
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: dict[str, OrderItem] = {}
|
||||
self.index: list[str] = []
|
||||
self.lock = Lock()
|
||||
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 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:
|
||||
with self.mutex:
|
||||
self._clear_expired_locks(datetime.now().timestamp())
|
||||
key = f"{side}-{code}"
|
||||
order = self.data.get(key)
|
||||
return key in self.index or bool(order and order.status in BUSY_STATUSES)
|
||||
return key in self.lock
|
||||
|
||||
def refresh(self, client: Any) -> None:
|
||||
def refresh(self, client: Client) -> None:
|
||||
"""从 QMT 刷新当前委托明细和方向索引。"""
|
||||
parsed_orders = [
|
||||
parse_order(row) for row in client.trade_detail_data("order")
|
||||
]
|
||||
with self.lock:
|
||||
orders = client.trade_detail_data("order")
|
||||
parsed_orders = [(f"{item.side}-{item.code}", item) for item in orders]
|
||||
now_timestamp = datetime.now().timestamp()
|
||||
with self.mutex:
|
||||
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
|
||||
]
|
||||
self.lock = {
|
||||
key: (
|
||||
item.created_at.timestamp()
|
||||
if item.created_at is not None
|
||||
else now_timestamp
|
||||
)
|
||||
for key, item in parsed_orders
|
||||
if item.status in BUSY_STATUSES
|
||||
}
|
||||
self._clear_expired_locks(now_timestamp)
|
||||
|
||||
def cancel_expired(self, client: Any, now: datetime | None = None) -> None:
|
||||
"""尝试撤销超过有效期且具有委托编号的订单。"""
|
||||
@@ -87,7 +75,8 @@ class OrderBook:
|
||||
for order in list(self.data.values()):
|
||||
if (
|
||||
order.created_at is not None
|
||||
and current - order.created_at > self.timeout
|
||||
and order.status in {"49", "50", "51", "52"}
|
||||
and current - order.created_at > self.cancel_timeout_sec
|
||||
and order.id
|
||||
):
|
||||
client.cancel_by_id(order.id)
|
||||
@@ -107,7 +96,7 @@ class OrderBook:
|
||||
if result.get("status") != "success" or order_ref in {"", "unknown", "none"}:
|
||||
return False
|
||||
|
||||
side = OFFSET_FLAG.get(str(request.op), "")
|
||||
side = ORDER_SIDE_BY_OFFSET.get(str(request.op), "")
|
||||
pending = OrderItem(
|
||||
id=order_ref,
|
||||
code=request.code,
|
||||
@@ -118,61 +107,18 @@ class OrderBook:
|
||||
volume=request.volume,
|
||||
local_order_id=request.order_id,
|
||||
)
|
||||
with self.lock:
|
||||
with self.mutex:
|
||||
key = f"{side}-{request.code}"
|
||||
self.data[key] = pending
|
||||
if key not in self.index:
|
||||
self.index.append(key)
|
||||
self.lock[key] = pending.created_at.timestamp()
|
||||
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 ""
|
||||
def _clear_expired_locks(self, now_timestamp: float) -> None:
|
||||
"""清理过期方向锁;调用方必须已持有 ``mutex``。"""
|
||||
expired = [
|
||||
key
|
||||
for key, created_at in self.lock.items()
|
||||
if now_timestamp - created_at >= self.lock_timeout_sec
|
||||
]
|
||||
for key in expired:
|
||||
self.lock.pop(key, None)
|
||||
|
||||
Reference in New Issue
Block a user