"""PaperCtaEngine:vnpy_ctastrategy CtaTemplate 的纸面适配器(spec §5)。 实现 CtaTemplate 所需的 cta_engine 接口(send_order/cancel_order), 拦截 send_order → 构造 PaperOrder → 收集到 pending_orders, 供 PaperEngine 撮合。参考 vnpy_ctastrategy BacktestingEngine 的策略桥接 (它也是假 cta_engine)。 vnpy_ctastrategy 是 pip 依赖(容器有,本机可能无)——本模块不顶部 import 它, 只在 load_strategy 时 lazy import;本机用 mock 策略测 send_order 收集逻辑。 """ import logging from .models import MatchSession, OrderSide, PaperOrder logger = logging.getLogger(__name__) def _direction_to_side(direction) -> OrderSide: """vnpy Direction.LONG/SHORT(或 '多'/'空')→ OrderSide。""" s = str(direction) if "LONG" in s or "多" in s: return OrderSide.BUY if "SHORT" in s or "空" in s: return OrderSide.SELL return OrderSide.BUY class PaperCtaEngine: """纸面 cta_engine:拦截 CtaTemplate.send_order 转 PaperOrder。""" def __init__(self, strategy_id: str, match_session: MatchSession | str = MatchSession.NEXT_OPEN, listing_days: int = 0, size: int = 1) -> None: self.strategy_id = strategy_id self.match_session = MatchSession(match_session) if isinstance(match_session, str) else match_session self.listing_days = listing_days self.size = size # 合约乘数:A 股 1 手=100 股 → size=100;mock 默认 1 self.strategy = None self.pending_orders: list[PaperOrder] = [] def set_strategy(self, strategy) -> None: self.strategy = strategy def send_order(self, strategy, direction, offset, price, volume, stop: bool = False, lock: bool = False, net: bool = False) -> list[str]: """拦截 CtaTemplate.send_order → 收集 PaperOrder。返回假 vt_orderids。""" del stop, lock, net # 未用(首版不支持 stop/lock/net) vt_symbol = getattr(strategy, "vt_symbol", "") symbol = vt_symbol.split(".")[0] if vt_symbol else getattr(strategy, "symbol", "") order = PaperOrder( strategy_id=self.strategy_id, symbol=symbol, side=_direction_to_side(direction), price=float(price), volume=int(volume) * self.size, # vnpy 策略 volume 单位=手,转股(A 股 ×100) is_market=True, match_session=self.match_session, listing_days=self.listing_days, ) self.pending_orders.append(order) return [f"paper.{self.strategy_id}.{len(self.pending_orders)}"] def cancel_order(self, vt_orderid) -> None: """首版简化:取消从 pending 移除(未撮合的)。""" if isinstance(vt_orderid, (list, tuple)): for oid in vt_orderid: self._cancel_one(oid) else: self._cancel_one(vt_orderid) def cancel_all(self, vt_symbol: str | None = None) -> None: """撤销所有挂单(纸面:清 pending;CtaTemplate.on_bar 开头常调)。""" self.pending_orders = [] def __getattr__(self, name: str): """兜底未实现的 cta_engine 方法(load_bar/put_event/write_log/send_email /get_data/load_tick 等)返回 no-op,避免 CtaTemplate 调用时 AttributeError。""" return lambda *a, **kw: None def _cancel_one(self, vt_orderid: str) -> None: try: idx = int(vt_orderid.rsplit(".", 1)[-1]) - 1 if 0 <= idx < len(self.pending_orders): self.pending_orders.pop(idx) except (ValueError, IndexError): logger.warning("cancel_order 找不到 %s", vt_orderid) def on_bar(self, bar) -> None: """逐根 bar:清空上一根 pending,转发策略 on_bar(策略内调 send_order 收集新单)。""" self.pending_orders = [] if self.strategy is not None: self.strategy.on_bar(bar) def pop_orders(self) -> list[PaperOrder]: """PaperEngine 撮合后取走 pending_orders。""" orders = self.pending_orders self.pending_orders = [] return orders