diff --git a/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc b/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc index 7d38c50..400ac1a 100644 Binary files a/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc and b/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc differ diff --git a/py-client/strategy/trend/__pycache__/order.cpython-311.pyc b/py-client/strategy/trend/__pycache__/order.cpython-311.pyc index 721a37a..d1bf66a 100644 Binary files a/py-client/strategy/trend/__pycache__/order.cpython-311.pyc and b/py-client/strategy/trend/__pycache__/order.cpython-311.pyc differ diff --git a/py-client/strategy/trend/__pycache__/state.cpython-311.pyc b/py-client/strategy/trend/__pycache__/state.cpython-311.pyc index 20d2d0c..1161cf6 100644 Binary files a/py-client/strategy/trend/__pycache__/state.cpython-311.pyc and b/py-client/strategy/trend/__pycache__/state.cpython-311.pyc differ diff --git a/py-client/strategy/trend/boot.py b/py-client/strategy/trend/boot.py index 79f0cb2..fc42ffe 100644 --- a/py-client/strategy/trend/boot.py +++ b/py-client/strategy/trend/boot.py @@ -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 diff --git a/py-client/strategy/trend/order.py b/py-client/strategy/trend/order.py index c666cf5..203713b 100644 --- a/py-client/strategy/trend/order.py +++ b/py-client/strategy/trend/order.py @@ -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 diff --git a/py-client/strategy/trend/state.py b/py-client/strategy/trend/state.py index 8c092fb..f8d3804 100644 --- a/py-client/strategy/trend/state.py +++ b/py-client/strategy/trend/state.py @@ -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 diff --git a/py-client/tests/__pycache__/test_trend.cpython-311.pyc b/py-client/tests/__pycache__/test_trend.cpython-311.pyc index d3df8ef..ea67ad1 100644 Binary files a/py-client/tests/__pycache__/test_trend.cpython-311.pyc and b/py-client/tests/__pycache__/test_trend.cpython-311.pyc differ diff --git a/py-client/tests/test_trend.py b/py-client/tests/test_trend.py index 02d37cc..91e54f6 100644 --- a/py-client/tests/test_trend.py +++ b/py-client/tests/test_trend.py @@ -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)