feat(trader): PaperCtaEngine 策略适配器(拦截send_order→PaperOrder)
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
"""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) -> 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
|
||||
@@ -0,0 +1,65 @@
|
||||
"""PaperCtaEngine 适配器测试(mock 策略,不依赖 vnpy_ctastrategy,spec §5)。"""
|
||||
import pytest
|
||||
from sanguo_trader.cta_adapter import PaperCtaEngine, _direction_to_side
|
||||
from sanguo_trader.models import MatchSession, OrderSide, PaperOrder
|
||||
|
||||
|
||||
class MockStrategy:
|
||||
"""模拟 CtaTemplate:on_bar 调 cta_engine.send_order。"""
|
||||
def __init__(self, engine, vt_symbol="600000.SSE"):
|
||||
self.cta_engine = engine
|
||||
self.vt_symbol = vt_symbol
|
||||
|
||||
def on_bar(self, bar):
|
||||
# 模拟策略:每根 bar 买 100 股 @10.0
|
||||
self.cta_engine.send_order(self, "LONG", "OPEN", 10.0, 100)
|
||||
|
||||
|
||||
def test_send_order_collects_paper_order():
|
||||
engine = PaperCtaEngine("s1")
|
||||
strat = MockStrategy(engine)
|
||||
engine.set_strategy(strat)
|
||||
engine.on_bar(None) # 触发策略 on_bar → send_order
|
||||
orders = engine.pop_orders()
|
||||
assert len(orders) == 1
|
||||
assert isinstance(orders[0], PaperOrder)
|
||||
assert orders[0].strategy_id == "s1"
|
||||
assert orders[0].symbol == "600000"
|
||||
assert orders[0].side == OrderSide.BUY
|
||||
assert orders[0].price == 10.0
|
||||
|
||||
|
||||
def test_match_session_propagates():
|
||||
engine = PaperCtaEngine("s1", match_session="current_close")
|
||||
strat = MockStrategy(engine)
|
||||
engine.set_strategy(strat)
|
||||
engine.on_bar(None)
|
||||
orders = engine.pop_orders()
|
||||
assert orders[0].match_session == MatchSession.CURRENT_CLOSE
|
||||
|
||||
|
||||
def test_on_bar_clears_previous_pending():
|
||||
engine = PaperCtaEngine("s1")
|
||||
engine.pending_orders.append(PaperOrder("s1", "600000", OrderSide.BUY, 10, 100))
|
||||
strat = MockStrategy(engine)
|
||||
engine.set_strategy(strat)
|
||||
engine.on_bar(None) # 应清空上一根,只留本根 1 单
|
||||
assert len(engine.pending_orders) == 1
|
||||
|
||||
|
||||
def test_cancel_order_removes_pending():
|
||||
engine = PaperCtaEngine("s1")
|
||||
strat = MockStrategy(engine)
|
||||
engine.set_strategy(strat)
|
||||
engine.on_bar(None)
|
||||
oids = engine.send_order(strat, "LONG", "OPEN", 11.0, 200) # 第 2 单
|
||||
assert len(engine.pending_orders) == 2
|
||||
engine.cancel_order(oids[0])
|
||||
assert len(engine.pending_orders) == 1
|
||||
|
||||
|
||||
def test_direction_mapping():
|
||||
assert _direction_to_side("LONG") == OrderSide.BUY
|
||||
assert _direction_to_side("SHORT") == OrderSide.SELL
|
||||
assert _direction_to_side("多") == OrderSide.BUY
|
||||
assert _direction_to_side("空") == OrderSide.SELL
|
||||
Reference in New Issue
Block a user