feat dev
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Binary file not shown.
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user