之前在用2.9版本的脚本策略scripttrader交易的时候,遇到了一个平今转换上的问题,即不会自动把Offset.CLOSE转换成Offset.CLOSETODAY或Offset.CLOSEYESTERDAY,最终导致被拒单。当然可以为每个策略自行做本地仓位记录,记录今仓和昨仓的量,然后在下单时直接指定平今平昨,但是更新起来会比较麻烦,不如调用vnpy\trader\converter.py来实现自动转换。一步一步排查之后,我发现了问题所在,斗胆在下面展示一下我的解决方案。
首先来看vnpy_scripttrader\engine.py里面的send_order()函数:
def send_order(
self,
vt_symbol: str,
price: float,
volume: float,
direction: Direction,
offset: Offset,
order_type: OrderType
) -> str:
""""""
contract: Optional[ContractData] = self.get_contract(vt_symbol)
if not contract:
return ""
req: OrderRequest = OrderRequest(
symbol=contract.symbol,
exchange=contract.exchange,
direction=direction,
type=order_type,
volume=volume,
price=price,
offset=offset,
reference=APP_NAME
)
vt_orderid: str = self.main_engine.send_order(req, contract.gateway_name)
return vt_orderid
这里在使用sell()或者cover()这两个平仓的函数时,他传入的Offset对象是Offset.CLOSE,这里没有对平今平昨进行转换,而传Offset.CLOSE默认是平昨,所以在没有昨仓时会被拒单。那么我们需要在这一步用trader\converter.py中的OffsetConverter.convert_order_request()进行转换,但是似乎脚本策略中初始化一个OffsetConverter对象后直接调用并不能做到平今转换,还需要对OffsetConverter以及PositionHolding进行修改。原始代码如下
class OffsetConverter:
""""""
def __init__(self, main_engine: MainEngine):
""""""
self.main_engine: MainEngine = main_engine
self.holdings: Dict[str, "PositionHolding"] = {}
def update_position(self, position: PositionData) -> None:
""""""
if not self.is_convert_required(position.vt_symbol):
return
holding = self.get_position_holding(position.vt_symbol)
holding.update_position(position)
def update_trade(self, trade: TradeData) -> None:
""""""
if not self.is_convert_required(trade.vt_symbol):
return
holding = self.get_position_holding(trade.vt_symbol)
holding.update_trade(trade)
def update_order(self, order: OrderData) -> None:
""""""
if not self.is_convert_required(order.vt_symbol):
return
holding = self.get_position_holding(order.vt_symbol)
holding.update_order(order)
def update_order_request(self, req: OrderRequest, vt_orderid: str) -> None:
""""""
if not self.is_convert_required(req.vt_symbol):
return
holding = self.get_position_holding(req.vt_symbol)
holding.update_order_request(req, vt_orderid)
def get_position_holding(self, vt_symbol: str) -> "PositionHolding":
""""""
holding = self.holdings.get(vt_symbol, None)
if not holding:
contract = self.main_engine.get_contract(vt_symbol)
holding = PositionHolding(contract)
self.holdings[vt_symbol] = holding
return holding
def convert_order_request(
self,
req: OrderRequest,
lock: bool,
net: bool = False
) -> List[OrderRequest]:
""""""
if not self.is_convert_required(req.vt_symbol):
return [req]
holding = self.get_position_holding(req.vt_symbol)
if lock:
return holding.convert_order_request_lock(req)
elif net:
return holding.convert_order_request_net(req)
elif req.exchange in [Exchange.SHFE, Exchange.INE]:
return holding.convert_order_request_shfe(req)
else:
return [req]
class PositionHolding:
""""""
def __init__(self, contract: ContractData):
""""""
self.vt_symbol: str = contract.vt_symbol
self.exchange: Exchange = contract.exchange
self.active_orders: Dict[str, OrderData] = {}
self.long_pos: float = 0
self.long_yd: float = 0
self.long_td: float = 0
self.short_pos: float = 0
self.short_yd: float = 0
self.short_td: float = 0
self.long_pos_frozen: float = 0
self.long_yd_frozen: float = 0
self.long_td_frozen: float = 0
self.short_pos_frozen: float = 0
self.short_yd_frozen: float = 0
self.short_td_frozen: float = 0
.......
def convert_order_request_shfe(self, req: OrderRequest) -> List[OrderRequest]:
""""""
if req.offset == Offset.OPEN:
return [req]
if req.direction == Direction.LONG:
pos_available = self.short_pos - self.short_pos_frozen
td_available = self.short_td - self.short_td_frozen
else:
pos_available = self.long_pos - self.long_pos_frozen
td_available = self.long_td - self.long_td_frozen
if req.volume > pos_available:
return []
elif req.volume <= td_available:
req_td = copy(req)
req_td.offset = Offset.CLOSETODAY
return [req_td]
else:
req_list = []
if td_available > 0:
req_td = copy(req)
req_td.offset = Offset.CLOSETODAY
req_td.volume = td_available
req_list.append(req_td)
req_yd = copy(req)
req_yd.offset = Offset.CLOSEYESTERDAY
req_yd.volume = req.volume - td_available
req_list.append(req_yd)
return req_list
......
不知道是什么原因,这个类中的holdings字典并不会在有交易时进行更新,所以get_position_holding(req.vt_symbol)得到的是初始值,以上期所合约订单转换为例,在调用convert_order_request_shfe(req)后,pos_available=0,返回空列表[]。为了解决这个问题,我们需要在convert_order_request()中执行holding=self.get_position_holding(req.vt_symbol)之前手动update_position()更新一下holdings字典。我修改后的代码如下:
def convert_order_request(
self,
req: OrderRequest,
lock: bool,
net: bool = False
) -> List[OrderRequest]:
""""""
if not self.is_convert_required(req.vt_symbol):
return [req]
pos_long = self.main_engine.get_position(req.vt_symbol+'.多')
pos_short = self.main_engine.get_position(req.vt_symbol+'.空')
if pos_long:
self.update_position(pos_long)
if pos_short:
self.update_position(pos_short)
holding = self.get_position_holding(req.vt_symbol)
if lock:
return holding.convert_order_request_lock(req)
elif net:
return holding.convert_order_request_net(req)
elif req.exchange in [Exchange.SHFE, Exchange.INE]:
return holding.convert_order_request_shfe(req)
else:
return [req]
这样就可以正确执行convert_order_request_shfe(req)了,我们再回到vnpy_scripttrader\engine.py中来加入一步平今转换,我的做法是在send_order()中初始化一个converter,但似乎直接在engine初始化时就加入一个converter会更好(为了实盘稳定我没有尝试)。修改之后的send_order()如下:
def send_order(
self,
vt_symbol: str,
price: float,
volume: float,
direction: Direction,
offset: Offset,
order_type: OrderType
) -> str:
""""""
contract: Optional[ContractData] = self.get_contract(vt_symbol)
if not contract:
return ""
req: OrderRequest = OrderRequest(
symbol=contract.symbol,
exchange=contract.exchange,
direction=direction,
type=order_type,
volume=volume,
price=price,
offset=offset,
reference=APP_NAME
)
try:
converter=OffsetConverter(self.main_engine)
req=converter.convert_order_request(req,lock=False,net=False)[0]
except:
pass
vt_orderid: str = self.main_engine.send_order(req, contract.gateway_name)
return vt_orderid
这样改之后从我目前的使用情况来看可以稳定解决平今转换的问题,如果有用脚本策略的大神有更好的修改意见欢迎讨论!