from typing import Any OP_BUY = 23 OP_SELL = 24 ORDER_TYPE_VOLUME = 1101 PR_TYPE_LATEST = 5 QUICK_TRADE_NOW = 2 ORDER_SIDE_BY_OFFSET = {"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"} # QMT 的发行数据按市场再分一层时使用的市场键。 _IPO_MARKETS = {"SH", "SZ", "BJ"} def _ipo_stock_code(code: Any, info: dict[str, Any]) -> str: """把 QMT 的证券代码键补成全码 ``600000.SH``。""" text = str(code or "").strip() if not text or "." in text: return text market = str( info.get("market") or info.get("exchange") or info.get("ExchangeID") or "" ).strip().upper() return f"{text}.{market}" if market else text def _ipo_entry(code: Any, info: dict[str, Any]) -> dict[str, Any]: """保留原始发行信息,并补出候选列表使用的 ``stock`` 字段。""" entry = dict(info) stock = _ipo_stock_code(code, entry) if stock: entry["stock"] = stock return entry def _ipo_candidates(response: Any) -> list[dict[str, Any]]: """把 ``/api/trade/ipo_data`` 的响应规范化为候选列表。 QMT 的 ``get_ipo_data(type)`` 返回 ``{证券代码: 发行信息}`` 字典,部分 版本再按市场分一层 ``{市场: {证券代码: 发行信息}}``,旧版服务端还会包 一层 ``{"data": ...}``。空响应表示当日没有可申购标的;无法识别的结构抛 ``ValueError``,避免把接口异常静默当成“今日无新股”。 """ if isinstance(response, dict) and len(response) == 1 and "data" in response: response = response["data"] if response is None: return [] if isinstance(response, list): # 列表逐项交给策略层校验,单条异常不影响其他候选。 return list(response) if not isinstance(response, dict): raise ValueError(f"unsupported IPO response type: {type(response).__name__}") if not response: return [] if not all(isinstance(value, dict) for value in response.values()): raise ValueError("IPO response values must be objects") candidates: list[dict[str, Any]] = [] for key, value in response.items(): market = str(key).strip().upper() if market in _IPO_MARKETS and all(isinstance(item, dict) for item in value.values()): for code, info in value.items(): entry = dict(info) entry.setdefault("market", market) candidates.append(_ipo_entry(code, entry)) else: candidates.append(_ipo_entry(key, value)) return candidates class TradeMixin: def passorder( self, op_type: int, stock_code: str = "", volume: int = 0, order_type: int = ORDER_TYPE_VOLUME, pr_type: int = PR_TYPE_LATEST, price: float = -1, quick_trade: int = QUICK_TRADE_NOW, strategy_name: str = "", order_id: str = "", stock: str = "", ) -> dict[str, Any]: # ``stock`` is retained for compatibility with the original IPO client. stock_code = str(stock_code or stock).strip() if not stock_code: raise ValueError("stock_code cannot be empty") return self._post_json( "/api/trade/passorder", { "opType": op_type, "orderType": order_type, "stockCode": stock_code, "prType": pr_type, "price": price, "volume": volume, "quickTrade": quick_trade, "strategyName": strategy_name, "orderId": order_id, }, ) def ipo_data(self, ipo_type: str = "STOCK") -> list[dict[str, Any]]: """Return today's IPO candidates from the QMT REST service. QMT 按证券代码返回字典,这里统一成候选列表,字段名保持不变。 """ response = self._post_json( "/api/trade/ipo_data", {"type": str(ipo_type).strip().upper()}, ) return _ipo_candidates(response) def cancel_by_id(self, order_id: str) -> dict[str, Any]: return self._post_json("/api/trade/cancel_by_id", {"order_id": order_id})