Files
vnpy/tests/test_engine.py
vn.py 5a11a4a6d5 [Add] lock offset splits and alpha backtest fills
Cover the current close, fill, and fallback behavior without changing the engines.
2026-10-04 17:09:08 +08:00

199 lines
6.1 KiB
Python

import time
from collections.abc import Callable
from vnpy.event import EventEngine
from vnpy.trader.constant import Direction, Exchange, Offset, OrderType, Product
from vnpy.trader.converter import OffsetConverter, PositionHolding
from vnpy.trader.engine import BaseEngine, MainEngine, OmsEngine
from vnpy.trader.gateway import BaseGateway
from vnpy.trader.object import (
CancelRequest,
ContractData,
OrderRequest,
PositionData,
SubscribeRequest,
)
class FakeGateway(BaseGateway):
default_name: str = "FAKE"
exchanges: list[Exchange] = [Exchange.SHFE]
def __init__(self, event_engine: EventEngine, gateway_name: str) -> None:
super().__init__(event_engine, gateway_name)
self.orders: list[OrderRequest] = []
self.cancels: list[CancelRequest] = []
def connect(self, setting: dict[str, str | int | float | bool]) -> None:
return
def close(self) -> None:
return
def subscribe(self, req: SubscribeRequest) -> None:
return
def send_order(self, req: OrderRequest) -> str:
self.orders.append(req)
return f"{self.gateway_name}.1"
def cancel_order(self, req: CancelRequest) -> None:
self.cancels.append(req)
def query_account(self) -> None:
return
def query_position(self) -> None:
return
def test_main_engine_routes_send_and_cancel_to_gateway() -> None:
main_engine: MainEngine = MainEngine()
try:
gateway: BaseGateway = main_engine.add_gateway(FakeGateway)
assert isinstance(gateway, FakeGateway)
assert main_engine.exchanges == [Exchange.SHFE]
request: OrderRequest = OrderRequest(
symbol="rb2410",
exchange=Exchange.SHFE,
direction=Direction.LONG,
type=OrderType.LIMIT,
volume=1,
price=3000,
offset=Offset.OPEN,
)
vt_orderid: str = main_engine.send_order(request, FakeGateway.default_name)
assert vt_orderid == "FAKE.1"
assert gateway.orders == [request]
cancel: CancelRequest = CancelRequest(
orderid="1",
symbol="rb2410",
exchange=Exchange.SHFE,
)
main_engine.cancel_order(cancel, FakeGateway.default_name)
assert gateway.cancels == [cancel]
assert main_engine.send_order(request, "MISSING") == ""
assert gateway.orders == [request]
finally:
main_engine.close()
def wait_until(ready: Callable[[], bool]) -> None:
deadline: float = time.monotonic() + 2
while time.monotonic() < deadline:
if ready():
return
time.sleep(0.01)
raise AssertionError("合约或持仓事件没有进入 OmsEngine")
def publish_shfe_short(gateway: FakeGateway, oms: OmsEngine) -> None:
contract: ContractData = ContractData(
gateway_name=FakeGateway.default_name,
symbol="rb2410",
exchange=Exchange.SHFE,
name="rb",
product=Product.FUTURES,
size=10,
pricetick=1,
)
position: PositionData = PositionData(
gateway_name=FakeGateway.default_name,
symbol="rb2410",
exchange=Exchange.SHFE,
direction=Direction.SHORT,
volume=8,
yd_volume=5,
)
gateway.on_contract(contract)
gateway.on_position(position)
def position_loaded() -> bool:
converter: OffsetConverter | None = oms.get_converter(FakeGateway.default_name)
if converter is None:
return False
holding: PositionHolding | None = converter.get_position_holding(contract.vt_symbol)
if holding is None:
return False
return holding.short_td == 3 and holding.short_yd == 5
wait_until(position_loaded)
def shfe_close_request() -> OrderRequest:
return OrderRequest(
symbol="rb2410",
exchange=Exchange.SHFE,
direction=Direction.LONG,
type=OrderType.LIMIT,
volume=5,
price=3500,
offset=Offset.CLOSE,
)
def test_oms_convert_order_request_splits_only_after_contract() -> None:
main_engine: MainEngine = MainEngine()
try:
gateway: BaseGateway = main_engine.add_gateway(FakeGateway)
assert isinstance(gateway, FakeGateway)
oms_engine: BaseEngine | None = main_engine.get_engine("oms")
assert isinstance(oms_engine, OmsEngine)
request: OrderRequest = shfe_close_request()
# 尚未收到该网关合约事件时,没有转换器,请求原样返回。
passthrough: list[OrderRequest] = oms_engine.convert_order_request(
request,
FakeGateway.default_name,
False,
False,
)
assert passthrough == [request]
assert passthrough[0] is request
publish_shfe_short(gateway, oms_engine)
converted: list[OrderRequest] = oms_engine.convert_order_request(
request,
FakeGateway.default_name,
False,
False,
)
parts: list[tuple[Offset, float]] = [
(item.offset, item.volume) for item in converted
]
assert parts == [
(Offset.CLOSETODAY, 3),
(Offset.CLOSEYESTERDAY, 2),
]
finally:
main_engine.close()
def test_send_order_keeps_one_gateway_order_when_conversion_splits() -> None:
main_engine: MainEngine = MainEngine()
try:
gateway: BaseGateway = main_engine.add_gateway(FakeGateway)
assert isinstance(gateway, FakeGateway)
oms_engine: BaseEngine | None = main_engine.get_engine("oms")
assert isinstance(oms_engine, OmsEngine)
request: OrderRequest = shfe_close_request()
publish_shfe_short(gateway, oms_engine)
converted: list[OrderRequest] = oms_engine.convert_order_request(
request,
FakeGateway.default_name,
False,
False,
)
assert len(converted) == 2
vt_orderid: str = main_engine.send_order(request, FakeGateway.default_name)
# 转换结果有两腿,send_order 仍只把原始请求交给网关一次。
assert vt_orderid == "FAKE.1"
assert gateway.orders == [request]
finally:
main_engine.close()