feat(trader): PaperCtaEngine 策略适配器(拦截send_order→PaperOrder)

This commit is contained in:
2026-07-07 12:00:14 +08:00
parent b2c5d8fd79
commit f0d8fd2a03
2 changed files with 153 additions and 0 deletions
+88
View File
@@ -0,0 +1,88 @@
"""PaperCtaEnginevnpy_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) -> 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.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),
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_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