diff --git a/api/qmt_api_new.py b/api/qmt_api_new.py index 381fbda..46039f6 100644 --- a/api/qmt_api_new.py +++ b/api/qmt_api_new.py @@ -1123,7 +1123,7 @@ class AssetsHandler(BaseHandler): info = _data[0] if _data else None if not info: raise HTTPError(500, "Failed to get account data") - self.write(json.dumps({"data": info}, ensure_ascii=False)) + self.write(json.dumps({"total": round(info.m_dBalance, 2),"available": round(info.m_dAvailable, 2)}, ensure_ascii=False)) # get_trade_detail_data('account') - Query total assets diff --git a/py-client/libs/__pycache__/calc.cpython-311.pyc b/py-client/libs/__pycache__/calc.cpython-311.pyc index f7b26b8..70a6308 100644 Binary files a/py-client/libs/__pycache__/calc.cpython-311.pyc and b/py-client/libs/__pycache__/calc.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/__init__.cpython-311.pyc b/py-client/sdk/__pycache__/__init__.cpython-311.pyc index 842d9ff..e79a499 100644 Binary files a/py-client/sdk/__pycache__/__init__.cpython-311.pyc and b/py-client/sdk/__pycache__/__init__.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/account.cpython-311.pyc b/py-client/sdk/__pycache__/account.cpython-311.pyc index 5596a6f..e2aaf11 100644 Binary files a/py-client/sdk/__pycache__/account.cpython-311.pyc and b/py-client/sdk/__pycache__/account.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/models.cpython-311.pyc b/py-client/sdk/__pycache__/models.cpython-311.pyc index 2330f73..8c50173 100644 Binary files a/py-client/sdk/__pycache__/models.cpython-311.pyc and b/py-client/sdk/__pycache__/models.cpython-311.pyc differ diff --git a/py-client/sdk/__pycache__/trade.cpython-311.pyc b/py-client/sdk/__pycache__/trade.cpython-311.pyc index 3907e5a..3871555 100644 Binary files a/py-client/sdk/__pycache__/trade.cpython-311.pyc and b/py-client/sdk/__pycache__/trade.cpython-311.pyc differ diff --git a/py-client/strategy/ipo/__pycache__/boot.cpython-311.pyc b/py-client/strategy/ipo/__pycache__/boot.cpython-311.pyc index e500050..141828a 100644 Binary files a/py-client/strategy/ipo/__pycache__/boot.cpython-311.pyc and b/py-client/strategy/ipo/__pycache__/boot.cpython-311.pyc differ diff --git a/py-client/strategy/trend/__pycache__/__init__.cpython-311.pyc b/py-client/strategy/trend/__pycache__/__init__.cpython-311.pyc index c4e359b..502dd84 100644 Binary files a/py-client/strategy/trend/__pycache__/__init__.cpython-311.pyc and b/py-client/strategy/trend/__pycache__/__init__.cpython-311.pyc differ diff --git a/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc b/py-client/strategy/trend/__pycache__/boot.cpython-311.pyc index 6192430..7d38c50 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__/open.cpython-311.pyc b/py-client/strategy/trend/__pycache__/open.cpython-311.pyc index 15e7d46..029722e 100644 Binary files a/py-client/strategy/trend/__pycache__/open.cpython-311.pyc and b/py-client/strategy/trend/__pycache__/open.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 e194695..721a37a 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__/positions.cpython-311.pyc b/py-client/strategy/trend/__pycache__/positions.cpython-311.pyc index c6c3c8c..3ee802d 100644 Binary files a/py-client/strategy/trend/__pycache__/positions.cpython-311.pyc and b/py-client/strategy/trend/__pycache__/positions.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 ec5fb14..20d2d0c 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 9789485..79f0cb2 100644 --- a/py-client/strategy/trend/boot.py +++ b/py-client/strategy/trend/boot.py @@ -73,15 +73,15 @@ def StartTrend() -> None: ) assets = client.assets() _, positions = client.positions() + order_book = OrderBook() + order_book.refresh() storeState = State.for_strategy( config.global_config.qmt_data_dir, config.account_config.strategy, config.account_config.account_id, ) - orders = client.trade_detail_data("order") - deals = client.deals() - storeState.reconcile(positions, orders, deals) + storeState.reconcile(positions, order_book.data) # 获取本策略的信号开仓数据 signals = init_signals( @@ -93,7 +93,7 @@ def StartTrend() -> None: global_cfg=config.global_config, account_cfg=config.account_config, state=storeState, - orders=OrderBook(), + orders=order_book, open_watch=DipWatch(), add_watch=DipWatch(), profit_tracker=GridTrailingTracker(config.account_config.grid_step_pct), @@ -120,7 +120,7 @@ def RunOnce(run: Runtime, signals) -> None: # 1. 取消超过有效期仍未完成的委托订单。 try: - run.orders.cancel_expired(run.client) + run.orders.refresh(run.client) except Exception: logging.exception("取消过期订单失败") diff --git a/py-client/strategy/trend/order.py b/py-client/strategy/trend/order.py index 45afd74..c666cf5 100644 --- a/py-client/strategy/trend/order.py +++ b/py-client/strategy/trend/order.py @@ -12,6 +12,9 @@ from sdk import ORDER_SIDE_BY_OFFSET, Client, OrderItem # 表示委托仍在处理、可能继续成交的 QMT 状态。 BUSY_STATUSES = {"48", "49", "50", "51", "52", "55"} +COMPLETED_STATUSES = {"56"} +TRACKED_STATUSES = BUSY_STATUSES | COMPLETED_STATUSES +CANCELABLE_STATUSES = {"49", "50", "51", "52"} @dataclass(frozen=True, slots=True) @@ -32,7 +35,7 @@ class OrderBook: def __init__(self, lock_timeout_sec: float = 180, cancel_timeout_sec: float = 10) -> None: self.lock_timeout_sec = max(0.0, float(lock_timeout_sec)) self.cancel_timeout_sec = timedelta(seconds=cancel_timeout_sec) - self.data: dict[str, OrderItem] = {} + self.data: list[OrderItem] = [] self.lock: dict[str, float] = {} self.mutex = Lock() @@ -44,42 +47,45 @@ class OrderBook: def busy(self, code: str, side: str) -> bool: """判断证券是否存在仍在处理中的同方向委托。""" with self.mutex: - self._clear_expired_locks(datetime.now().timestamp()) key = f"{side}-{code}" return key in self.lock def refresh(self, client: Client) -> None: - """从 QMT 刷新当前委托明细和方向索引。""" + """从 QMT 刷新进行中和已完成委托,并撤销超时的活动委托。""" orders = client.trade_detail_data("order") - parsed_orders = [(f"{item.side}-{item.code}", item) for item in orders] - now_timestamp = datetime.now().timestamp() - with self.mutex: - self.data = {key: item for key, item in parsed_orders} - self.lock = { - key: ( + current = datetime.now() + now_timestamp = current.timestamp() + data: list[OrderItem] = [] + lock: dict[str, float] = {} + + for item in orders: + # 不处理状态不对的 + if item.status not in TRACKED_STATUSES: + continue + # 清理过期的 + if ( + item.created_at is not None + and item.status in CANCELABLE_STATUSES + and current - item.created_at > self.cancel_timeout_sec + ): + client.cancel_by_id(item.id) + continue + + # 缓存本次有效订单 + data.append(item) + + if item.status in BUSY_STATUSES: + key = f"{item.side}-{item.code}" + lock[key] = ( item.created_at.timestamp() if item.created_at is not None else now_timestamp ) - for key, item in parsed_orders - if item.status in BUSY_STATUSES - } - self._clear_expired_locks(now_timestamp) + - def cancel_expired(self, client: Any, now: datetime | None = None) -> None: - """尝试撤销超过有效期且具有委托编号的订单。""" - self.refresh(client) - current = now or datetime.now() - - # 使用快照遍历,避免网络调用期间长期持有互斥锁。 - for order in list(self.data.values()): - if ( - order.created_at is not None - and order.status in {"49", "50", "51", "52"} - and current - order.created_at > self.cancel_timeout_sec - and order.id - ): - client.cancel_by_id(order.id) + with self.mutex: + self.data = data + self.lock = lock def place(self, request: PlaceOrderRequest) -> bool: """按最新价提交委托,并立即写入本地方向锁。""" @@ -112,13 +118,3 @@ class OrderBook: self.data[key] = pending self.lock[key] = pending.created_at.timestamp() return True - - def _clear_expired_locks(self, now_timestamp: float) -> None: - """清理过期方向锁;调用方必须已持有 ``mutex``。""" - expired = [ - key - for key, created_at in self.lock.items() - if now_timestamp - created_at >= self.lock_timeout_sec - ] - for key in expired: - self.lock.pop(key, None) diff --git a/py-client/strategy/trend/state.py b/py-client/strategy/trend/state.py index 4fb0bd9..8c092fb 100644 --- a/py-client/strategy/trend/state.py +++ b/py-client/strategy/trend/state.py @@ -136,7 +136,6 @@ class State: self, positions: Iterable[PositionItem], orders: list[OrderItem], - deals: list[dict[str, str]], ) -> None: """用真实持仓、委托和成交恢复本地状态,不增加持久化字段。""" position_list = list(positions) diff --git a/py-client/tests/__pycache__/test_ipo.cpython-311.pyc b/py-client/tests/__pycache__/test_ipo.cpython-311.pyc new file mode 100644 index 0000000..4f49a41 Binary files /dev/null and b/py-client/tests/__pycache__/test_ipo.cpython-311.pyc differ diff --git a/py-client/tests/__pycache__/test_trend.cpython-311.pyc b/py-client/tests/__pycache__/test_trend.cpython-311.pyc index b7abf4a..d3df8ef 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 6f68611..02d37cc 100644 --- a/py-client/tests/test_trend.py +++ b/py-client/tests/test_trend.py @@ -1,12 +1,13 @@ from __future__ import annotations import unittest +from datetime import datetime, timedelta from tempfile import TemporaryDirectory from types import SimpleNamespace from unittest.mock import patch from libs.grid_take_profit import GridState, GridTrailingTracker -from sdk import Assets, PositionItem, Tick +from sdk import Assets, OrderItem, PositionItem, Tick from strategy.trend.order import OrderBook, PlaceOrderRequest from strategy.trend.positions import LOSS_TIERS, handle_loss, manage_positions from strategy.trend.boot import RunOnce @@ -22,6 +23,18 @@ class FakeClient: return {"status": "success", "order_ref": f"broker-{len(self.orders)}"} +class FakeOrderClient: + def __init__(self, orders): + self.orders = orders + self.canceled = [] + + def trade_detail_data(self, _datatype): + return self.orders + + def cancel_by_id(self, order_id): + self.canceled.append(order_id) + + class TrendTests(unittest.TestCase): def test_grid_states_and_account_isolation(self): tracker = GridTrailingTracker(1) @@ -39,6 +52,22 @@ class TrendTests(unittest.TestCase): self.assertTrue(book.place(request)) self.assertTrue(book.busy("000001.SZ", "BUY")) + def test_refresh_tracks_active_and_completed_and_cancels_expired(self): + old = datetime.now() - timedelta(seconds=20) + orders = [ + OrderItem("active", "A", "BUY", "", "49", old, 100), + OrderItem("completed", "B", "SELL", "", "56", old, 100), + OrderItem("canceled", "C", "BUY", "", "54", old, 100), + OrderItem("failed", "D", "BUY", "", "57", old, 100), + ] + client = FakeOrderClient(orders) + book = OrderBook(cancel_timeout_sec=10) + + book.refresh(client) + + self.assertEqual(set(book.data), {"BUY-A", "SELL-B"}) + self.assertEqual(client.canceled, ["active"]) + def test_position_dataclasses_execute_without_type_error(self): with TemporaryDirectory() as directory: state = State.for_strategy(directory, "trend", "A") @@ -121,7 +150,7 @@ class TrendTests(unittest.TestCase): client=client, account_cfg=SimpleNamespace(min_cash_ratio=0.1), global_cfg=SimpleNamespace(api_host="http://example"), - orders=SimpleNamespace(cancel_expired=lambda _client: None), + orders=SimpleNamespace(refresh=lambda _client: None), state=SimpleNamespace( codes=["A"], unresolved_codes=[],