feat order.py
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -73,15 +73,15 @@ def StartTrend() -> None:
|
||||
)
|
||||
assets = client.assets()
|
||||
_, positions = client.positions()
|
||||
order_book = OrderBook()
|
||||
order_book.refresh()
|
||||
|
||||
storeState = State.for_strategy(
|
||||
config.global_config.qmt_data_dir,
|
||||
config.account_config.strategy,
|
||||
config.account_config.account_id,
|
||||
)
|
||||
orders = client.trade_detail_data("order")
|
||||
deals = client.deals()
|
||||
storeState.reconcile(positions, orders, deals)
|
||||
storeState.reconcile(positions, order_book.data)
|
||||
|
||||
# 获取本策略的信号开仓数据
|
||||
signals = init_signals(
|
||||
@@ -93,7 +93,7 @@ def StartTrend() -> None:
|
||||
global_cfg=config.global_config,
|
||||
account_cfg=config.account_config,
|
||||
state=storeState,
|
||||
orders=OrderBook(),
|
||||
orders=order_book,
|
||||
open_watch=DipWatch(),
|
||||
add_watch=DipWatch(),
|
||||
profit_tracker=GridTrailingTracker(config.account_config.grid_step_pct),
|
||||
@@ -120,7 +120,7 @@ def RunOnce(run: Runtime, signals) -> None:
|
||||
|
||||
# 1. 取消超过有效期仍未完成的委托订单。
|
||||
try:
|
||||
run.orders.cancel_expired(run.client)
|
||||
run.orders.refresh(run.client)
|
||||
except Exception:
|
||||
logging.exception("取消过期订单失败")
|
||||
|
||||
|
||||
@@ -12,6 +12,9 @@ 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)
|
||||
@@ -32,7 +35,7 @@ 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: dict[str, OrderItem] = {}
|
||||
self.data: list[OrderItem] = []
|
||||
self.lock: dict[str, float] = {}
|
||||
self.mutex = Lock()
|
||||
|
||||
@@ -44,42 +47,45 @@ class OrderBook:
|
||||
def busy(self, code: str, side: str) -> bool:
|
||||
"""判断证券是否存在仍在处理中的同方向委托。"""
|
||||
with self.mutex:
|
||||
self._clear_expired_locks(datetime.now().timestamp())
|
||||
key = f"{side}-{code}"
|
||||
return key in self.lock
|
||||
|
||||
def refresh(self, client: Client) -> None:
|
||||
"""从 QMT 刷新当前委托明细和方向索引。"""
|
||||
"""从 QMT 刷新进行中和已完成委托,并撤销超时的活动委托。"""
|
||||
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.lock = {
|
||||
key: (
|
||||
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
|
||||
)
|
||||
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:
|
||||
"""尝试撤销超过有效期且具有委托编号的订单。"""
|
||||
self.refresh(client)
|
||||
current = now or datetime.now()
|
||||
|
||||
# 使用快照遍历,避免网络调用期间长期持有互斥锁。
|
||||
for order in list(self.data.values()):
|
||||
if (
|
||||
order.created_at is not None
|
||||
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)
|
||||
with self.mutex:
|
||||
self.data = data
|
||||
self.lock = lock
|
||||
|
||||
def place(self, request: PlaceOrderRequest) -> bool:
|
||||
"""按最新价提交委托,并立即写入本地方向锁。"""
|
||||
@@ -112,13 +118,3 @@ class OrderBook:
|
||||
self.data[key] = pending
|
||||
self.lock[key] = pending.created_at.timestamp()
|
||||
return True
|
||||
|
||||
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)
|
||||
|
||||
@@ -136,7 +136,6 @@ class State:
|
||||
self,
|
||||
positions: Iterable[PositionItem],
|
||||
orders: list[OrderItem],
|
||||
deals: list[dict[str, str]],
|
||||
) -> None:
|
||||
"""用真实持仓、委托和成交恢复本地状态,不增加持久化字段。"""
|
||||
position_list = list(positions)
|
||||
|
||||
BIN
py-client/tests/__pycache__/test_ipo.cpython-311.pyc
Normal file
BIN
py-client/tests/__pycache__/test_ipo.cpython-311.pyc
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,12 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
from datetime import datetime, timedelta
|
||||
from tempfile import TemporaryDirectory
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
from libs.grid_take_profit import GridState, GridTrailingTracker
|
||||
from sdk import Assets, PositionItem, Tick
|
||||
from sdk import Assets, OrderItem, PositionItem, Tick
|
||||
from strategy.trend.order import OrderBook, PlaceOrderRequest
|
||||
from strategy.trend.positions import LOSS_TIERS, handle_loss, manage_positions
|
||||
from strategy.trend.boot import RunOnce
|
||||
@@ -22,6 +23,18 @@ class FakeClient:
|
||||
return {"status": "success", "order_ref": f"broker-{len(self.orders)}"}
|
||||
|
||||
|
||||
class FakeOrderClient:
|
||||
def __init__(self, orders):
|
||||
self.orders = orders
|
||||
self.canceled = []
|
||||
|
||||
def trade_detail_data(self, _datatype):
|
||||
return self.orders
|
||||
|
||||
def cancel_by_id(self, order_id):
|
||||
self.canceled.append(order_id)
|
||||
|
||||
|
||||
class TrendTests(unittest.TestCase):
|
||||
def test_grid_states_and_account_isolation(self):
|
||||
tracker = GridTrailingTracker(1)
|
||||
@@ -39,6 +52,22 @@ class TrendTests(unittest.TestCase):
|
||||
self.assertTrue(book.place(request))
|
||||
self.assertTrue(book.busy("000001.SZ", "BUY"))
|
||||
|
||||
def test_refresh_tracks_active_and_completed_and_cancels_expired(self):
|
||||
old = datetime.now() - timedelta(seconds=20)
|
||||
orders = [
|
||||
OrderItem("active", "A", "BUY", "", "49", old, 100),
|
||||
OrderItem("completed", "B", "SELL", "", "56", old, 100),
|
||||
OrderItem("canceled", "C", "BUY", "", "54", old, 100),
|
||||
OrderItem("failed", "D", "BUY", "", "57", old, 100),
|
||||
]
|
||||
client = FakeOrderClient(orders)
|
||||
book = OrderBook(cancel_timeout_sec=10)
|
||||
|
||||
book.refresh(client)
|
||||
|
||||
self.assertEqual(set(book.data), {"BUY-A", "SELL-B"})
|
||||
self.assertEqual(client.canceled, ["active"])
|
||||
|
||||
def test_position_dataclasses_execute_without_type_error(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
state = State.for_strategy(directory, "trend", "A")
|
||||
@@ -121,7 +150,7 @@ class TrendTests(unittest.TestCase):
|
||||
client=client,
|
||||
account_cfg=SimpleNamespace(min_cash_ratio=0.1),
|
||||
global_cfg=SimpleNamespace(api_host="http://example"),
|
||||
orders=SimpleNamespace(cancel_expired=lambda _client: None),
|
||||
orders=SimpleNamespace(refresh=lambda _client: None),
|
||||
state=SimpleNamespace(
|
||||
codes=["A"],
|
||||
unresolved_codes=[],
|
||||
|
||||
Reference in New Issue
Block a user