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