Source code for mlthon.order.order

import typing

from mlthon.basics import utils, logging
from mlthon.basics.defs import Side, OrderType, TIF, ExecInstructions
from mlthon.basics.price import Price
from mlthon.basics.qty import Qty


[docs]class Order(object):
[docs] def get_client_id(self) -> str: raise NotImplementedError
[docs] def get_exchange_id(self) -> str: raise NotImplementedError
[docs] def is_open(self) -> bool: raise NotImplementedError
[docs] def is_new_pending(self) -> bool: raise NotImplementedError
[docs] def is_modify_pending(self) -> bool: raise NotImplementedError
[docs] def is_cancel_pending(self) -> bool: raise NotImplementedError
[docs] def get_instrument_id(self): raise NotImplementedError
[docs] def get_side(self) -> Side: raise NotImplementedError
[docs] def get_price(self) -> Price: raise NotImplementedError
[docs] def get_leaves_qty(self) -> Qty: raise NotImplementedError
[docs] def get_pending_price(self) -> Price: raise NotImplementedError
[docs] def get_pending_qty(self) -> Qty: raise NotImplementedError
[docs] def get_attachment(self) -> typing.Any: raise NotImplementedError
[docs] def get_tif(self) -> TIF: raise NotImplementedError
[docs] def get_type(self) -> OrderType: raise NotImplementedError
[docs] def get_exec_instructions(self) -> ExecInstructions: raise NotImplementedError
[docs] def get_leaves_qty_after_modify_prep(self) -> Qty: raise NotImplementedError
[docs] def has_any_pending_request(self) -> bool: raise NotImplementedError
[docs] def is_cancelled(self) -> bool: raise NotImplementedError
class _Order(Order): # Constructor, equivalent to a "prepareNew". The order is pending until the creation request is either acked by # invoking onNewAck, or rejected in which case the order object should simply deleted def __init__(self, client_id: str, instrument_id: int, side: Side, price: Price, qty: Qty, order_type: OrderType, tif: TIF, exec_instr: ExecInstructions, attachment): self._log_ = logging.get_logger(self) self._client_id_ = client_id self._exchange_id_ = None self._instrument_id_ = instrument_id self._side_ = side self._price_ = price self._pending_qty_ = qty self._leaves_qty_ = Qty.zero() self._type_ = order_type self._tif_ = tif self._exec_instructions_ = exec_instr self._attachment_ = attachment self._accum_qty_ = Qty.zero() self._tmp_price_ = Price.zero() self._new_ts_ = utils.get_now_ts() self._cancel_ts_ = 0 self._awaiting_cancelled_ts_ = 0 # Awaiting order_cancelled since this timestamp self._modify_ts_ = 0 self._marked_for_deletion_ = False self._cancelled_ = False # Set to true when on_cancelled has been received @staticmethod def create_open_order(client_id: str, exchange_id: str, instrument_id: int, side: Side, price: Price, qty: Qty, order_type: OrderType, tif: TIF, exec_instr: ExecInstructions): order = _Order(client_id, instrument_id, side, price, Qty.zero(), order_type, tif, exec_instr, None) order._exchange_id_ = exchange_id order._new_ts_ = 0 order._exchange_id_ = exchange_id order._leaves_qty_ = qty return order # Prepare a modify for that order.The modified data is kept as pending until the modify is either acked by calling # onModifyAck(), or deleted by calling onModifyReject() def prepare_modify(self, new_price: Price, new_qty: Qty): if self._new_ts_ > 0 or self._cancel_ts_ > 0 or self._modify_ts_ > 0 or self._awaiting_cancelled_ts_ > 0 \ or self._marked_for_deletion_: return False self._modify_ts_ = utils.get_now_ts() self._tmp_price_ = new_price self._pending_qty_ = new_qty return True # Prepare the cancellation (via individual cancel) of the order def prepare_cancel(self): if self._new_ts_ > 0 or self._cancel_ts_ > 0 or self._modify_ts_ > 0 or self._awaiting_cancelled_ts_ > 0 \ or self._marked_for_deletion_: raise RuntimeError("Trying to prepare a cancellation on order with pending request or marked for deletion!") self._cancel_ts_ = utils.get_now_ts() # To be invoked upon acknowledgement of the new order def on_new_ack(self, exchange_id, price: Price, leave_qty: Qty): assert (self._new_ts_ > 0) self._new_ts_ = 0 self._exchange_id_ = exchange_id self._leaves_qty_ = leave_qty self._pending_qty_ = Qty.zero() if self._price_ != price: if self._side_ == Side.Buy: if price < self._price_: self._log_.warn("NewOrderAck price (" + str(price) + ") is improved on the requested price (" + str(self._price_) + ")") else: raise RuntimeError("NewOrderAck price (" + str(price) + ") does not match or improve the requested " "price (" + str(self._price_) + ")") else: if price > self._price_: self._log_.warn("NewOrderAck price (" + str(price) + ") is improved on the requested price (" + str(self._price_) + ")") else: raise RuntimeError("NewOrderAck price (" + str(price) + ") does not match or improve the requested " "price (" + str(self._price_) + ")") def on_new_reject(self): assert (self._new_ts_ > 0) self._new_ts_ = 0 self._leaves_qty_ = Qty.zero() self._pending_qty_ = Qty.zero() # To be invoked upon acknowledgement of the modify. When the modify is acked, the associated data passed to # prepare modify replaces the one that was passed to the constructor. def on_modify_ack(self, leaves_qty: Qty): assert (self._modify_ts_ > 0) self._modify_ts_ = 0 self._price_ = self._tmp_price_ if leaves_qty and not leaves_qty.is_null(): self._leaves_qty_ = leaves_qty self._tmp_price_ = Price.zero() self._pending_qty_ = Qty.zero() # To be invoked upon rejection of the modify.The associated data passed to prepareModify() is deleted. def on_modify_reject(self): assert (self._modify_ts_ > 0) self._modify_ts_ = 0 self._tmp_price_ = Price.zero() self._pending_qty_ = Qty.zero() def on_cancel_ack(self): assert (self._cancel_ts_ > 0) self._cancel_ts_ = 0 # self._marked_for_deletion_ = True # If not already cancelled, we record the timestamp at which we start waiting for the on_cancelled() invocation if not self._cancelled_: self._awaiting_cancelled_ts_ = utils.get_now_ts() # To be invoked upon rejection of the cancel def on_cancel_reject(self): assert (self._cancel_ts_ > 0) self._cancel_ts_ = 0 # To be called upon reception of a fill. Returns the quantity left. def on_execution(self, fill_qty: Qty, nullable_leaves_qty: typing.Optional[Qty]): if not self.is_new_pending(): if nullable_leaves_qty and not nullable_leaves_qty.is_null(): self._leaves_qty_ = nullable_leaves_qty else: self._leaves_qty_ -= fill_qty self._accum_qty_ += fill_qty if self._leaves_qty_ < Qty.zero(): raise RuntimeError("leaves_qty: " + str(self._leaves_qty_) + " is < 0!") return self._leaves_qty_ else: # We did not receive the order_ack yet if nullable_leaves_qty: self._pending_qty_ = nullable_leaves_qty else: self._pending_qty_ -= fill_qty if self._pending_qty_ < Qty.zero(): raise RuntimeError("pending_qty: " + str(self._pending_qty_) + " is < 0!") return self._pending_qty_ def on_cancelled(self): assert (self._cancelled_ is False) self._leaves_qty_ = Qty.zero() # self._marked_for_deletion_ = True self._awaiting_cancelled_ts_ = 0 self._cancelled_ = True # ------------------------ Getters ------------------------ def get_client_id(self) -> str: return self._client_id_ def get_exchange_id(self) -> str: return self._exchange_id_ def is_open(self) -> bool: return self.is_new_pending() or (self._leaves_qty_ > Qty.zero() and not self._cancelled_) def is_new_pending(self) -> bool: return self._new_ts_ > 0 def is_modify_pending(self) -> bool: return self._modify_ts_ > 0 def is_cancel_pending(self) -> bool: return self._cancel_ts_ > 0 def get_instrument_id(self): return self._instrument_id_ def get_side(self) -> Side: return self._side_ def get_price(self) -> Price: return self._price_ def get_leaves_qty(self) -> Qty: return self._leaves_qty_ def get_pending_price(self) -> Price: return self._tmp_price_ def get_pending_qty(self) -> Qty: return self._pending_qty_ def get_accum_qty(self) -> Qty: return self._accum_qty_ def get_attachment(self) -> typing.Any: return self._attachment_ def get_tif(self) -> TIF: return self._tif_ def get_type(self) -> OrderType: return self._type_ def get_exec_instructions(self) -> ExecInstructions: return self._exec_instructions_ # Return the quantity left at the time the last prepareModify() was made.This method returns null if not # called between a prepareModify() and a onModifyAck() or onModifyReject() def get_leaves_qty_after_modify_prep(self): pass # TODO: !!! # Return true if the order is marked for deletion. An order is marked for deletion if it is not open, but it still # has pending requests. For instance, if a modify is sent on an order, and right after a complete fill is received # the order is marked for deletion while waiting for the modify ack ( or reject) to come back. Once the modify # response is back, the order can be deleted since we do not expect any other info to come for that order. def is_marked_for_deletion(self) -> bool: return self._marked_for_deletion_ # Return true if a request has been pending for more than staleness_threshold_ms on an order. def is_stale(self, now_ts, staleness_threshold_ms) -> bool: return _Order.__is_stale(self._new_ts_, now_ts, staleness_threshold_ms) \ or _Order.__is_stale(self._modify_ts_, now_ts, staleness_threshold_ms) \ or _Order.__is_stale(self._cancel_ts_, now_ts, staleness_threshold_ms) \ or _Order.__is_stale(self._awaiting_cancelled_ts_, now_ts, staleness_threshold_ms) def is_cancelled(self) -> bool: return self._cancelled_ # Return true if any request is pending on that order def has_any_pending_request(self) -> bool: return self._new_ts_ > 0 or self._modify_ts_ > 0 or self._cancel_ts_ > 0 or self._awaiting_cancelled_ts_ > 0 # Stringify the order into a pretty string def __str__(self): return "[" + self._side_.name + " " + str(self._leaves_qty_) + " @ " + str(self._price_) + \ ", new_ts: " + str(self._new_ts_) + ", modify_ts: " + str(self._modify_ts_) + ", cancel_ts: " + \ str(self._cancel_ts_) + ", awaiting_cancelled_ts: " + str(self._awaiting_cancelled_ts_) + "]" def mark_for_deletion(self): if self._awaiting_cancelled_ts_ > 0: self._awaiting_cancelled_ts_ = 0 if self.has_any_pending_request(): raise RuntimeError("Order {} in corrupted state".format(self)) self._marked_for_deletion_ = True @staticmethod def __is_stale(ts, now_ts, staleness_threshold_ms): return ts > 0 and now_ts - ts > staleness_threshold_ms