update api,libs
This commit is contained in:
@@ -8,7 +8,7 @@ from pathlib import Path
|
||||
from threading import Lock
|
||||
from typing import Iterable
|
||||
|
||||
from sdk import Position
|
||||
from sdk import OrderItem, PositionItem
|
||||
|
||||
|
||||
# 委托状态:无操作、处理中、已完成。
|
||||
@@ -70,6 +70,16 @@ 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:
|
||||
@@ -80,12 +90,21 @@ class State:
|
||||
with self.lock:
|
||||
self.items[item.code] = item
|
||||
|
||||
def delete(self, code: str) -> None:
|
||||
"""删除证券状态;证券不存在时不报错。"""
|
||||
def delete(self, code: str) -> bool:
|
||||
"""删除已终结的证券状态,并返回是否实际删除。"""
|
||||
with self.lock:
|
||||
self.items.pop(code, None)
|
||||
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 sync_positions(self, positions: Iterable[Position]) -> 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:
|
||||
"""把尚未接管的真实持仓初始化为已完成底仓。
|
||||
|
||||
无证券代码、无持仓数量或成本无效的记录会被忽略。同步结束后
|
||||
@@ -115,29 +134,31 @@ class State:
|
||||
|
||||
def reconcile(
|
||||
self,
|
||||
positions: Iterable[Position],
|
||||
orders: list[dict[str, str]],
|
||||
positions: Iterable[PositionItem],
|
||||
orders: list[OrderItem],
|
||||
deals: list[dict[str, str]],
|
||||
) -> None:
|
||||
"""用真实持仓、委托和成交恢复本地状态,不增加持久化字段。"""
|
||||
position_list = list(positions)
|
||||
self.sync_positions(position_list)
|
||||
active_codes = {
|
||||
position_codes = {
|
||||
item.stock_code for item in position_list if item.volume > 0
|
||||
}
|
||||
for code in list(self.codes):
|
||||
if code not in active_codes:
|
||||
self.delete(code)
|
||||
|
||||
for code in list(self.codes):
|
||||
item = self.get(code)
|
||||
item.base_status = _reconcile_leg(
|
||||
item.base_order_id, item.base_status, orders, deals
|
||||
item.base_order_id, item.base_status, item.base_qty, orders, deals
|
||||
)
|
||||
item.added_status = _reconcile_leg(
|
||||
item.added_order_id, item.added_status, orders, deals
|
||||
item.added_order_id, item.added_status, item.added_qty, orders, deals
|
||||
)
|
||||
self.set(item)
|
||||
|
||||
# Opening orders normally have no position until their first fill. Order
|
||||
# reconciliation must therefore happen before stale state is removed.
|
||||
for code in list(self.codes):
|
||||
if code not in position_codes:
|
||||
self.delete(code)
|
||||
self.save()
|
||||
|
||||
def save(self) -> None:
|
||||
@@ -179,31 +200,70 @@ class State:
|
||||
def _reconcile_leg(
|
||||
local_order_id: str,
|
||||
current_status: str,
|
||||
orders: list[dict[str, str]],
|
||||
expected_qty: int,
|
||||
orders: list[OrderItem],
|
||||
deals: list[dict[str, str]],
|
||||
) -> str:
|
||||
if current_status != STATUS_ING or not local_order_id:
|
||||
if current_status not in {STATUS_ING, STATUS_UNKNOWN} or not local_order_id:
|
||||
return current_status
|
||||
if any(local_order_id in row.get("m_strRemark", "") for row in deals):
|
||||
return STATUS_OK
|
||||
order = next(
|
||||
(
|
||||
row for row in orders
|
||||
if local_order_id in row.get("m_strRemark", "")
|
||||
),
|
||||
None,
|
||||
)
|
||||
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
|
||||
traded = _as_int(order.get("m_nVolumeTraded"))
|
||||
status = str(order.get("m_nOrderStatus", ""))
|
||||
if traded > 0 and status not in {"48", "49", "50", "51", "52", "55"}:
|
||||
|
||||
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_CANCELED
|
||||
return STATUS_UNKNOWN if traded > 0 else STATUS_CANCELED
|
||||
if status in {"57", "58"}:
|
||||
return STATUS_FAILED
|
||||
return STATUS_ING
|
||||
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:
|
||||
|
||||
Reference in New Issue
Block a user