From cf5f33ab9e5afa1015070f4c763b47a05035db9c Mon Sep 17 00:00:00 2001 From: yanweidong Date: Thu, 3 Sep 2026 15:27:11 +0800 Subject: [PATCH] fix bug --- py-client/strategy/trend/open.py | 16 ++++++++--- py-client/strategy/trend/order.py | 6 ++--- py-client/strategy/trend/state.py | 8 +++++- py-client/tests/test_trend.py | 44 +++++++++++++++++++++++++++++++ 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/py-client/strategy/trend/open.py b/py-client/strategy/trend/open.py index bfbdddb..8106ff5 100644 --- a/py-client/strategy/trend/open.py +++ b/py-client/strategy/trend/open.py @@ -43,7 +43,7 @@ def open_signal(run:Runtime, ticks, open_signals) -> None: # 当前价高于昨收价可开仓 if signal_config.gt_last_price_is_open and item.last_close>0 and price>item.last_close: try: - do_open(run,item.code,volume,item.signal_key) + do_open(run, item.code, volume, item.signal_key, price) log.info("[Open] %s 信号=%s,买入=%d股,原因=现价高于昨收", item.code, item.signal_key, volume) except RuntimeError as exc: log.info("[Open] %s 信号=%s,买入=%d股失败:%s", item.code, item.signal_key, volume, exc) @@ -56,7 +56,7 @@ def open_signal(run:Runtime, ticks, open_signals) -> None: continue try: - do_open(run,item.code,volume,item.signal_key) + do_open(run, item.code, volume, item.signal_key, price) log.info("[Open] %s 信号=%s,买入=%d股,原因=反弹已确认", item.code, item.signal_key, volume) except RuntimeError as exc: log.warning("[Open] %s 信号=%s,买入=%d股失败:%s", item.code, item.signal_key, volume, exc) @@ -64,7 +64,7 @@ def open_signal(run:Runtime, ticks, open_signals) -> None: log.exception("[Open] %s 信号=%s,买入=%d股异常", item.code, item.signal_key, volume) -def do_open(run:Runtime,code:str,volume:int,signal_key:str)->None: +def do_open(run: Runtime, code: str, volume: int, signal_key: str, price: float) -> None: """生成本地订单号并按最新价提交开仓委托。""" order_id = run.orders.new_order_id("base") request = PlaceOrderRequest( @@ -78,6 +78,16 @@ def do_open(run:Runtime,code:str,volume:int,signal_key:str)->None: if not run.orders.place(request): raise RuntimeError("订单提交失败") + run.state.set(StateItem( + code=code, + base_order_id=order_id, + base_qty=volume, + base_cost=round(price, 2), + base_status=STATUS_ING, + )) + run.state.save() + run.open_watch.forget(code) + def check_timezone(timezone: str, now: datetime | None = None) -> bool: """验证当前时间是否处于配置区间。 diff --git a/py-client/strategy/trend/order.py b/py-client/strategy/trend/order.py index 093e7e3..60f4983 100644 --- a/py-client/strategy/trend/order.py +++ b/py-client/strategy/trend/order.py @@ -41,9 +41,9 @@ class OrderBook: self.mutex = Lock() @staticmethod - def new_order_id(leg: str) -> str: - """生成短订单号,为 QMT 备注中的信号键预留空间。""" - return f"zt-{leg[:1]}-{secrets.token_hex(4)}" + def new_order_id(_leg: str) -> str: + """生成 ``trend-xxxxxxxx`` 格式的本地订单号。""" + return f"trend-{secrets.token_hex(4)}" def busy(self, code: str, side: str) -> bool: """判断证券是否存在仍在处理中的同方向委托。""" diff --git a/py-client/strategy/trend/state.py b/py-client/strategy/trend/state.py index 7475e14..b7647d6 100644 --- a/py-client/strategy/trend/state.py +++ b/py-client/strategy/trend/state.py @@ -108,7 +108,7 @@ class State: StateItem( code=position.stock_code, base_qty=position.volume, - base_cost=position.open_price, + base_cost=round(position.open_price, 2), base_status=STATUS_OK, ) ) @@ -165,6 +165,12 @@ class State: # reconciliation must therefore happen before stale state is removed. for code in list(self.codes): if code not in position_codes: + item = self.get(code) + if any( + order_id and order_id in orders_by_local_id + for order_id in (item.base_order_id, item.added_order_id) + ): + continue if self.delete(code): log.info("[状态] 已移除持仓状态,代码=%s", code) self.save() diff --git a/py-client/tests/test_trend.py b/py-client/tests/test_trend.py index b73c9c5..f0b43c1 100644 --- a/py-client/tests/test_trend.py +++ b/py-client/tests/test_trend.py @@ -10,6 +10,7 @@ from unittest.mock import patch from libs.grid_take_profit import GridState, GridTrailingTracker from sdk import APIError, Assets, OrderItem, Portfolio, PositionItem, Tick from strategy.trend.order import OrderBook, PlaceOrderRequest +from strategy.trend.open import do_open from strategy.trend.positions import LOSS_TIERS, handle_loss, manage_positions from strategy.trend.boot import RunOnce from strategy.trend.state import STATUS_OK, STATUS_UNKNOWN, State, StateItem @@ -42,6 +43,49 @@ class FailedOrderClient: class TrendTests(unittest.TestCase): + def test_trend_order_id_format(self): + self.assertRegex(OrderBook.new_order_id("base"), r"^trend-[0-9a-f]{8}$") + + def test_open_records_base_order_and_rounded_cost(self): + with TemporaryDirectory() as directory: + state = State.for_strategy(directory, "trend", "A") + forgotten = [] + runtime = SimpleNamespace( + client=FakeClient(), + orders=OrderBook(), + state=state, + open_watch=SimpleNamespace(forget=forgotten.append), + ) + + do_open(runtime, "000001.SZ", 100, "morning", 12.345) + + item = state.get("000001.SZ") + self.assertRegex(item.base_order_id, r"^trend-[0-9a-f]{8}$") + self.assertEqual(item.base_qty, 100) + self.assertEqual(item.base_cost, 12.35) + self.assertEqual(item.base_status, "ING") + self.assertEqual(forgotten, ["000001.SZ"]) + + def test_pending_base_order_survives_position_delay(self): + with TemporaryDirectory() as directory: + state = State.for_strategy(directory, "trend", "A") + state.set(StateItem( + "000001.SZ", + base_order_id="trend-12345678", + base_status="ING", + )) + pending = OrderItem( + "1", "000001.SZ", "BUY", "", "50", None, 100, + local_order_id="trend-12345678", + ) + + state.reconcile([], [pending]) + + self.assertEqual( + state.get("000001.SZ").base_order_id, + "trend-12345678", + ) + def test_grid_states_and_account_isolation(self): tracker = GridTrailingTracker(1) self.assertEqual(tracker.observe("A:code", 2.1).state, GridState.ARMED)