Files
2026-07-13 12:07:23 +08:00

403 lines
14 KiB
Python

from copy import copy
from typing import TYPE_CHECKING
from .object import (
ContractData,
OrderData,
TradeData,
PositionData,
OrderRequest
)
from .constant import Direction, Offset, Exchange
if TYPE_CHECKING:
from .engine import OmsEngine
class PositionHolding:
""""""
def __init__(self, contract: ContractData) -> None:
""""""
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 update_position(self, position: PositionData) -> None:
""""""
if position.direction == Direction.LONG:
self.long_pos = position.volume
self.long_yd = position.yd_volume
self.long_td = self.long_pos - self.long_yd
else:
self.short_pos = position.volume
self.short_yd = position.yd_volume
self.short_td = self.short_pos - self.short_yd
def update_order(self, order: OrderData) -> None:
""""""
if order.is_active():
self.active_orders[order.vt_orderid] = order
else:
if order.vt_orderid in self.active_orders:
self.active_orders.pop(order.vt_orderid)
self.calculate_frozen()
def update_order_request(self, req: OrderRequest, vt_orderid: str) -> None:
""""""
gateway_name, orderid = vt_orderid.split(".")
order: OrderData = req.create_order_data(orderid, gateway_name)
self.update_order(order)
def update_trade(self, trade: TradeData) -> None:
""""""
if trade.direction == Direction.LONG:
if trade.offset == Offset.OPEN:
self.long_td += trade.volume
elif trade.offset == Offset.CLOSETODAY:
self.short_td -= trade.volume
elif trade.offset == Offset.CLOSEYESTERDAY:
self.short_yd -= trade.volume
elif trade.offset == Offset.CLOSE:
if trade.exchange in {Exchange.SHFE, Exchange.INE}:
self.short_yd -= trade.volume
else:
self.short_td -= trade.volume
if self.short_td < 0:
self.short_yd += self.short_td
self.short_td = 0
else:
if trade.offset == Offset.OPEN:
self.short_td += trade.volume
elif trade.offset == Offset.CLOSETODAY:
self.long_td -= trade.volume
elif trade.offset == Offset.CLOSEYESTERDAY:
self.long_yd -= trade.volume
elif trade.offset == Offset.CLOSE:
if trade.exchange in {Exchange.SHFE, Exchange.INE}:
self.long_yd -= trade.volume
else:
self.long_td -= trade.volume
if self.long_td < 0:
self.long_yd += self.long_td
self.long_td = 0
self.long_pos = self.long_td + self.long_yd
self.short_pos = self.short_td + self.short_yd
# Update frozen volume to ensure no more than total volume
self.sum_pos_frozen()
def calculate_frozen(self) -> None:
""""""
self.long_pos_frozen = 0
self.long_yd_frozen = 0
self.long_td_frozen = 0
self.short_pos_frozen = 0
self.short_yd_frozen = 0
self.short_td_frozen = 0
for order in self.active_orders.values():
# Ignore position open orders
if order.offset == Offset.OPEN:
continue
frozen: float = order.volume - order.traded
if order.direction == Direction.LONG:
if order.offset == Offset.CLOSETODAY:
self.short_td_frozen += frozen
elif order.offset == Offset.CLOSEYESTERDAY:
self.short_yd_frozen += frozen
elif order.offset == Offset.CLOSE:
self.short_td_frozen += frozen
if self.short_td_frozen > self.short_td:
self.short_yd_frozen += (self.short_td_frozen
- self.short_td)
self.short_td_frozen = self.short_td
elif order.direction == Direction.SHORT:
if order.offset == Offset.CLOSETODAY:
self.long_td_frozen += frozen
elif order.offset == Offset.CLOSEYESTERDAY:
self.long_yd_frozen += frozen
elif order.offset == Offset.CLOSE:
self.long_td_frozen += frozen
if self.long_td_frozen > self.long_td:
self.long_yd_frozen += (self.long_td_frozen
- self.long_td)
self.long_td_frozen = self.long_td
self.sum_pos_frozen()
def sum_pos_frozen(self) -> None:
""""""
# Frozen volume should be no more than total volume
self.long_td_frozen = min(self.long_td_frozen, self.long_td)
self.long_yd_frozen = min(self.long_yd_frozen, self.long_yd)
self.short_td_frozen = min(self.short_td_frozen, self.short_td)
self.short_yd_frozen = min(self.short_yd_frozen, self.short_yd)
self.long_pos_frozen = self.long_td_frozen + self.long_yd_frozen
self.short_pos_frozen = self.short_td_frozen + self.short_yd_frozen
def convert_order_request_shfe(self, req: OrderRequest) -> list[OrderRequest]:
""""""
if req.offset == Offset.OPEN:
return [req]
if req.direction == Direction.LONG:
pos_available: float = self.short_pos - self.short_pos_frozen
td_available: float = 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: OrderRequest = copy(req)
req_td.offset = Offset.CLOSETODAY
return [req_td]
else:
req_list: list[OrderRequest] = []
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: OrderRequest = copy(req)
req_yd.offset = Offset.CLOSEYESTERDAY
req_yd.volume = req.volume - td_available
req_list.append(req_yd)
return req_list
def convert_order_request_lock(self, req: OrderRequest) -> list[OrderRequest]:
""""""
if req.direction == Direction.LONG:
td_volume: float = self.short_td
yd_available: float = self.short_yd - self.short_yd_frozen
else:
td_volume = self.long_td
yd_available = self.long_yd - self.long_yd_frozen
close_yd_exchanges: set[Exchange] = {Exchange.SHFE, Exchange.INE}
# If there is td_volume, we can only lock position
if td_volume and self.exchange not in close_yd_exchanges:
req_open: OrderRequest = copy(req)
req_open.offset = Offset.OPEN
return [req_open]
# If no td_volume, we close opposite yd position first
# then open new position
else:
close_volume: float = min(req.volume, yd_available)
open_volume: float = max(0, req.volume - yd_available)
req_list: list[OrderRequest] = []
if yd_available:
req_yd: OrderRequest = copy(req)
if self.exchange in close_yd_exchanges:
req_yd.offset = Offset.CLOSEYESTERDAY
else:
req_yd.offset = Offset.CLOSE
req_yd.volume = close_volume
req_list.append(req_yd)
if open_volume:
req_open = copy(req)
req_open.offset = Offset.OPEN
req_open.volume = open_volume
req_list.append(req_open)
return req_list
def convert_order_request_net(self, req: OrderRequest) -> list[OrderRequest]:
""""""
if req.direction == Direction.LONG:
pos_available: float = self.short_pos - self.short_pos_frozen
td_available: float = self.short_td - self.short_td_frozen
yd_available: float = self.short_yd - self.short_yd_frozen
else:
pos_available = self.long_pos - self.long_pos_frozen
td_available = self.long_td - self.long_td_frozen
yd_available = self.long_yd - self.long_yd_frozen
# Split close order to close today/yesterday for SHFE/INE exchange
if req.exchange in {Exchange.SHFE, Exchange.INE}:
reqs: list[OrderRequest] = []
volume_left: float = req.volume
if td_available:
td_volume: float = min(td_available, volume_left)
volume_left -= td_volume
td_req: OrderRequest = copy(req)
td_req.offset = Offset.CLOSETODAY
td_req.volume = td_volume
reqs.append(td_req)
if volume_left and yd_available:
yd_volume: float = min(yd_available, volume_left)
volume_left -= yd_volume
yd_req: OrderRequest = copy(req)
yd_req.offset = Offset.CLOSEYESTERDAY
yd_req.volume = yd_volume
reqs.append(yd_req)
if volume_left > 0:
open_volume: float = volume_left
open_req: OrderRequest = copy(req)
open_req.offset = Offset.OPEN
open_req.volume = open_volume
reqs.append(open_req)
return reqs
# Just use close for other exchanges
else:
reqs = []
volume_left = req.volume
if pos_available:
close_volume: float = min(pos_available, volume_left)
volume_left -= pos_available
close_req: OrderRequest = copy(req)
close_req.offset = Offset.CLOSE
close_req.volume = close_volume
reqs.append(close_req)
if volume_left > 0:
open_volume = volume_left
open_req = copy(req)
open_req.offset = Offset.OPEN
open_req.volume = open_volume
reqs.append(open_req)
return reqs
class OffsetConverter:
""""""
def __init__(self, oms_engine: "OmsEngine") -> None:
""""""
self.holdings: dict[str, PositionHolding] = {}
self.get_contract = oms_engine.get_contract
def update_position(self, position: PositionData) -> None:
""""""
if not self.is_convert_required(position.vt_symbol):
return
holding: PositionHolding | None = self.get_position_holding(position.vt_symbol)
if holding:
holding.update_position(position)
def update_trade(self, trade: TradeData) -> None:
""""""
if not self.is_convert_required(trade.vt_symbol):
return
holding: PositionHolding | None = self.get_position_holding(trade.vt_symbol)
if holding:
holding.update_trade(trade)
def update_order(self, order: OrderData) -> None:
""""""
if not self.is_convert_required(order.vt_symbol):
return
holding: PositionHolding | None = self.get_position_holding(order.vt_symbol)
if holding:
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: PositionHolding | None = self.get_position_holding(req.vt_symbol)
if holding:
holding.update_order_request(req, vt_orderid)
def get_position_holding(self, vt_symbol: str) -> PositionHolding | None:
""""""
holding: PositionHolding | None = self.holdings.get(vt_symbol, None)
if not holding:
contract: ContractData | None = self.get_contract(vt_symbol)
if contract:
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: PositionHolding | None = self.get_position_holding(req.vt_symbol)
if not holding:
return [req]
elif 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]
def is_convert_required(self, vt_symbol: str) -> bool:
"""
Check if the contract needs offset convert.
"""
contract: ContractData | None = self.get_contract(vt_symbol)
# Only contracts with long-short position mode requires convert
if not contract:
return False
elif contract.net_position:
return False
else:
return True