This commit is contained in:
2026-08-30 15:29:21 +08:00
parent 6334f10904
commit a946c5b53d
8 changed files with 38 additions and 109 deletions

View File

@@ -74,7 +74,7 @@ def StartTrend() -> None:
assets = client.assets()
_, positions = client.positions()
order_book = OrderBook()
order_book.refresh()
order_book.refresh(client)
storeState = State.for_strategy(
config.global_config.qmt_data_dir,
@@ -149,8 +149,7 @@ def RunOnce(run: Runtime, signals) -> None:
previous_state_codes = set(run.state.codes)
try:
broker_orders = run.client.trade_detail_data("order")
broker_deals = run.client.deals()
run.state.reconcile(positions, broker_orders, broker_deals)
run.state.reconcile(positions, broker_orders)
except Exception:
logging.exception("订单状态对账失败,本轮禁止自动交易")
return

View File

@@ -115,6 +115,6 @@ class OrderBook:
)
with self.mutex:
key = f"{side}-{request.code}"
self.data[key] = pending
self.data.append(pending)
self.lock[key] = pending.created_at.timestamp()
return True

View File

@@ -70,16 +70,6 @@ class State:
with self.lock:
return list(self.items)
@property
def unresolved_codes(self) -> list[str]:
"""返回存在处理中或未知订单状态的证券代码快照。"""
with self.lock:
return [
code
for code, item in self.items.items()
if _has_unresolved_order(item)
]
def get(self, code: str) -> StateItem:
"""获取指定证券的状态;不存在时抛出 KeyError。"""
with self.lock:
@@ -93,16 +83,8 @@ class State:
def delete(self, code: str) -> bool:
"""删除已终结的证券状态,并返回是否实际删除。"""
with self.lock:
item = self.items.get(code)
if item is not None and _has_unresolved_order(item):
return False
return self.items.pop(code, None) is not None
def has_unresolved_order(self, code: str) -> bool:
"""判断证券是否存在必须阻止自动下单的未决订单。"""
with self.lock:
item = self.items.get(code)
return item is not None and _has_unresolved_order(item)
def sync_positions(self, positions: Iterable[PositionItem]) -> None:
"""把尚未接管的真实持仓初始化为已完成底仓。
@@ -137,20 +119,39 @@ class State:
positions: Iterable[PositionItem],
orders: list[OrderItem],
) -> None:
"""用真实持仓委托和成交恢复本地状态,不增加持久化字段"""
"""用真实持仓委托恢复本地状态;拆分订单全部完成才算完成"""
position_list = list(positions)
self.sync_positions(position_list)
position_codes = {
item.stock_code for item in position_list if item.volume > 0
}
orders_by_local_id: dict[str, list[OrderItem]] = {}
for order in orders:
if order.local_order_id:
orders_by_local_id.setdefault(order.local_order_id, []).append(order)
for code in list(self.codes):
item = self.get(code)
item.base_status = _reconcile_leg(
item.base_order_id, item.base_status, item.base_qty, orders, deals
)
item.added_status = _reconcile_leg(
item.added_order_id, item.added_status, item.added_qty, orders, deals
)
for order_id_attr, status_attr in (
("base_order_id", "base_status"),
("added_order_id", "added_status"),
):
local_order_id = getattr(item, order_id_attr)
current_status = getattr(item, status_attr)
if (
current_status not in {STATUS_ING, STATUS_UNKNOWN}
or not local_order_id
):
continue
matching_orders = orders_by_local_id.get(local_order_id)
if matching_orders:
status = (
STATUS_OK
if all(order.status == "56" for order in matching_orders)
else STATUS_ING
)
setattr(item, status_attr, status)
self.set(item)
# Opening orders normally have no position until their first fill. Order
@@ -194,79 +195,3 @@ class State:
}
except (TypeError, ValueError) as exc:
raise ValueError(f"[状态] 状态字段无效: {exc}") from exc
def _reconcile_leg(
local_order_id: str,
current_status: str,
expected_qty: int,
orders: list[OrderItem],
deals: list[dict[str, str]],
) -> str:
if current_status not in {STATUS_ING, STATUS_UNKNOWN} or not local_order_id:
return current_status
matching_orders = [
order for order in orders if order.local_order_id == local_order_id
]
order = matching_orders[-1] if matching_orders else None
if order is None:
matching_deals = [
row for row in deals if _matches_local_order(row, local_order_id)
]
dealt = sum(_deal_volume(row) for row in matching_deals)
if expected_qty > 0 and dealt >= expected_qty:
return STATUS_OK
if expected_qty <= 0 and matching_deals:
return STATUS_OK
return STATUS_UNKNOWN
system_order_id = order.id.strip()
matching_deals = [
row
for row in deals
if (
system_order_id
and str(row.get("m_strOrderSysID") or "").strip() == system_order_id
)
or (not system_order_id and _matches_local_order(row, local_order_id))
]
dealt = sum(_deal_volume(row) for row in matching_deals)
traded = max(order.traded_volume, dealt)
ordered = order.volume or expected_qty
status = order.status
if ordered > 0 and traded >= ordered:
return STATUS_OK
if status in {"48", "49", "50", "51", "52", "55"}:
return STATUS_ING
if status in {"54", "56"}:
return STATUS_UNKNOWN if traded > 0 else STATUS_CANCELED
if status in {"57", "58"}:
return STATUS_UNKNOWN if traded > 0 else STATUS_FAILED
return STATUS_UNKNOWN
def _has_unresolved_order(item: StateItem) -> bool:
return item.base_status in {STATUS_ING, STATUS_UNKNOWN} or item.added_status in {
STATUS_ING,
STATUS_UNKNOWN,
}
def _matches_local_order(row: dict[str, str], local_order_id: str) -> bool:
remark = str(row.get("m_strRemark") or "")
return remark.split("|", 1)[0] == local_order_id
def _deal_volume(deal: dict[str, str]) -> int:
for key in ("m_nVolume", "m_nTradeVolume", "m_nVolumeTraded"):
volume = _as_int(deal.get(key))
if volume > 0:
return volume
return 0
def _as_int(value: object) -> int:
try:
return int(value or 0)
except (TypeError, ValueError):
return 0

View File

@@ -65,7 +65,7 @@ class TrendTests(unittest.TestCase):
book.refresh(client)
self.assertEqual(set(book.data), {"BUY-A", "SELL-B"})
self.assertEqual({item.id for item in book.data}, {"completed"})
self.assertEqual(client.canceled, ["active"])
def test_position_dataclasses_execute_without_type_error(self):
@@ -126,15 +126,20 @@ class TrendTests(unittest.TestCase):
second = handle_loss(runtime, position, Tick(last_price=5), -50, 5000)
self.assertIn("等待", second.message)
def test_reconcile_ing_order_from_deal(self):
def test_reconcile_split_orders_complete_only_when_all_are_status_56(self):
with TemporaryDirectory() as directory:
state = State.for_strategy(directory, "trend", "A")
position = PositionItem(stock_code="A", volume=100, open_price=10)
state.set(StateItem("A", base_order_id="local-1", base_status="ING"))
completed = OrderItem("1", "A", "BUY", "", "56", None, 50, "local-1")
processing = OrderItem("2", "A", "BUY", "", "50", None, 50, "local-1")
state.reconcile([position], [completed, processing])
self.assertEqual(state.get("A").base_status, "ING")
state.reconcile(
[position],
[],
[{"m_strRemark": "local-1|morning"}],
[completed, OrderItem("2", "A", "BUY", "", "56", None, 50, "local-1")],
)
self.assertEqual(state.get("A").base_status, STATUS_OK)