fix order busy
This commit is contained in:
@@ -52,24 +52,6 @@ class TradeMixin:
|
||||
) or []
|
||||
return response if isinstance(response, list) else []
|
||||
|
||||
def passorder_latest(self, side: int, stock_code: str, volume: int) -> dict[str, Any]:
|
||||
return self.passorder(side, stock_code, volume)
|
||||
|
||||
def passorder_latest_tagged(
|
||||
self,
|
||||
side: int,
|
||||
stock_code: str,
|
||||
volume: int,
|
||||
strategy_name: str,
|
||||
order_id: str,
|
||||
) -> dict[str, Any]:
|
||||
return self.passorder(
|
||||
side,
|
||||
stock_code,
|
||||
volume,
|
||||
strategy_name=strategy_name,
|
||||
order_id=order_id,
|
||||
)
|
||||
|
||||
def cancel_by_id(self, order_id: str) -> dict[str, Any]:
|
||||
return self._post_json("/api/trade/cancel_by_id", {"order_id": order_id})
|
||||
|
||||
@@ -38,6 +38,7 @@ class OrderBook:
|
||||
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()
|
||||
self.busy_cache = SimpleCache(threshold=10_000, default_timeout=self.lock_timeout_sec)
|
||||
self.mutex = Lock()
|
||||
|
||||
@@ -49,7 +50,8 @@ class OrderBook:
|
||||
def busy(self, code: str, side: str) -> bool:
|
||||
"""判断证券是否存在仍在处理中的同方向委托。"""
|
||||
with self.mutex:
|
||||
return self.busy_cache.has(self._busy_key(side, code))
|
||||
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:
|
||||
@@ -66,6 +68,8 @@ class OrderBook:
|
||||
# 不处理状态不对的
|
||||
if item.status not in TRACKED_STATUSES:
|
||||
continue
|
||||
if item.status in BUSY_STATUSES:
|
||||
busy_keys.add(self._busy_key(item.side, item.code))
|
||||
# 清理过期的
|
||||
if (
|
||||
item.created_at is not None
|
||||
@@ -80,12 +84,11 @@ class OrderBook:
|
||||
# 缓存本次有效订单
|
||||
data.append(item)
|
||||
|
||||
if item.status in BUSY_STATUSES:
|
||||
busy_keys.add(self._busy_key(item.side, item.code))
|
||||
|
||||
|
||||
with self.mutex:
|
||||
self.data = data
|
||||
self.busy_keys = busy_keys
|
||||
log.info("[Order] 刷新完成,跟踪=%d,处理中=%d,撤销=%d", len(data), len(busy_keys), canceled)
|
||||
|
||||
def place(self, request: PlaceOrderRequest) -> bool:
|
||||
@@ -97,18 +100,18 @@ class OrderBook:
|
||||
|
||||
key = self._busy_key(side, request.code)
|
||||
with self.mutex:
|
||||
if self.busy_cache.has(key):
|
||||
if key in self.busy_keys or self.busy_cache.has(key):
|
||||
log.info("[Order] 跳过重复下单,代码=%s,方向=%s", request.code, side)
|
||||
return False
|
||||
self.busy_cache.set(key, True, timeout=self.lock_timeout_sec)
|
||||
|
||||
try:
|
||||
result = request.client.passorder_latest_tagged(
|
||||
request.op,
|
||||
request.code,
|
||||
request.volume,
|
||||
request.strategy_name,
|
||||
request.order_id,
|
||||
result = request.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:
|
||||
log.exception("[Order] 下单失败,代码=%s,本地订单=%s,HTTP状态=%d,错误=%s", request.code, request.order_id, exc.status_code, exc.message or str(exc))
|
||||
|
||||
42
py-client/tests/test_trend_busy.py
Normal file
42
py-client/tests/test_trend_busy.py
Normal file
@@ -0,0 +1,42 @@
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import Mock
|
||||
|
||||
from sdk import OrderItem
|
||||
from strategy.trend.order import OrderBook, PlaceOrderRequest
|
||||
|
||||
|
||||
class TrendBusyTests(unittest.TestCase):
|
||||
def test_snapshot_blocks_order_without_local_cache(self):
|
||||
book = OrderBook()
|
||||
client = Mock()
|
||||
order = OrderItem("1", "A", "BUY", "", "50", None, 100)
|
||||
book.refresh(client, [order])
|
||||
self.assertTrue(book.busy("A", "BUY"))
|
||||
self.assertFalse(book.busy("A", "SELL"))
|
||||
self.assertFalse(book.place(PlaceOrderRequest(client, 23, "A", 100, "local", "trend")))
|
||||
client.passorder.assert_not_called()
|
||||
book.refresh(client, [])
|
||||
self.assertFalse(book.busy("A", "BUY"))
|
||||
|
||||
def test_cancel_request_keeps_order_busy_until_terminal_snapshot(self):
|
||||
book = OrderBook()
|
||||
client = Mock()
|
||||
order = OrderItem("1", "A", "BUY", "", "50", datetime.now() - timedelta(seconds=20), 100)
|
||||
book.refresh(client, [order])
|
||||
client.cancel_by_id.assert_called_once_with("1")
|
||||
self.assertTrue(book.busy("A", "BUY"))
|
||||
order.status = "54"
|
||||
book.refresh(client, [order])
|
||||
self.assertFalse(book.busy("A", "BUY"))
|
||||
|
||||
def test_empty_snapshot_keeps_local_cache_protection(self):
|
||||
book = OrderBook()
|
||||
client = Mock()
|
||||
client.passorder.return_value = {"status": "success", "order_ref": "1"}
|
||||
request = PlaceOrderRequest(client, 23, "A", 100, "local", "trend")
|
||||
self.assertTrue(book.place(request))
|
||||
book.refresh(client, [])
|
||||
self.assertTrue(book.busy("A", "BUY"))
|
||||
self.assertFalse(book.place(request))
|
||||
client.passorder.assert_called_once()
|
||||
Reference in New Issue
Block a user