2026-09-06 13:12:48 +08:00
|
|
|
|
"""策略共用委托簿。"""
|
2026-09-06 11:53:28 +08:00
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import secrets
|
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
from threading import Lock
|
|
|
|
|
|
from cachelib import SimpleCache
|
|
|
|
|
|
import logging
|
|
|
|
|
|
|
|
|
|
|
|
import httpx
|
2026-09-06 13:12:48 +08:00
|
|
|
|
from sdk import Client, ORDER_SIDE_BY_OFFSET, APIError, OrderItem
|
2026-09-06 11:53:28 +08:00
|
|
|
|
|
|
|
|
|
|
# 表示委托仍在处理、可能继续成交的 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(slots=True)
|
|
|
|
|
|
class PlaceOrderRequest:
|
|
|
|
|
|
"""``OrderBook.place`` 提交委托所需的全部参数。"""
|
2026-09-06 13:12:48 +08:00
|
|
|
|
|
2026-09-06 11:53:28 +08:00
|
|
|
|
op: int
|
|
|
|
|
|
code: str
|
|
|
|
|
|
volume: int
|
|
|
|
|
|
order_id: str
|
|
|
|
|
|
strategy_name: str
|
|
|
|
|
|
kind: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OrderBook:
|
|
|
|
|
|
"""线程安全的活动委托缓存。"""
|
|
|
|
|
|
|
2026-09-06 13:12:48 +08:00
|
|
|
|
def __init__(
|
|
|
|
|
|
self, order_prefix: str, lock_timeout_sec: int = 180, cancel_timeout_sec: float = 10
|
|
|
|
|
|
) -> None:
|
|
|
|
|
|
self.order_prefix = order_prefix
|
2026-09-06 11:53:28 +08:00
|
|
|
|
self.lock_timeout_sec = max(1, lock_timeout_sec)
|
|
|
|
|
|
self.cancel_timeout_sec = timedelta(seconds=cancel_timeout_sec)
|
|
|
|
|
|
self.data: list[OrderItem] = []
|
|
|
|
|
|
self.busy_keys: set[str] = set()
|
2026-09-06 13:12:48 +08:00
|
|
|
|
self.busy_cache = SimpleCache(
|
|
|
|
|
|
threshold=10_000, default_timeout=self.lock_timeout_sec
|
|
|
|
|
|
)
|
2026-09-06 11:53:28 +08:00
|
|
|
|
self.mutex = Lock()
|
|
|
|
|
|
|
2026-09-06 13:12:48 +08:00
|
|
|
|
def new_order_id(self, side: str) -> str:
|
|
|
|
|
|
"""生成带策略前缀的本地订单号。"""
|
|
|
|
|
|
return f"{self.order_prefix}-{side}-{secrets.token_hex(10)}"
|
2026-09-06 11:53:28 +08:00
|
|
|
|
|
|
|
|
|
|
def busy(self, code: str, side: str) -> bool:
|
|
|
|
|
|
"""判断证券是否存在仍在处理中的同方向委托。"""
|
|
|
|
|
|
with self.mutex:
|
|
|
|
|
|
key = self._busy_key(side, code)
|
|
|
|
|
|
return key in self.busy_keys or self.busy_cache.has(key)
|
|
|
|
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
|
|
def _busy_key(side: str, code: str) -> str:
|
|
|
|
|
|
return f"{side}-{code}"
|
|
|
|
|
|
|
|
|
|
|
|
def refresh(self, client: Client, orders: list[OrderItem]) -> None:
|
|
|
|
|
|
"""用账户快照刷新委托,并撤销超时的活动委托。"""
|
|
|
|
|
|
current = datetime.now()
|
|
|
|
|
|
data: list[OrderItem] = []
|
|
|
|
|
|
busy_keys: set[str] = set()
|
|
|
|
|
|
canceled = 0
|
|
|
|
|
|
|
|
|
|
|
|
for item in orders:
|
|
|
|
|
|
# 不处理状态不对的
|
2026-09-07 14:04:26 +08:00
|
|
|
|
if str(item.order_status) not in TRACKED_STATUSES:
|
2026-09-06 11:53:28 +08:00
|
|
|
|
continue
|
2026-09-07 14:04:26 +08:00
|
|
|
|
if str(item.order_status) in BUSY_STATUSES:
|
|
|
|
|
|
busy_keys.add(self._busy_key(item.side, item.stock_code))
|
2026-09-06 11:53:28 +08:00
|
|
|
|
# 清理过期的
|
|
|
|
|
|
if (
|
|
|
|
|
|
item.created_at is not None
|
2026-09-06 13:12:48 +08:00
|
|
|
|
and item.local_order_id.startswith(f"{self.order_prefix}-")
|
2026-09-07 14:04:26 +08:00
|
|
|
|
and str(item.order_status) in CANCELABLE_STATUSES
|
2026-09-06 11:53:28 +08:00
|
|
|
|
and current - item.created_at > self.cancel_timeout_sec
|
|
|
|
|
|
):
|
|
|
|
|
|
try:
|
2026-09-07 14:04:26 +08:00
|
|
|
|
client.cancel_by_id(item.order_sys_id)
|
2026-09-06 11:53:28 +08:00
|
|
|
|
canceled += 1
|
2026-09-06 13:12:48 +08:00
|
|
|
|
logging.info(
|
|
|
|
|
|
"[Order] 超时撤单,代码=%s,方向=%s,柜台订单=%s",
|
2026-09-07 14:04:26 +08:00
|
|
|
|
item.stock_code,
|
2026-09-06 13:12:48 +08:00
|
|
|
|
item.side,
|
2026-09-07 14:04:26 +08:00
|
|
|
|
item.order_sys_id,
|
2026-09-06 13:12:48 +08:00
|
|
|
|
)
|
2026-09-06 11:53:28 +08:00
|
|
|
|
except Exception:
|
2026-09-06 13:12:48 +08:00
|
|
|
|
logging.exception(
|
2026-09-07 14:04:26 +08:00
|
|
|
|
"[Order] 撤单失败,保留在途状态,订单=%s", item.order_sys_id
|
2026-09-06 13:12:48 +08:00
|
|
|
|
)
|
2026-09-06 11:53:28 +08:00
|
|
|
|
|
|
|
|
|
|
# 缓存本次有效订单
|
|
|
|
|
|
data.append(item)
|
|
|
|
|
|
|
|
|
|
|
|
with self.mutex:
|
|
|
|
|
|
self.data = data
|
|
|
|
|
|
self.busy_keys = busy_keys
|
2026-09-06 13:12:48 +08:00
|
|
|
|
logging.info(
|
|
|
|
|
|
"[Order] 刷新完成,跟踪=%d,处理中=%d,撤销=%d",
|
|
|
|
|
|
len(data),
|
|
|
|
|
|
len(busy_keys),
|
|
|
|
|
|
canceled,
|
|
|
|
|
|
)
|
2026-09-06 11:53:28 +08:00
|
|
|
|
|
|
|
|
|
|
def place(self, client: Client, request: PlaceOrderRequest) -> bool:
|
|
|
|
|
|
"""按最新价提交委托,并立即写入本地方向锁。"""
|
|
|
|
|
|
side = ORDER_SIDE_BY_OFFSET.get(str(request.op), "")
|
|
|
|
|
|
if not side:
|
2026-09-06 13:12:48 +08:00
|
|
|
|
logging.warning(
|
|
|
|
|
|
"[Order] 下单失败,代码=%s,原因=未知买卖方向(%s)",
|
|
|
|
|
|
request.code,
|
|
|
|
|
|
request.op,
|
|
|
|
|
|
)
|
2026-09-06 11:53:28 +08:00
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
key = self._busy_key(side, request.code)
|
|
|
|
|
|
with self.mutex:
|
|
|
|
|
|
if key in self.busy_keys or self.busy_cache.has(key):
|
2026-09-06 13:12:48 +08:00
|
|
|
|
logging.info(
|
|
|
|
|
|
"[Order] 跳过重复下单,代码=%s,方向=%s", request.code, side
|
|
|
|
|
|
)
|
2026-09-06 11:53:28 +08:00
|
|
|
|
return False
|
|
|
|
|
|
self.busy_cache.set(key, True, timeout=self.lock_timeout_sec)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = client.passorder(
|
|
|
|
|
|
op_type=request.op,
|
|
|
|
|
|
stock_code=request.code,
|
|
|
|
|
|
volume=request.volume,
|
|
|
|
|
|
strategy_name=request.strategy_name,
|
|
|
|
|
|
order_id=request.order_id,
|
|
|
|
|
|
)
|
|
|
|
|
|
except APIError as exc:
|
2026-09-06 13:12:48 +08:00
|
|
|
|
logging.exception(
|
|
|
|
|
|
"[Order] 下单失败,代码=%s,本地订单=%s,HTTP状态=%d,错误=%s",
|
|
|
|
|
|
request.code,
|
|
|
|
|
|
request.order_id,
|
|
|
|
|
|
exc.status_code,
|
|
|
|
|
|
exc.message or str(exc),
|
|
|
|
|
|
)
|
2026-09-06 11:53:28 +08:00
|
|
|
|
return False
|
|
|
|
|
|
except (httpx.RequestError, ValueError):
|
|
|
|
|
|
# 响应异常不能证明柜台未受理,保留缓存防重,不自动重试。
|
2026-09-06 13:12:48 +08:00
|
|
|
|
logging.exception(
|
|
|
|
|
|
"[Order] 下单请求或响应异常,代码=%s,本地订单=%s",
|
|
|
|
|
|
request.code,
|
|
|
|
|
|
request.order_id,
|
|
|
|
|
|
)
|
2026-09-06 11:53:28 +08:00
|
|
|
|
return False
|
2026-09-06 13:12:48 +08:00
|
|
|
|
|
|
|
|
|
|
logging.info(
|
|
|
|
|
|
"[Order] 下单已受理,代码=%s,方向=%s,数量=%d,本地订单=%s,返回=%s",
|
|
|
|
|
|
request.code,
|
|
|
|
|
|
side,
|
|
|
|
|
|
request.volume,
|
|
|
|
|
|
request.order_id,
|
|
|
|
|
|
result,
|
|
|
|
|
|
)
|
2026-09-06 11:53:28 +08:00
|
|
|
|
return True
|