feat order.py

This commit is contained in:
2026-08-30 14:46:34 +08:00
parent cdccc48d8c
commit 6334f10904
19 changed files with 70 additions and 46 deletions

View File

@@ -1123,7 +1123,7 @@ class AssetsHandler(BaseHandler):
info = _data[0] if _data else None info = _data[0] if _data else None
if not info: if not info:
raise HTTPError(500, "Failed to get account data") raise HTTPError(500, "Failed to get account data")
self.write(json.dumps({"data": info}, ensure_ascii=False)) self.write(json.dumps({"total": round(info.m_dBalance, 2),"available": round(info.m_dAvailable, 2)}, ensure_ascii=False))
# get_trade_detail_data('account') - Query total assets # get_trade_detail_data('account') - Query total assets

View File

@@ -73,15 +73,15 @@ def StartTrend() -> None:
) )
assets = client.assets() assets = client.assets()
_, positions = client.positions() _, positions = client.positions()
order_book = OrderBook()
order_book.refresh()
storeState = State.for_strategy( storeState = State.for_strategy(
config.global_config.qmt_data_dir, config.global_config.qmt_data_dir,
config.account_config.strategy, config.account_config.strategy,
config.account_config.account_id, config.account_config.account_id,
) )
orders = client.trade_detail_data("order") storeState.reconcile(positions, order_book.data)
deals = client.deals()
storeState.reconcile(positions, orders, deals)
# 获取本策略的信号开仓数据 # 获取本策略的信号开仓数据
signals = init_signals( signals = init_signals(
@@ -93,7 +93,7 @@ def StartTrend() -> None:
global_cfg=config.global_config, global_cfg=config.global_config,
account_cfg=config.account_config, account_cfg=config.account_config,
state=storeState, state=storeState,
orders=OrderBook(), orders=order_book,
open_watch=DipWatch(), open_watch=DipWatch(),
add_watch=DipWatch(), add_watch=DipWatch(),
profit_tracker=GridTrailingTracker(config.account_config.grid_step_pct), profit_tracker=GridTrailingTracker(config.account_config.grid_step_pct),
@@ -120,7 +120,7 @@ def RunOnce(run: Runtime, signals) -> None:
# 1. 取消超过有效期仍未完成的委托订单。 # 1. 取消超过有效期仍未完成的委托订单。
try: try:
run.orders.cancel_expired(run.client) run.orders.refresh(run.client)
except Exception: except Exception:
logging.exception("取消过期订单失败") logging.exception("取消过期订单失败")

View File

@@ -12,6 +12,9 @@ from sdk import ORDER_SIDE_BY_OFFSET, Client, OrderItem
# 表示委托仍在处理、可能继续成交的 QMT 状态。 # 表示委托仍在处理、可能继续成交的 QMT 状态。
BUSY_STATUSES = {"48", "49", "50", "51", "52", "55"} 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) @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: 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.lock_timeout_sec = max(0.0, float(lock_timeout_sec))
self.cancel_timeout_sec = timedelta(seconds=cancel_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.lock: dict[str, float] = {}
self.mutex = Lock() self.mutex = Lock()
@@ -44,42 +47,45 @@ class OrderBook:
def busy(self, code: str, side: str) -> bool: def busy(self, code: str, side: str) -> bool:
"""判断证券是否存在仍在处理中的同方向委托。""" """判断证券是否存在仍在处理中的同方向委托。"""
with self.mutex: with self.mutex:
self._clear_expired_locks(datetime.now().timestamp())
key = f"{side}-{code}" key = f"{side}-{code}"
return key in self.lock return key in self.lock
def refresh(self, client: Client) -> None: def refresh(self, client: Client) -> None:
"""从 QMT 刷新当前委托明细和方向索引""" """从 QMT 刷新进行中和已完成委托,并撤销超时的活动委托"""
orders = client.trade_detail_data("order") orders = client.trade_detail_data("order")
parsed_orders = [(f"{item.side}-{item.code}", item) for item in orders] current = datetime.now()
now_timestamp = datetime.now().timestamp() now_timestamp = current.timestamp()
with self.mutex: data: list[OrderItem] = []
self.data = {key: item for key, item in parsed_orders} lock: dict[str, float] = {}
self.lock = {
key: ( 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() item.created_at.timestamp()
if item.created_at is not None if item.created_at is not None
else now_timestamp 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: with self.mutex:
"""尝试撤销超过有效期且具有委托编号的订单。""" self.data = data
self.refresh(client) self.lock = lock
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)
def place(self, request: PlaceOrderRequest) -> bool: def place(self, request: PlaceOrderRequest) -> bool:
"""按最新价提交委托,并立即写入本地方向锁。""" """按最新价提交委托,并立即写入本地方向锁。"""
@@ -112,13 +118,3 @@ class OrderBook:
self.data[key] = pending self.data[key] = pending
self.lock[key] = pending.created_at.timestamp() self.lock[key] = pending.created_at.timestamp()
return True 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)

View File

@@ -136,7 +136,6 @@ class State:
self, self,
positions: Iterable[PositionItem], positions: Iterable[PositionItem],
orders: list[OrderItem], orders: list[OrderItem],
deals: list[dict[str, str]],
) -> None: ) -> None:
"""用真实持仓、委托和成交恢复本地状态,不增加持久化字段。""" """用真实持仓、委托和成交恢复本地状态,不增加持久化字段。"""
position_list = list(positions) position_list = list(positions)

Binary file not shown.

View File

@@ -1,12 +1,13 @@
from __future__ import annotations from __future__ import annotations
import unittest import unittest
from datetime import datetime, timedelta
from tempfile import TemporaryDirectory from tempfile import TemporaryDirectory
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import patch from unittest.mock import patch
from libs.grid_take_profit import GridState, GridTrailingTracker 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.order import OrderBook, PlaceOrderRequest
from strategy.trend.positions import LOSS_TIERS, handle_loss, manage_positions from strategy.trend.positions import LOSS_TIERS, handle_loss, manage_positions
from strategy.trend.boot import RunOnce from strategy.trend.boot import RunOnce
@@ -22,6 +23,18 @@ class FakeClient:
return {"status": "success", "order_ref": f"broker-{len(self.orders)}"} 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): class TrendTests(unittest.TestCase):
def test_grid_states_and_account_isolation(self): def test_grid_states_and_account_isolation(self):
tracker = GridTrailingTracker(1) tracker = GridTrailingTracker(1)
@@ -39,6 +52,22 @@ class TrendTests(unittest.TestCase):
self.assertTrue(book.place(request)) self.assertTrue(book.place(request))
self.assertTrue(book.busy("000001.SZ", "BUY")) 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): def test_position_dataclasses_execute_without_type_error(self):
with TemporaryDirectory() as directory: with TemporaryDirectory() as directory:
state = State.for_strategy(directory, "trend", "A") state = State.for_strategy(directory, "trend", "A")
@@ -121,7 +150,7 @@ class TrendTests(unittest.TestCase):
client=client, client=client,
account_cfg=SimpleNamespace(min_cash_ratio=0.1), account_cfg=SimpleNamespace(min_cash_ratio=0.1),
global_cfg=SimpleNamespace(api_host="http://example"), global_cfg=SimpleNamespace(api_host="http://example"),
orders=SimpleNamespace(cancel_expired=lambda _client: None), orders=SimpleNamespace(refresh=lambda _client: None),
state=SimpleNamespace( state=SimpleNamespace(
codes=["A"], codes=["A"],
unresolved_codes=[], unresolved_codes=[],